From bc866e5ad1847e84770680609cdeece522e976e9 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:14:00 +0800 Subject: [PATCH 01/83] H-MEASURE: measure the margin retentions against the kernel and TradingView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUDIT4 X14 rows G2-09 (M7), G2-10 (M8), G2-12 (M10), G2-13 (M11) and the E20 f1 probe-tail pin. tests/test_adapter_margin_schedule_differential.cpp runs the same bars and orders through the Pine adapter and through a bare kernel host whose margin hooks answer TradingView's money and slice (the twin's), so only the kernel's own scheduling, check kinds and opening gate differ; every section prints both sides and pins the measured difference: - M7: the kernel's AfterApplied point after a leveraged opening or a mid-bar add, and its BarOpen point on a carried commissioned POOC short, are points margin_check_allowed refuses. - M8: a gap-open breach is a market execution at the open, sized on the open's money; no kernel check kind books that row. - M10: the pre-open slice's own x4 parts from the kernel's AfterApplied point (and from ab9714be) on the one-contract band, the +1e-6 restore floor and the frozen signal-time units. - M11: AdmitWithHostMargin and initial_margin_fraction part both ways. - E20 f1 (ruled 2026-09-22): the probe-suppressed tail books the kernel's path liquidation and the adapter's post-script margin call. M7 is also measured against TradingView: four lab tv tapes (tests/fixtures/margin_entry_bar, BINANCE:ETHUSDT.P 15m, rangeProof covered) book the margin call of a leveraged opening ON ITS ENTRY BAR at that bar's low, as the kernel's AfterApplied point does; the adapter books it a bar late or not at all. That is a TradingView-visible divergence, pinned as recorded (not fixed here: the lane measures). Docs made true: ADR-0001 "Margin" (the pre-open slice does have a kernel point beside it) and its initial_margin_fraction row, native-engine.md's margin paragraph, pine-to-native.md and contributing-llm.md (M11), live-surface.md §3.2 (the tail's two routes; the dispatch_bar paragraph named helpers that no longer exist), design rows MG4 and MG15; two design anchors into native-engine.md re-anchored for the moved lines. No src/ or include/ change. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit bafefba06cebfc6989313e91c4a97e1a815a7239) INT26 pick note: the ADR and design pages conflicted on citation digits only (int16_digit_merge: 5 blocks, no text refused). The resolution is PAR-MARGIN's 6574de6d, the same pick onto the same tree (83d1ca7f): its two design citations of native-engine.md carried through the exact line map (2941-3009 -> 2963-3031). hunkcheck: every hunk of bafefba0 carried; the two pages differ from its diff in anchor digits only. --- docs/adr/0001-kernel-adapter-boundary.md | 18 +- docs/design/native-feature-parity.md | 8 +- docs/pages/contributing-llm.md | 2 +- docs/pages/live-surface.md | 24 +- docs/pages/native-engine.md | 26 +- docs/pages/pine-to-native.md | 3 +- tests/CMakeLists.txt | 21 + tests/fixtures/margin_entry_bar/README.md | 26 + tests/fixtures/margin_entry_bar/bars.inc | 34 + .../hm-m7a-eth-m20-k4-1010/meta.json | 16 + .../hm-m7a-eth-m20-k4-1010/metrics.json | 40 + .../hm-m7a-eth-m20-k4-1010/strategy.pine | 14 + .../hm-m7a-eth-m20-k4-1010/tv_trades.csv | 5 + .../hm-m7a-eth-m5-k16-0621/meta.json | 16 + .../hm-m7a-eth-m5-k16-0621/metrics.json | 40 + .../hm-m7a-eth-m5-k16-0621/strategy.pine | 14 + .../hm-m7a-eth-m5-k16-0621/tv_trades.csv | 3 + .../hm-m7a-eth-m5-k16-0922/meta.json | 16 + .../hm-m7a-eth-m5-k16-0922/metrics.json | 40 + .../hm-m7a-eth-m5-k16-0922/strategy.pine | 14 + .../hm-m7a-eth-m5-k16-0922/tv_trades.csv | 3 + .../hm-m7a-eth-m5-k16-1121/meta.json | 16 + .../hm-m7a-eth-m5-k16-1121/metrics.json | 40 + .../hm-m7a-eth-m5-k16-1121/strategy.pine | 14 + .../hm-m7a-eth-m5-k16-1121/tv_trades.csv | 3 + ...t_adapter_margin_schedule_differential.cpp | 1000 +++++++++++++++++ 26 files changed, 1437 insertions(+), 19 deletions(-) create mode 100644 tests/fixtures/margin_entry_bar/README.md create mode 100644 tests/fixtures/margin_entry_bar/bars.inc create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/meta.json create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/meta.json create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/meta.json create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/meta.json create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/tv_trades.csv create mode 100644 tests/test_adapter_margin_schedule_differential.cpp diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 8f96c92b..4fee08d4 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -248,10 +248,22 @@ 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:16804`), the pre-open admission slice and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16804`) and the 1x-long money call — plus the TV admission scopes (`src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`:67-72`, - `:79-136`). + `:79-136`). The pre-open admission slice stays too, though the kernel's `AfterApplied` point + after the open fill measures the same bar. Measured by R5 lane H-MEASURE + (`tests/test_adapter_margin_schedule_differential.cpp`: the same bars and orders through the + adapter and through a bare host whose hooks answer TradingView's money and slice): the pre-open + slice parts from the kernel's point, and from `ab9714be`, on the one-contract band, on the + restore's `+1e-6` floor and on the frozen signal-time units; a gap-open breach executes at the + open, sized on the open's money, where no kernel check kind does; the opening gate declines a + 1x long whose entry fee `AdmitWithHostMargin` admits, and admits a gap-up add the adapter + refuses against its signal-time equity. The scheduling also misses one class the kernel + reaches: a leveraged opening is never checked on its own entry bar. Four `lab tv` tapes + (`tests/fixtures/margin_entry_bar`) book TradingView's margin call on that bar at its low, as + the kernel's `AfterApplied` point does; the adapter books it a bar late or not at all — a + recorded divergence, open. - **Calculation timing.** The cadence itself is the kernel's: a `calc_on_order_fills` strategy projects `NativeCalculationTrigger::BarCloseAndFills` with TradingView's guard literal as `max_recalculations_per_point`. What stays are the specifics COOF adds on top — @@ -566,7 +578,7 @@ Every line number in this section names its symbol on this tree, which | `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13867`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14922`), `chart_day_key` (`pine_adapter.cpp:13764`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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). `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:13226-13227`): 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:2360-2362`) | design row MG3 (R5-1); the contract comment in `project()` | `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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | design row MG4 and the wave-4 ruling recorded in `project()`: a positive initial requirement would decline openings TradingView takes | `tests/test_native_precommit_view.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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end`): report shape, not a mark-to-market row (`sha256:71a6a8622f560fca3a27d37e098ea8b6f68f831abcca0b6c46ad004d3ffe9358` pine_adapter.cpp:2334-2341) | 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:2325-2327`) | 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:1583`): 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 sites the predicate leaves out (lower timeframe, lookahead, auxiliary, streams) keep the source evaluator | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 499faefb..49b4d96e 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -97,7 +97,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2361) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2947-2955 | 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:594 tests the resulting book native_execution_consumer.cpp:2961-2964 | 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:13226-13229) is TV. | | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6180-6226, cap left unset `max_open_lots` pine_adapter.cpp:2360-2362) is TV. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13280-13345 and leaves the kernel gate unset pine_adapter.cpp:13279-13281) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 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. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13280-13345 and leaves the kernel gate unset pine_adapter.cpp:13279-13281) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13935-13936, `margin_pct` pine_adapter.cpp:13343) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14035-14213, `submit_margin_call_units` pine_adapter.cpp:14174-14220, `schedule_margin_call_path` pine_adapter.cpp:18185-18262) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2943 **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:13966-14002) | **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:983 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. | @@ -108,7 +108,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | MG12 | Max intraday loss → flatten + block (`intraday_loss_breached` pine_adapter.cpp:13896-13910, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14922-14952) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14965-15199) | **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:13706-13785) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:13924, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13983, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14691-14731, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14215-14342, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14215) | n/a | A | — | F:D2 F:G4 O:D4 S:M4 | R5-1 | +| MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:13924, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13983, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14691-14731, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14215-14342, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14215) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, the restore's `+1e-6` floor and the frozen signal-time units (M10); `margin_check_allowed` never admits the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low — a recorded divergence, open (M7). | | MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3817-3884, `record_market_review` pine_adapter.cpp:15203-15261) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -293,7 +293,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2941-3009) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2963-3031) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `sha256:60a689e9bbb16d77370ee18557929c420ee846d162b7d2bcd5472ffd74409e69` runner/CMakeLists.txt:37-45, `sha256:e7b0011fb5e730a159692b9f8ab97b2d43bd386e7c4802f13a4ae4c1db083c80` check_native_include_independence.py:36-39, the tests at `sha256:2c88205b43a02dafe8915e74417743b3b3faa0c65595686ccd305386505bdf55` runner/CMakeLists.txt:98-128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -1352,7 +1352,7 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1620-1621) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6982-6994) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1642-1643) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6982-6994) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). 2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`sha256:d2bc11d04fb37fc9c5d8b87bef2f5438be7a5f5c8ff72da5fcf255500d92f5cb` native_execution_consumer.cpp:3491-3500). `born_on_remaining_path` (native_execution_consumer.cpp:5616-5620) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1659-1666) is calibrated against it. 3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is native_execution_consumer.cpp:6835-6842). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2557); valid in bar-open, applied and tick callbacks. diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 63fee8cb..3ab8405e 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -223,7 +223,7 @@ decision, and removing one is a regression: | the kernel's risk limits (`NativeRunSpec::risk`) and the adapter's `strategy.risk.*` | structurally, Pine's risk calls are per-bar statements that arrive after the spec has been digested; substantively, four measured divergences in the latch, the streak, the close price and the day key. | ADR 0001 ruling table, row `risk`; design `native-feature-parity.md:438` | | the kernel's `max_abs_units` and the adapter's `max_position_size` | the kernel caps the *resulting* book, TradingView gates the *live* book before the fill. | ADR 0001 ruling table, row `max_abs_units` | | the kernel's `max_open_lots` and Pine's `pyramiding` | Pine counts *entries per cycle*, the kernel counts *physical lots*; a resting source entry must not consume a lot slot before it fills. | ADR 0001 ruling table, row `max_open_lots` | -| the kernel's margin model and the adapter's money admission | the adapter answers TradingView's ten-significant-digit admission itself and declares a *maintenance-only* model, because a positive initial requirement would decline openings TradingView takes. | ADR 0001 ruling table, row `initial_margin_fraction` | +| the kernel's margin model and the adapter's money admission | the adapter answers TradingView's ten-significant-digit admission itself and declares a *maintenance-only* model, because a positive initial requirement would decline openings TradingView takes and admit adds it refuses (measured both ways, `tests/test_adapter_margin_schedule_differential.cpp`). | ADR 0001 ruling table, row `initial_margin_fraction` | | `NativeRunSpec::report_open_position_at_end` and the adapter's range-end rows | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it: report *shape*, not a mark-to-market row. | ADR 0001 ruling table, row `report_open_position_at_end`; design `native-feature-parity.md:615` | | `subscriptions` in the spec and the adapter's begin-time declaration | the adapter declares the same kernel subscriptions through a hook instead of the field, so a plain `request.security` site really is a kernel subscription. | ADR 0001 ruling table, row `subscriptions` | | the kernel's auxiliary feed and the adapter's auxiliary drive | the adapter's chart slice leaves pre-range coverage inert where the kernel folds by time, and evaluates after the bar's matching pass where the kernel delivers before it. | ADR 0001 ruling table, row `auxiliary_feed` | diff --git a/docs/pages/live-surface.md b/docs/pages/live-surface.md index 8e1a25d1..c002984e 100644 --- a/docs/pages/live-surface.md +++ b/docs/pages/live-surface.md @@ -120,14 +120,21 @@ before this flag existed. The last bar of the array fed to every subsequent `run()` does not calculate the script: the host's `on_bar` is not invoked for that bar, so neither is anything the script would have ordered on it. What the *broker* -owes that bar it still does, because the adapter settles the bar's close -whether or not the script ran. A margin call or an intraday-cap close that +owes that bar it still does: the kernel still matches a resting liquidation +on the bar's path, and the adapter still runs its post-script close pass +(`on_bar_close`) whether or not the script ran. A margin call or an +intraday-cap close that comes due against the forming bar is therefore **booked on that bar**, not deferred to the next non-suppressed run. Measured on a two-bar short tape whose second bar liquidates: with the flag on, the host is called once (bar 0 only) and the closed-trade row is still `Margin call @105.0000 qty=3.809524 t=2000` — the same row, at the same -timestamp, as the same tape with the flag off. +timestamp, as the same tape with the flag off. That row is the kernel's +path liquidation; the close pass books its own: a carried 9.5 @ 100 short +with a 0.1 % fee whose tail bar reaches 105 books the adapter's margin call +1.7504761904761923 @ 105 at the post-script checkpoint, flag on or off +(`tests/test_adapter_margin_schedule_differential.cpp`, E20 f1; `ab9714be` +booked neither route on the tail). This paragraph used to promise the opposite ("surfaces only at settlement") and to list `process_margin_call` among the steps that never run. The @@ -173,11 +180,12 @@ while either historical override is set, with `native stream cannot use historical probe/tail overrides`. A one-bar array is all tail, so `run(bars, 1)` under this flag calculates nothing. -Under `process_orders_on_close`, the pre-script carried-position margin -helpers `dispatch_bar` runs ahead of the script -(`tv_money_long_margin_call(…, carried_pooc_pre_close=true)` and -`process_carried_pooc_short_margin_before_script`) are skipped on the -suppressed last bar along with the script calculation itself. +Under `process_orders_on_close`, a carried position's margin checkpoints are +not skipped on the suppressed last bar: the bar-open slice, the kernel's path +liquidation and the adapter's post-script close pass all run there, as on any +settled bar (the E20 f1 rows of +`tests/test_adapter_margin_schedule_differential.cpp`). Only the script +calculation itself is suppressed. Default off (`on == 0`): every historical run stays byte-identical to before this flag existed. diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 25b6028c..75a5dc25 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1450,12 +1450,34 @@ the kernel's can reach: the bar-open mark checkpoint and the script-close pass (the kernel checks once per point, at the remaining path's adverse mark), the account-currency FX rollover revaluation (the adapter refuses the kernel's `FxRoll` point by kind: TradingView's rollover is its broker-open slice, taken -on the source's own sub-bar rate), the pre-open admission slice, and the +on the source's own sub-bar rate), and the one-contract 1×-long money call — which is not a maintenance liquidation -at all and fires on the favorable side of the path. Measured against the +at all and fires on the favorable side of the path. The pre-open admission +slice stays adapter-side as well, though the kernel's `AfterApplied` point +after the open fill does reach its bar. Measured against the adapter as it stood before the re-lowering, on the same books, bit for bit: `tests/test_adapter_margin_relower.cpp`. +Measured against the kernel's own points (R5 lane H-MEASURE, +`tests/test_adapter_margin_schedule_differential.cpp`): the same bars and +orders through the adapter and through a bare host whose hooks answer +TradingView's money and slice, so that only the kernel's own scheduling, +check kinds and opening gate differ. A gap-open breach books the adapter's +market execution at the open, sized on the open's money; the kernel's mark +check rests its slice at the remaining path's adverse extreme and +`CalculationOnly` executes at the calculation, so no kernel check kind books +that row. On the same money the pre-open slice and the kernel's +`AfterApplied` point part on the one-contract band, on the restore's `+1e-6` +floor and on the frozen signal-time units. The opening gate parts both ways: +the kernel declines a 1× long whose entry fee the adapter admits, and admits a +gap-up add the adapter refuses against its signal-time equity. And the +scheduling misses one class the kernel reaches: a leveraged opening (margin +below 100 %) is never checked on its own entry bar. Four `lab tv` tapes +(`tests/fixtures/margin_entry_bar`) book TradingView's margin call on that +bar at its low, as the kernel's `AfterApplied` point does; the adapter books +it a bar late or not at all. The row pins that divergence as recorded, not +ruled. + ### Risk limits `NativeRunSpec::risk` is the generic account-risk block, and it is opt-in. A diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 5ff1c047..678d6718 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -440,7 +440,8 @@ ruled **adapter-policy** (ADR-0001's `initial_margin_fraction` row): the adapter answers TradingView's ten-significant-digit money admission itself with `NativePrecommitVerdict::AdmitWithHostMargin` native_host.hpp:509, and declares a maintenance-only model, because a positive initial requirement would decline -openings TradingView takes. +openings TradingView takes — and admit adds TradingView refuses: the two gates +part both ways (`tests/test_adapter_margin_schedule_differential.cpp`, M11). ## Calculation context {#pine_to_native_map_context} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5002790e..fd9bf3b2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1316,6 +1316,14 @@ list(APPEND TEST_SOURCES test_native_current_execution_rate) # leg first (|H - O| < |O - L| is strict), and so does the observation. # Source-bound. list(APPEND TEST_SOURCES test_adapter_dual_entry_tie) +# R5 lane H-MEASURE (AUDIT4 X14): every adapter retention AUDIT4 §6 still read +# as "ruled, unmeasured", measured -- the same bars and orders through the Pine +# adapter and through a bare kernel host (a paired differential), and/or +# TradingView's own trades (`lab tv` tapes under tests/fixtures/, each +# directory's README.md names them). All source-bound. +# G2-09/-10/-12/-13 margin scheduling, market-execution checkpoints, the +# pre-open slice, opening admission; E20 f1's probe-tail margin call. +list(APPEND TEST_SOURCES test_adapter_margin_schedule_differential) # R4-D L4b-fix restores every removed ShortSeed, affordability, sizing and # deferred-close oracle as an individually compiled native-route twin. Keep @@ -2047,6 +2055,19 @@ if(TARGET test_offset_trail_quantized_arm) PINEFORGE_E5_FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/fixtures/offset_trail_arm") endif() +# R5 lane H-MEASURE: the rows that read TradingView tapes from the fixture tree. +foreach(_pf_hm_row IN ITEMS + "test_adapter_margin_schedule_differential|PINEFORGE_HM_M7A_FIXTURE_DIR|margin_entry_bar") + string(REPLACE "|" ";" _pf_hm_fields "${_pf_hm_row}") + list(GET _pf_hm_fields 0 _pf_hm_target) + list(GET _pf_hm_fields 1 _pf_hm_macro) + list(GET _pf_hm_fields 2 _pf_hm_dir) + if(TARGET ${_pf_hm_target}) + target_compile_definitions(${_pf_hm_target} PRIVATE + ${_pf_hm_macro}="${CMAKE_CURRENT_SOURCE_DIR}/fixtures/${_pf_hm_dir}") + endif() +endforeach() + # R5 E21 reads the kernel's own sources to prove BacktestEngine::marked_equity_at # is the one implementation of the marked equity. Kernel files only, so the # kernel-only profile runs the row too. diff --git a/tests/fixtures/margin_entry_bar/README.md b/tests/fixtures/margin_entry_bar/README.md new file mode 100644 index 00000000..9021f9f4 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/README.md @@ -0,0 +1,26 @@ +# Leveraged entry-bar margin tapes (R5 lane H-MEASURE, row M7 / G2-09) + +TradingView's own trades for one question AUDIT4 §6 left "ruled, unmeasured": +does TradingView check a LEVERAGED opening (margin below 100 %) on the bar its +entry fills, where the adapter's `margin_check_allowed` refuses the kernel's +AfterApplied point? `tests/test_adapter_margin_schedule_differential.cpp` +(section "M7 on tapes") replays every tape through the Pine adapter and through +a bare kernel host with TradingView's money and slice. + +Each directory is one `lab tv` export (pineforge-workflow, channel +`ws-report-v1`, `rangeProof: covered`), byte-identical: `strategy.pine`, +`tv_trades.csv` (times UTC+8), `meta.json`, `metrics.json`. The sha256 of each +tape is `metrics.json` `tvTradesCsvHash`; the campaign note is +`tv-tape--`. + +| probe | chart | margin, size | entry bar (UTC) | TradingView | +|---|---|---|---|---| +| `hm-m7a-eth-m5-k16-0621` | BINANCE:ETHUSDT.P 15 | 5 %, 16 x equity | 2025-06-21 21:30 | margin call 6.842 @2230 (the entry bar's low), the whole book | +| `hm-m7a-eth-m5-k16-0922` | same | 5 %, 16 x | 2025-09-22 06:00 | margin call 3.793 @4000.88, the whole book | +| `hm-m7a-eth-m5-k16-1121` | same | 5 %, 16 x | 2025-11-21 07:30 | margin call 5.806 @2642.65, the whole book | +| `hm-m7a-eth-m20-k4-1010` | same | 20 %, 4 x | 2025-10-10 21:15 | margin call 0.4544 @3400 (4 x 0.1136), the rest 0.6196 at the timeout @3895 | + +All four export windows are 2025-04-01 .. 2026-05-01; each script trades once. +`bars.inc` holds each event's signal bar through the entry bar + 4, from the +corpus feed `corpus/data/derived/ohlcv_ETH-USDT-USDT_15m.csv` materialized by +`scripts/derive_corpus_feeds.py` at corpus gitlink eede4a2. diff --git a/tests/fixtures/margin_entry_bar/bars.inc b/tests/fixtures/margin_entry_bar/bars.inc new file mode 100644 index 00000000..17a7d050 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/bars.inc @@ -0,0 +1,34 @@ +// ETH 15m bars (corpus-derived feed) per M7-A event: signal bar .. entry bar + 4. +// {timestamp_ms, open, high, low, close} +constexpr FeedBar kM7a_m5_k16_0621[6] = { + {1750540500000LL, 2394.21, 2397, 2296.97, 2338.34}, + {1750541400000LL, 2338.34, 2338.68, 2230, 2297.51}, + {1750542300000LL, 2297.51, 2301.5, 2274.47, 2280.35}, + {1750543200000LL, 2280.35, 2306.85, 2278.14, 2298.95}, + {1750544100000LL, 2298.94, 2305.61, 2292.36, 2301.61}, + {1750545000000LL, 2301.62, 2302.23, 2277.22, 2280.4}, +}; +constexpr FeedBar kM7a_m5_k16_0922[6] = { + {1758519900000LL, 4278.21, 4285, 4210, 4217.7}, + {1758520800000LL, 4217.7, 4224.69, 4000.88, 4152.54}, + {1758521700000LL, 4152.54, 4202.15, 4144.24, 4178.59}, + {1758522600000LL, 4178.6, 4196.75, 4170.94, 4191.45}, + {1758523500000LL, 4191.46, 4193.48, 4177.09, 4190}, + {1758524400000LL, 4190.01, 4214.7, 4183.36, 4190}, +}; +constexpr FeedBar kM7a_m5_k16_1121[6] = { + {1763709300000LL, 2778.6, 2780.59, 2753.33, 2755.45}, + {1763710200000LL, 2755.46, 2760.88, 2642.65, 2731.13}, + {1763711100000LL, 2731.13, 2733, 2701.42, 2718.66}, + {1763712000000LL, 2718.67, 2750.57, 2705, 2741.67}, + {1763712900000LL, 2741.67, 2753.18, 2733.46, 2734.82}, + {1763713800000LL, 2734.82, 2748.02, 2720.46, 2727.64}, +}; +constexpr FeedBar kM7a_m20_k4_1010[6] = { + {1760130000000LL, 3864.76, 3911.79, 3610, 3723.05}, + {1760130900000LL, 3723.05, 3789.87, 3400, 3617.44}, + {1760131800000LL, 3617.43, 3884.92, 3594.44, 3820.15}, + {1760132700000LL, 3822.2, 3980.68, 3788.68, 3941.17}, + {1760133600000LL, 3941.17, 3947.66, 3820, 3895.43}, + {1760134500000LL, 3895, 3913, 3832, 3866.7}, +}; diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/meta.json b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/meta.json new file mode 100644 index 00000000..db40b683 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/meta.json @@ -0,0 +1,16 @@ +{ + "title": "hm-m7a-eth-m20-k4-1010", + "slug": "hm-m7a-eth-m20-k4-1010", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T20:06:50.598Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01" + }, + "pine_sha256": "e3760bb45d6def409893a914b54ab27d4b3f1a4164a971dacb6a33f280100c34", + "tv_trades_sha256": "5904c2624a0219b943b699f38334453f61ebac1ecd6f5361c9ac6278d091149b", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/metrics.json b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/metrics.json new file mode 100644 index 00000000..2e250345 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-m7a-eth-m20-k4-1010", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "e3760bb45d6def409893a914b54ab27d4b3f1a4164a971dacb6a33f280100c34", + "tvTradesCsvHash": "5904c2624a0219b943b699f38334453f61ebac1ecd6f5361c9ac6278d091149b", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74424", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "5251ff3979740627989641ebf8b88d602e541f5c136929765f41b3cb54ba399f", + "decodedReportSha256": "81ecac82a281b9ee2875a086520a4748a399d66fd6314de1bf299156f35b21e5", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T20:06:50.597Z" +} diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/strategy.pine b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/strategy.pine new file mode 100644 index 00000000..9d9949b1 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/strategy.pine @@ -0,0 +1,14 @@ +// H-MEASURE M7-A (G2-09): does TradingView check a LEVERAGED opening on its entry bar? +// One long market entry, placed on the signal bar 2025-10-10 21:00 UTC, fills at the open of +// 2025-10-10 21:15 UTC, whose low sits far under that open. margin_long=20 (the Pine percent), +// quantity = 4 x equity / signal close: the entry bar's low breaches the requirement. +// Timeout close 3 bars after the entry bar. +//@version=6 +strategy("hm-m7a-eth-m20-k4-1010", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 10, 10, 21, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/tv_trades.csv b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/tv_trades.csv new file mode 100644 index 00000000..87b6a068 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m20-k4-1010/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-10-11 05:15,Margin call,3400,0.4544,1691.75392,-146.79391,-8.68,0,30.363008,1.79,-146.79391,-8.68,-146.79391,-14.68,0 +1,Entry long,2025-10-11 05:15,L,3723.05,0.4544,1691.75392,-146.79391,-8.68,0,30.363008,1.79,-146.79391,-8.68,-146.79391,-14.68,0 +2,Exit long,2025-10-11 06:15,timeout,3895,0.6196,2306.80178,106.54022,4.62,0,159.62755,6.92,-200.16177,-8.68,-40.2537,-4.03,4 +2,Entry long,2025-10-11 05:15,L,3723.05,0.6196,2306.80178,106.54022,4.62,0,159.62755,6.92,-200.16177,-8.68,-40.2537,-4.03,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/meta.json b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/meta.json new file mode 100644 index 00000000..f12ddf36 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/meta.json @@ -0,0 +1,16 @@ +{ + "title": "hm-m7a-eth-m5-k16-0621", + "slug": "hm-m7a-eth-m5-k16-0621", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T20:06:54.750Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01" + }, + "pine_sha256": "f61e9d0e864e1d47b4c7523c6690441351a5eca99c184164a68cd54327f42ba7", + "tv_trades_sha256": "82afdec0f73135dfa1af943f73db2cd6ee01a1fa2125935355b5a65cc7e85ca0", + "tv_trades_rows": 2 +} diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/metrics.json b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/metrics.json new file mode 100644 index 00000000..ee6815c5 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-m7a-eth-m5-k16-0621", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01", + "deepBacktesting": true, + "trades": 1, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "f61e9d0e864e1d47b4c7523c6690441351a5eca99c184164a68cd54327f42ba7", + "tvTradesCsvHash": "82afdec0f73135dfa1af943f73db2cd6ee01a1fa2125935355b5a65cc7e85ca0", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74425", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "c5a8eceffa659b3c008f8d4c316c4a634357e32baf6d4c2d0686852d1c701721", + "decodedReportSha256": "0ef387e8afad397a35bf5460a4ed90a04daf33f096bfc921955fff9c846712e8", + "closedTrades": 1, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T20:06:54.746Z" +} diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/strategy.pine b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/strategy.pine new file mode 100644 index 00000000..94608f3a --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/strategy.pine @@ -0,0 +1,14 @@ +// H-MEASURE M7-A (G2-09): does TradingView check a LEVERAGED opening on its entry bar? +// One long market entry, placed on the signal bar 2025-06-21 21:15 UTC, fills at the open of +// 2025-06-21 21:30 UTC, whose low sits far under that open. margin_long=5 (the Pine percent), +// quantity = 16 x equity / signal close: the entry bar's low breaches the requirement. +// Timeout close 3 bars after the entry bar. +//@version=6 +strategy("hm-m7a-eth-m5-k16-0621", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 6, 21, 21, 15) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(16 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/tv_trades.csv b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/tv_trades.csv new file mode 100644 index 00000000..66689658 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0621/tv_trades.csv @@ -0,0 +1,3 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-22 05:30,Margin call,2230,6.842,15998.92228,-741.26227,-4.63,0,2.32628,0.01,-741.26227,-4.63,-741.26227,-74.13,0 +1,Entry long,2025-06-22 05:30,L,2338.34,6.842,15998.92228,-741.26227,-4.63,0,2.32628,0.01,-741.26227,-4.63,-741.26227,-74.13,0 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/meta.json b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/meta.json new file mode 100644 index 00000000..9bf9d9b4 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/meta.json @@ -0,0 +1,16 @@ +{ + "title": "hm-m7a-eth-m5-k16-0922", + "slug": "hm-m7a-eth-m5-k16-0922", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T20:06:59.175Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01" + }, + "pine_sha256": "6b8e350eb37e6d2fdc9998b9a08b515962157150f1a33f8bc215f149f301769a", + "tv_trades_sha256": "a56518c2145a0aa788b494a247ad79dbafea2768ee688a8da0e3b3636c5f4b9c", + "tv_trades_rows": 2 +} diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/metrics.json b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/metrics.json new file mode 100644 index 00000000..f4ad3710 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-m7a-eth-m5-k16-0922", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01", + "deepBacktesting": true, + "trades": 1, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "6b8e350eb37e6d2fdc9998b9a08b515962157150f1a33f8bc215f149f301769a", + "tvTradesCsvHash": "a56518c2145a0aa788b494a247ad79dbafea2768ee688a8da0e3b3636c5f4b9c", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74426", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "53356ee3a42318af87f909e6a19f019d2edd87ab94e27a8df0c1626f19f72065", + "decodedReportSha256": "77b8e1c072a5cb48129e9435f63686855ccd08f804d48de8e9060a50b21c0418", + "closedTrades": 1, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T20:06:59.174Z" +} diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/strategy.pine b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/strategy.pine new file mode 100644 index 00000000..8be88522 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/strategy.pine @@ -0,0 +1,14 @@ +// H-MEASURE M7-A (G2-09): does TradingView check a LEVERAGED opening on its entry bar? +// One long market entry, placed on the signal bar 2025-09-22 05:45 UTC, fills at the open of +// 2025-09-22 06:00 UTC, whose low sits far under that open. margin_long=5 (the Pine percent), +// quantity = 16 x equity / signal close: the entry bar's low breaches the requirement. +// Timeout close 3 bars after the entry bar. +//@version=6 +strategy("hm-m7a-eth-m5-k16-0922", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 9, 22, 5, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(16 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/tv_trades.csv b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/tv_trades.csv new file mode 100644 index 00000000..dfd9d766 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-0922/tv_trades.csv @@ -0,0 +1,3 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-09-22 14:00,Margin call,4000.88,3.793,15997.7361,-822.39825,-5.14,0,26.51307,0.17,-822.39825,-5.14,-822.39825,-82.24,0 +1,Entry long,2025-09-22 14:00,L,4217.7,3.793,15997.7361,-822.39825,-5.14,0,26.51307,0.17,-822.39825,-5.14,-822.39825,-82.24,0 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/meta.json b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/meta.json new file mode 100644 index 00000000..00d6a815 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/meta.json @@ -0,0 +1,16 @@ +{ + "title": "hm-m7a-eth-m5-k16-1121", + "slug": "hm-m7a-eth-m5-k16-1121", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T20:07:03.128Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01" + }, + "pine_sha256": "ca7e3ebc0f95cb32d908376d3e81e1ff4d8fc443ebcf8ed33f21ccb99d7ad7f2", + "tv_trades_sha256": "9f1b6f0dc175e472a236fe34254599b53657d07cc261d7a468621a8f72c5e47c", + "tv_trades_rows": 2 +} diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/metrics.json b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/metrics.json new file mode 100644 index 00000000..4afcfa1d --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-m7a-eth-m5-k16-1121", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01", + "deepBacktesting": true, + "trades": 1, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "ca7e3ebc0f95cb32d908376d3e81e1ff4d8fc443ebcf8ed33f21ccb99d7ad7f2", + "tvTradesCsvHash": "9f1b6f0dc175e472a236fe34254599b53657d07cc261d7a468621a8f72c5e47c", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74427", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "7005b59c46088ace7dcc57158f088f5d63e6e99c5549937e56782475fd608440", + "decodedReportSha256": "bb23edecc9f08b7c1cb256f9db3aa353cdaef867cfbbaae96bd85a30cdd6f81e", + "closedTrades": 1, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T20:07:03.127Z" +} diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/strategy.pine b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/strategy.pine new file mode 100644 index 00000000..4dcdeb52 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/strategy.pine @@ -0,0 +1,14 @@ +// H-MEASURE M7-A (G2-09): does TradingView check a LEVERAGED opening on its entry bar? +// One long market entry, placed on the signal bar 2025-11-21 07:15 UTC, fills at the open of +// 2025-11-21 07:30 UTC, whose low sits far under that open. margin_long=5 (the Pine percent), +// quantity = 16 x equity / signal close: the entry bar's low breaches the requirement. +// Timeout close 3 bars after the entry bar. +//@version=6 +strategy("hm-m7a-eth-m5-k16-1121", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 11, 21, 7, 15) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(16 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/tv_trades.csv b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/tv_trades.csv new file mode 100644 index 00000000..44582680 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/hm-m7a-eth-m5-k16-1121/tv_trades.csv @@ -0,0 +1,3 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-11-21 15:30,Margin call,2642.65,5.806,15998.20076,-654.97485,-4.09,0,31.46852,0.20,-654.97485,-4.09,-654.97485,-65.50,0 +1,Entry long,2025-11-21 15:30,L,2755.46,5.806,15998.20076,-654.97485,-4.09,0,31.46852,0.20,-654.97485,-4.09,-654.97485,-65.50,0 \ No newline at end of file diff --git a/tests/test_adapter_margin_schedule_differential.cpp b/tests/test_adapter_margin_schedule_differential.cpp new file mode 100644 index 00000000..5f8b1371 --- /dev/null +++ b/tests/test_adapter_margin_schedule_differential.cpp @@ -0,0 +1,1000 @@ +/* + * test_adapter_margin_schedule_differential.cpp — lane H-MEASURE (AUDIT4 X14): + * the four margin retentions AUDIT4 §6 still judged "ruled, unmeasured" + * (G2-09 / M7, G2-10 / M8, G2-12 / M10, G2-13 / M11), each measured as a + * PAIRED DIFFERENTIAL, plus the pin of lane E20's finding 1 (a margin call + * due on the probe-suppressed tail is booked there). + * + * Every pair runs the same bars and the same orders through + * - the Pine adapter (a source::PineStrategyHost whose script is a lambda, + * margin calls on), and + * - a bare kernel host (NativeStrategyHost) whose margin model is the one + * the adapter projects (maintenance = the Pine percents, the mark check, + * the adapter's equity basis and level base) and whose two money hooks + * answer TradingView's requirement and slice exactly as + * tests/test_native_margin_hooks_twin.cpp does -- so the only policy left + * on the kernel side is the kernel's OWN: every check point admitted + * (margin_check_allowed defaults to true), its own check kinds, its own + * opening gate. + * Each section prints both sides' margin executions (bar, phase, time, units, + * price, origin) and final book, then asserts the measured difference. + * + * M7 (G2-09) scheduling. The kernel's AfterApplied point after an opening + * or an add, and its BarOpen point on a carried POOC short with fees, + * are points the adapter refuses; the kernel books there. + * M8 (G2-10) checkpoints as market executions. A gap-open breach: the + * adapter executes at the open, sized at the open; the kernel has no + * check kind that does -- its mark check rests at the adverse extreme, + * its CalculationOnly executes at the calculation. + * M10 (G2-12) the pre-open slice. Same money through the pre-open x4 + * (schedule_preopen_margin_slice), through the shared rule + * (source_margin_units via the kernel's point) and through the kernel's + * AfterApplied point: they part on the one-contract band, on the +1e-6 + * restore fudge, and on the frozen (signal-time) units. + * M11 (G2-13) opening admission. AdmitWithHostMargin admits an opening the + * kernel's initial-margin gate declines (a percent entry fee), and + * refuses an add the kernel admits (signal-time equity vs marked). + * E20 f1: under set_probe_suppress_tail_logic the post-script close + * checkpoint still runs on the forming bar (ruling of 2026-09-22, + * docs/pages/live-surface.md §3.2). + * M7 on TradingView's tapes (tests/fixtures/margin_entry_bar): a leveraged + * opening whose entry bar breaches. TradingView books the margin call + * on the entry bar at its low (4 of 4 tapes), and so does the kernel's + * AfterApplied point with TradingView's money; the adapter checks the + * entry bar of a leveraged opening never -- a RECORDED divergence, pinned. + * + * Source-bound (includes pineforge/source): release profile only. + */ +#include "native_margin_hooks_fixture.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PINEFORGE_HM_M7A_FIXTURE_DIR +#error "PINEFORGE_HM_M7A_FIXTURE_DIR must name tests/fixtures/margin_entry_bar" +#endif + +using namespace r4_test; +using namespace l4b_fixture; + +namespace { + +struct FeedBar { + std::int64_t ts; + double open, high, low, close; +}; +#include "fixtures/margin_entry_bar/bars.inc" + +// ------------------------------------------------------------ TV policy +// The twin's TradingView money and slice (test_native_margin_hooks_twin.cpp, +// restating pine_adapter.cpp source_margin_money / source_margin_units). +double tv_money_round(double value) { + if (!std::isfinite(value) || value == 0.0) return value; + const double magnitude = std::floor(std::log10(std::abs(value))); + const double scale = std::pow(10.0, 9.0 - magnitude); + const double rounded = std::floor(std::abs(value) * scale + 0.5) / scale; + return value < 0.0 ? -rounded : rounded; +} +double tv_floor_grid(double units, double grid) { + if (!std::isfinite(units) || units <= 0.0) return 0.0; + if (!(grid > 0.0)) return units; + const double floored = std::floor(units / grid + 1e-6) * grid; + return floored < units ? floored : units; +} + +struct Config { + double capital = 1000.0; + double margin_long = 50.0; + double margin_short = 50.0; + double qty_step = 0.0; + double mintick = 0.01; + bool pooc = false; + CommissionType commission = CommissionType::PERCENT; + double commission_value = 0.0; + int pyramiding = 1; + QtyType qty_type = QtyType::FIXED; + double qty_value = 1.0; +}; + +std::vector tape(std::initializer_list> rows) { + std::vector out; + int index = 0; + for (const auto& r : rows) out.push_back(ohlc(index++, r[0], r[1], r[2], r[3])); + return out; +} + +const char* phase_name(NativePathPhase phase) { + switch (phase) { + case NativePathPhase::Open: return "Open"; + case NativePathPhase::High: return "High"; + case NativePathPhase::Low: return "Low"; + case NativePathPhase::Close: return "Close"; + case NativePathPhase::None: break; + } + return "None"; +} + +// One margin execution as the event record shows it, on either side: the +// adapter's own slices (labels __margin_call__ / __margin_preopen__) and +// every kernel-originated liquidation. +struct MarginFill { + int bar = -1; + NativePathPhase phase = NativePathPhase::None; + std::int64_t time = 0; + double units = 0.0; + double price = 0.0; + bool kernel = false; + std::string label; +}; + +std::vector margin_fills(const NativeStrategyHost& host) { + std::vector out; + for (const auto& row : host.native_events(0)) { + if (!row.command) continue; + const auto* e = std::get_if(&*row.command); + if (!e || !(e->closed_units > 0.0)) continue; + const bool kernel = e->definition->origin == no::RequestOrigin::KernelLiquidation; + if (!kernel && e->request().label.rfind("__margin", 0) != 0) continue; + out.push_back({e->cursor.point.interval_index, e->cursor.point.path_phase, + e->effective_time_ms(), e->closed_units, e->resolved_price, kernel, + e->request().label}); + } + return out; +} + +std::vector rejections(const NativeStrategyHost& host) { + std::vector out; + for (const auto& row : host.native_events(0)) { + if (!row.command) continue; + if (const auto* e = std::get_if(&*row.command)) out.push_back(*e); + } + return out; +} + +void print_side(const char* side, const std::vector& fills, double position, + double equity) { + std::printf(" %-9s %zu margin fill(s), position %.17g, equity %.17g\n", side, fills.size(), + position, equity); + for (const auto& f : fills) { + std::printf(" bar %d %-5s t=%lld %.17g @ %.17g %s (%s)\n", f.bar, + phase_name(f.phase), static_cast(f.time), f.units, f.price, + f.kernel ? "kernel liquidation" : "adapter execution", f.label.c_str()); + } +} + +bool same_value(double a, double b) { + return std::abs(a - b) <= 1e-12 * std::max(1.0, std::abs(b)); +} +void check_fill(const MarginFill& f, int bar, NativePathPhase phase, double units, double price, + bool kernel) { + CHECK(f.bar == bar); + CHECK(f.phase == phase); + CHECK(same_value(f.units, units)); + CHECK(same_value(f.price, price)); + CHECK(f.kernel == kernel); +} + +// ------------------------------------------------------------ the adapter side +struct PineSide final : source::PineStrategyHost { + std::function script; + int calls = 0; + explicit PineSide(const Config& c) { + auto& tv = fixture_configuration(); + tv.initial_capital = c.capital; + tv.default_qty_type = static_cast(c.qty_type); + tv.default_qty_value = c.qty_value; + tv.commission_type = static_cast(c.commission); + tv.commission_value = c.commission_value; + tv.margin_long = c.margin_long; + tv.margin_short = c.margin_short; + tv.process_orders_on_close = c.pooc; + tv.pyramiding = c.pyramiding; + initial_capital_ = c.capital; + qty_step_ = c.qty_step; + syminfo_mintick_ = c.mintick; + set_margin_call_enabled(true); + fixture_retain_all_events(); + } + void on_source_bar(const Bar&) override { + ++calls; + if (script) script(*this, pine_bar_index()); + } + void entry(const char* id, bool is_long, double limit, double stop, double qty) { + strategy_entry(id, is_long, limit, stop, qty); + } + void close(const char* id) { strategy_close(id); } + int entry_bar_index() const { return open_trade_entry_bar_index(0); } + double position() const { return physical_position().signed_units; } + double equity() const { return initial_capital_ + net_profit_sum_; } + int run_bars(const std::vector& bars) { + run(bars.data(), static_cast(bars.size())); + CHECK(last_error().empty()); + return trade_count(); + } +}; + +// ------------------------------------------------------------ the kernel side +struct KernelSide final : Host { + Config config; + int bars = 0; + std::function script; + std::function gate; // unset: every point + void on_native_bar(const Bar& bar, const NativeDecisionContext& context) override { + Host::on_native_bar(bar, context); + if (script) script(*this, bars); + ++bars; + } + bool margin_check_allowed(const NativeMarginCheckPoint& point) const override { + return gate ? gate(point) : true; + } + std::optional resolve_margin_requirement( + const NativeMarginRequirementView& view) const override { + if (!(config.qty_step > 0.0)) return std::nullopt; + const double lot_value = config.qty_step * view.mark; + if (!std::isfinite(lot_value) || !(lot_value < 1.0)) return std::nullopt; + NativeMarginDecision decision; + decision.required = tv_money_round(view.required); + decision.equity = view.equity; + return decision; + } + std::optional resolve_margin_call_units( + const NativeMarginCallView& view) const override { + const bool short_side = view.position.signed_units < 0.0; + const double pct = short_side ? config.margin_short : config.margin_long; + const double unit_margin = view.mark * pct / 100.0; + if (!(unit_margin > 0.0)) return 0.0; + const double raw_minimum = (view.required - view.equity) / unit_margin; + if (!(raw_minimum > 1e-10) || !std::isfinite(raw_minimum)) return 0.0; + const double book = std::abs(view.position.signed_units); + double minimum = raw_minimum; + if (config.qty_step > 0.0) + minimum = std::floor(raw_minimum / config.qty_step) * config.qty_step; + double units = minimum > 0.0 ? 4.0 * minimum : 0.0; + if (units > 0.0 && config.qty_step > 0.0) + units = std::floor(units / config.qty_step + 1e-6) * config.qty_step; + if (!(units > 0.0) && config.qty_step > 0.0 && config.qty_step <= 1.0 + && raw_minimum > 1e-10 && raw_minimum < 1.0) { + const double candidate = std::min(1.0, book); + const double rounded = tv_floor_grid(candidate, config.qty_step); + const double guard = std::max(1e-12, std::abs(candidate) * 1e-12); + if (candidate >= book - guard || std::abs(rounded - candidate) <= guard) + units = candidate; + } + units = std::min(book, units); + return units > 1e-10 && std::isfinite(units) ? units : 0.0; + } + // The slice books at the mark it rested on (MG-H), as the adapter's does. + no::ExecutionTerms resolve_execution_terms( + const NativeExecutionTermsFacts& facts) const override { + if (!facts.definition + || facts.definition->origin != no::RequestOrigin::KernelLiquidation) { + return Host::resolve_execution_terms(facts); + } + const double fire = facts.trigger_level ? *facts.trigger_level : facts.raw_price; + return {fire, std::nullopt, no::OpeningShape::Transact}; + } + double position() const { return physical_position().signed_units; } + void run_bars(const NativeRunSpec& spec, const std::vector& b) { + REQUIRE(configure_native(spec).status == NativeSetupStatus::Applied); + run(b.data(), static_cast(b.size())); + CHECK(last_error().empty()); + CHECK(native_state().kind == NativeLifecycleKind::Completed); + } +}; + +// The margin model PineExecutionAdapter::project() declares (pine_adapter.cpp +// project(): maintenance-only, the mark check, the equity basis, RealizedOnly) +// with `initial` as the per-side opening fraction (0 = the host admits). +NativeRunSpec pine_margin_spec(const char* key, const Config& c, double initial, + NativeLiquidationCheck check = + NativeLiquidationCheck::PathAdverseExtremeMark) { + NativeRunSpec s = margin_spec(key); + s.initial_capital = c.capital; + s.price_tick = c.mintick; + switch (c.commission) { + case CommissionType::PERCENT: s.fee_kind = NativeFeeKind::Percent; break; + case CommissionType::CASH_PER_ORDER: s.fee_kind = NativeFeeKind::CashPerExecution; break; + case CommissionType::CASH_PER_CONTRACT: s.fee_kind = NativeFeeKind::CashPerUnit; break; + } + s.fee_value = c.commission_value; + if (c.qty_step > 0.0) s.quantity_grid = c.qty_step; + s.close_execution = c.pooc ? NativeCloseExecution::AfterCalculation + : NativeCloseExecution::NextEligiblePoint; + NativeMarginModel m; + m.initial_long = initial; + m.initial_short = initial; + m.maintenance_long = c.margin_long / 100.0; + m.maintenance_short = c.margin_short / 100.0; + m.check = check; + m.basis = (c.commission == CommissionType::PERCENT && c.commission_value > 0.0) + ? NativeMarginEquityBasis::MarkedEquity + : NativeMarginEquityBasis::MarkedEquityBeforeOpenCommission; + m.level_base = NativeLiquidationLevelBase::RealizedOnly; + s.margin = m; + return s; +} + +no::Request limit_buy(double units, double level, const char* label) { + auto r = tx(units, label); + r.trigger = no::Limit{level}; + return r; +} +no::Request stop_buy(double units, double level, const char* label) { + auto r = tx(units, label); + r.trigger = no::Stop{level}; + return r; +} + +// ================================================================= M7 +// A. A leveraged (20 %) long of 40 opened by a market order at bar 1's open +// (the synthetic F2 shape of test_entry_bar_margin_path.cpp, retired from the +// build at 73817c1d; at ab9714be it passes and books 12 @92 on the entry bar, +// from 73817c1d on it fails). Bar 1 is low-first (O100 L92 H110 C105); at 92 +// the book is 680 against 736. +// kernel: the AfterApplied point after the fill measures the remaining path +// at 92 and books 4 x floor(56 / 18.4) = 12 there; +// adapter: margin_check_allowed refuses that point (no opening checkpoint +// for a leveraged opening without a full bracket), so the entry bar +// is never checked and bar 2 (O105) is solvent -- no margin row. +void m7_leveraged_opening_entry_bar() { + std::printf("-- M7-A leveraged market opening: the entry bar's AfterApplied point\n"); + Config c; + c.capital = 1000.0; c.margin_long = 20.0; c.margin_short = 20.0; c.qty_step = 1.0; + c.pyramiding = 0; + const auto bars = tape({{100, 100.5, 99.5, 100}, {100, 110, 92, 105}, + {105, 106, 104, 105.5}, {105.5, 106, 105, 105.5}}); + PineSide pine(c); + pine.script = [](PineSide& h, int bar) { if (bar == 0) h.entry("L", true, kNaN, kNaN, 40.0); }; + pine.run_bars(bars); + const auto adapter = margin_fills(pine); + print_side("adapter", adapter, pine.position(), pine.equity()); + + KernelSide kernel; + kernel.config = c; + kernel.script = [](KernelSide& h, int bar) { if (bar == 0) (void)put(h, tx(40.0, "L")); }; + kernel.run_bars(pine_margin_spec("m7-a", c, 0.0), bars); + const auto native = margin_fills(kernel); + print_side("kernel", native, kernel.position(), kernel.balance()); + + KernelSide open_only; + open_only.config = c; + open_only.script = kernel.script; + open_only.gate = [](const NativeMarginCheckPoint& p) { + return p.kind == NativeMarginCheckKind::BarOpen; + }; + open_only.run_bars(pine_margin_spec("m7-a-open", c, 0.0), bars); + print_side("k-BarOpen", margin_fills(open_only), open_only.position(), open_only.balance()); + + CHECK(adapter.empty()); + CHECK(pine.position() == 40.0); + REQUIRE(native.size() == 1); + check_fill(native[0], 1, NativePathPhase::Low, 12.0, 92.0, true); + CHECK(kernel.position() == 28.0); + // The whole difference is the one point: gated to BarOpen, the kernel + // books exactly what the adapter books. + CHECK(margin_fills(open_only).empty()); + CHECK(open_only.position() == pine.position()); +} + +// B. A carried 10 @100 long (50 %, capital 1050) adds 10 @95 on a buy limit +// filled on the H->L leg of bar 2 (O100 H101 L80 C81; high-first). After the +// add the rest of the path is the close 81: 720 against 810. +// kernel: AfterApplied at the add's point books 4 x 90 / 40.5 at 81; +// adapter: the point is refused (no scheduling after a leveraged add), and +// bar 3 (O100) is solvent -- no margin row at all. +// (ab9714be books a third answer, 10 @80: the post-add book at the bar's own +// low. The kernel's AfterApplied mark after a mid-segment fill starts at the +// segment's destination waypoint, so the rest of the H->L leg is not seen.) +void m7_mid_bar_add() { + std::printf("-- M7-B mid-bar pyramid add: the add's AfterApplied point\n"); + Config c; + c.capital = 1050.0; c.pyramiding = 2; + const auto bars = tape({{100, 100.5, 99.5, 100}, {100, 100.5, 99.5, 100}, + {100, 101, 80, 81}, {100, 101, 99, 100}}); + PineSide pine(c); + pine.script = [](PineSide& h, int bar) { + if (bar == 0) h.entry("L1", true, kNaN, kNaN, 10.0); + if (bar == 1) h.entry("L2", true, 95.0, kNaN, 10.0); + }; + pine.run_bars(bars); + const auto adapter = margin_fills(pine); + print_side("adapter", adapter, pine.position(), pine.equity()); + + KernelSide kernel; + kernel.config = c; + kernel.script = [](KernelSide& h, int bar) { + if (bar == 0) (void)put(h, tx(10.0, "L1")); + if (bar == 1) (void)put(h, limit_buy(10.0, 95.0, "L2")); + }; + kernel.run_bars(pine_margin_spec("m7-b", c, 0.0), bars); + const auto native = margin_fills(kernel); + print_side("kernel", native, kernel.position(), kernel.balance()); + + CHECK(adapter.empty()); + CHECK(pine.position() == 20.0); + REQUIRE(native.size() == 1); + check_fill(native[0], 2, NativePathPhase::Low, 8.8888888888888893, 81.0, true); + CHECK(same_value(kernel.position(), 11.111111111111111)); +} + +// C. A carried POOC short 9.5 @100 (100 %, 0.1 % fee) whose bar 1 reaches +// 105, with the script closing it at bar 1's close. +// kernel: the BarOpen point rests the slice at the high; it books +// 1.7504761904761923 @105 on the path, then the script's close +// takes the remaining 7.7495238095238079 @104; +// adapter: a carried POOC short with fees and nothing resting takes no +// open/path slice (schedule_margin_call_path) -- the post-script +// checkpoint owns it, and the script's close empties the book +// first: one row, 9.5 @104, no margin call. +void m7_carried_pooc_short_fee() { + std::printf("-- M7-C carried POOC short with a fee: the BarOpen point\n"); + Config c; + c.margin_long = 100.0; c.margin_short = 100.0; c.pooc = true; c.commission_value = 0.1; + const auto bars = tape({{100, 100, 99, 100}, {100, 105, 99.5, 104}, {104, 104.5, 103.5, 104}}); + PineSide pine(c); + pine.script = [](PineSide& h, int bar) { + if (bar == 0) h.entry("S", false, kNaN, kNaN, 9.5); + if (bar == 1) h.close("S"); + }; + pine.run_bars(bars); + const auto adapter = margin_fills(pine); + print_side("adapter", adapter, pine.position(), pine.equity()); + + KernelSide kernel; + kernel.config = c; + kernel.script = [](KernelSide& h, int bar) { + if (bar == 0) (void)put(h, tx(-9.5, "S")); + if (bar == 1) (void)put(h, flat("close")); + }; + kernel.run_bars(pine_margin_spec("m7-c", c, 0.0), bars); + const auto native = margin_fills(kernel); + print_side("kernel", native, kernel.position(), kernel.balance()); + + CHECK(adapter.empty()); + REQUIRE(pine.trade_count() == 1); + CHECK(pine.get_trade(0).qty == 9.5); + CHECK(pine.get_trade(0).exit_price == 104.0); + CHECK(pine.position() == 0.0); + REQUIRE(native.size() == 1); + check_fill(native[0], 1, NativePathPhase::High, 1.7504761904761923, 105.0, true); + REQUIRE(kernel.rows().size() == 2); + CHECK(same_value(kernel.rows()[1].qty, 7.7495238095238079)); + CHECK(kernel.rows()[1].exit_price == 104.0); + CHECK(kernel.position() == 0.0); +} + +// ================================================================= M8 +// A carried 20 @100 long (50 %, capital 1000) gaps to a 90 open (bar 1: +// O90 H91 L85 C88, high-first): at the open 800 < 900. +// adapter: submit_margin_call_slice at the opening mark, a market execution +// AT THE OPEN sized on the open's money: 4 x 100/45 @90; +// kernel, PathAdverseExtremeMark (the adapter's declared check): the BarOpen +// point sizes at the remaining path's adverse mark and rests there: +// 4 x 150/42.5 @85; +// kernel, CalculationOnly (its market-execution kind): at the calculation, +// marked at the close: 4 x 120/44 @88. +// Three books, three prices: no kernel check kind executes at the open. +void m8_gap_open() { + std::printf("-- M8 gap-open breach: a market execution at the open\n"); + Config c; + c.pooc = true; + const auto bars = tape({{100, 100, 100, 100}, {90, 91, 85, 88}, {88, 89, 87, 88}}); + PineSide pine(c); + pine.script = [](PineSide& h, int bar) { if (bar == 0) h.entry("L", true, kNaN, kNaN, 20.0); }; + pine.run_bars(bars); + const auto adapter = margin_fills(pine); + print_side("adapter", adapter, pine.position(), pine.equity()); + + const auto kernel_script = [](KernelSide& h, int bar) { + if (bar == 0) (void)put(h, tx(20.0, "L")); + }; + KernelSide mark; + mark.config = c; + mark.script = kernel_script; + mark.run_bars(pine_margin_spec("m8-mark", c, 0.0), bars); + const auto at_mark = margin_fills(mark); + print_side("k-mark", at_mark, mark.position(), mark.balance()); + + KernelSide calc; + calc.config = c; + calc.script = kernel_script; + calc.run_bars(pine_margin_spec("m8-calc", c, 0.0, NativeLiquidationCheck::CalculationOnly), bars); + const auto at_calc = margin_fills(calc); + print_side("k-calc", at_calc, calc.position(), calc.balance()); + + REQUIRE(adapter.size() == 1); + check_fill(adapter[0], 1, NativePathPhase::Open, 8.8888888888888893, 90.0, false); + REQUIRE(at_mark.size() == 1); + check_fill(at_mark[0], 1, NativePathPhase::Low, 14.117647058823529, 85.0, true); + REQUIRE(at_calc.size() == 1); + check_fill(at_calc[0], 1, NativePathPhase::None, 10.909090909090908, 88.0, true); +} + +// ================================================================= M10 +// The same money three ways (grid 1, margin 50 %): a book of 18 @102 whose +// bar reaches `low`, equity E at the bar's open. +// a-stop : a default-percent (190 %) buy stop at 101 placed at a 102 close, +// gapped through at the next 102 open -> schedule_preopen_margin_ +// slice, sized by its OWN x4 (floor_quantity_grid twice, no band, +// no dust gate, no money rounding) on the frozen tuple; +// a-carry: the same 18 lots carried in from an earlier 102 open -> the +// kernel's BarOpen point, sized by resolve_margin_call_units, i.e. +// the shared source_margin_units (floor, 4x, +1e-6 floor, the +// one-contract band, the kQtyEpsilon gate); +// kernel : the stop entry on a bare kernel host -> its AfterApplied point +// after the open fill, with the twin's slice. +// ab9714be books the kernel's numbers on the stop entry in every case below +// (1 @90 band, 1 @88 fudge, 1 @90 frozen); only the pre-open slice parts. +struct M10Case { + const char* label; + double capital; + double low; + int preopen_units; // 0 = no slice + int shared_units; +}; +void m10_same_money(const M10Case& m) { + std::printf("-- M10 %s: E=%.9f, low %.2f\n", m.label, m.capital, m.low); + Config c; + c.capital = m.capital; c.qty_step = 1.0; + c.qty_type = QtyType::PERCENT_OF_EQUITY; c.qty_value = 190.0; + const auto bars = tape({{102, 102.5, 101.5, 102}, {102, 102.5, 101.5, 102}, + {102, 103, m.low, 101}, {101, 101.5, 100.5, 101}}); + PineSide stop(c); + stop.script = [](PineSide& h, int bar) { if (bar == 1) h.entry("L", true, kNaN, 101.0, kNaN); }; + stop.run_bars(bars); + const auto preopen = margin_fills(stop); + print_side("a-stop", preopen, stop.position(), stop.equity()); + + PineSide carry(c); + carry.script = [](PineSide& h, int bar) { if (bar == 0) h.entry("L", true, kNaN, kNaN, 18.0); }; + carry.run_bars(bars); + const auto shared = margin_fills(carry); + print_side("a-carry", shared, carry.position(), carry.equity()); + + KernelSide kernel; + kernel.config = c; + kernel.script = [](KernelSide& h, int bar) { + if (bar == 1) (void)put(h, stop_buy(18.0, 101.0, "L")); + }; + kernel.run_bars(pine_margin_spec("m10", c, 0.0), bars); + const auto native = margin_fills(kernel); + print_side("kernel", native, kernel.position(), kernel.balance()); + + // Same book on all three sides. + CHECK(stop.position() + (preopen.empty() ? 0.0 : preopen[0].units) == 18.0); + CHECK(carry.position() + (shared.empty() ? 0.0 : shared[0].units) == 18.0); + if (m.preopen_units == 0) { + CHECK(preopen.empty()); + } else { + REQUIRE(preopen.size() == 1); + check_fill(preopen[0], 2, NativePathPhase::Low, m.preopen_units, m.low, false); + CHECK(preopen[0].label == "__margin_preopen__L"); + } + REQUIRE(shared.size() == 1); + check_fill(shared[0], 2, NativePathPhase::Low, m.shared_units, m.low, true); + REQUIRE(native.size() == 1); + check_fill(native[0], 2, NativePathPhase::Low, m.shared_units, m.low, true); +} + +// The frozen tuple: signal close 100, gap-through open 102. The pre-open slice +// sizes on the frozen 19 lots (sized at the 100 signal) while the book the +// kernel fills holds 18 (sized at the 102 fill): 4 lots against the kernel's +// one-contract band on the real book. +void m10_frozen_units() { + std::printf("-- M10 frozen: signal 100, fill 102, low 90\n"); + Config c; + c.qty_step = 1.0; c.qty_type = QtyType::PERCENT_OF_EQUITY; c.qty_value = 190.0; + const auto bars = tape({{100, 100.5, 99.5, 100}, {102, 103, 90, 101}, {101, 101.5, 100.5, 101}}); + PineSide stop(c); + stop.script = [](PineSide& h, int bar) { if (bar == 0) h.entry("L", true, kNaN, 101.0, kNaN); }; + stop.run_bars(bars); + const auto preopen = margin_fills(stop); + print_side("a-stop", preopen, stop.position(), stop.equity()); + KernelSide kernel; + kernel.config = c; + kernel.script = [](KernelSide& h, int bar) { + if (bar == 0) (void)put(h, stop_buy(18.0, 101.0, "L")); + }; + kernel.run_bars(pine_margin_spec("m10-frozen", c, 0.0), bars); + const auto native = margin_fills(kernel); + print_side("kernel", native, kernel.position(), kernel.balance()); + REQUIRE(preopen.size() == 1); + CHECK(stop.position() + preopen[0].units == 18.0); // the book is 18 lots + check_fill(preopen[0], 1, NativePathPhase::Low, 4.0, 90.0, false); + REQUIRE(native.size() == 1); + check_fill(native[0], 1, NativePathPhase::Low, 1.0, 90.0, true); +} + +// ================================================================= M11 +// A. A 1x long 10 @100 on 1000 with a 0.1 % entry fee. +// kernel: resulting notional 1000 against marked equity 1000 - the 1.0 +// ticket = 999 -> MatchRejectReason::InitialMargin, flat +// (initial_margin_fraction 1.0 and a model with initial 1.0 alike); +// adapter: validate_precommit admits it against the pre-entry realized +// budget (AdmitWithHostMargin) and the opening checkpoint trims +// 4 x 1/100 = 0.04 @100 (TradingView's observable residual). +void m11_fee_opening() { + std::printf("-- M11-A a 1x long whose entry fee the kernel's gate refuses\n"); + Config c; + c.margin_long = 100.0; c.margin_short = 100.0; c.commission_value = 0.1; + const auto bars = tape({{100, 100, 100, 100}, {100, 100.5, 99.5, 100}, {100, 100.5, 99.5, 100}}); + PineSide pine(c); + pine.script = [](PineSide& h, int bar) { if (bar == 0) h.entry("L", true, kNaN, kNaN, 10.0); }; + pine.run_bars(bars); + const auto adapter = margin_fills(pine); + print_side("adapter", adapter, pine.position(), pine.equity()); + CHECK(rejections(pine).empty()); + REQUIRE(adapter.size() == 1); + check_fill(adapter[0], 1, NativePathPhase::Open, 0.039999999999999147, 100.0, false); + CHECK(same_value(pine.position(), 9.9600000000000009)); + + for (const bool scalar : {true, false}) { + KernelSide kernel; + kernel.config = c; + kernel.script = [](KernelSide& h, int bar) { if (bar == 0) (void)put(h, tx(10.0, "L")); }; + auto spec = pine_margin_spec(scalar ? "m11-a-scalar" : "m11-a-model", c, 1.0); + if (scalar) { + spec.margin.reset(); + spec.initial_margin_fraction = 1.0; + } + kernel.run_bars(spec, bars); + const auto refused = rejections(kernel); + std::printf(" %-9s position %.17g, %zu rejection(s)%s\n", + scalar ? "k-scalar" : "k-model", kernel.position(), refused.size(), + !refused.empty() && refused[0].reason == no::MatchRejectReason::InitialMargin + ? " InitialMargin" : ""); + CHECK(kernel.position() == 0.0); + REQUIRE(refused.size() == 1); + CHECK(refused[0].reason == no::MatchRejectReason::InitialMargin); + } +} + +// B. The reverse. A carried 10 @100 long (50 %, capital 1050) adds 10 at the +// next open; bar 2 gaps to `open`. +// adapter: required 20 x open x 0.5 against the SIGNAL-TIME equity 1050 +// -> refuses at 110 (1100), admits at 104 (1040); +// kernel (initial_margin_fraction 0.5): against the equity marked at the +// fill, 1050 + 10 x (open - 100) -> admits at 110 (1100 <= 1150). +void m11_add(double open, bool adapter_admits) { + std::printf("-- M11-B a same-side add at a %.0f gap-up open\n", open); + Config c; + c.capital = 1050.0; c.pyramiding = 2; + const auto bars = tape({{100, 100.5, 99.5, 100}, {100, 100.5, 99.5, 100}, + {open, open + 0.5, open - 0.5, open}, {open, open + 0.5, open - 0.5, open}}); + PineSide pine(c); + pine.script = [](PineSide& h, int bar) { + if (bar == 0) h.entry("L1", true, kNaN, kNaN, 10.0); + if (bar == 1) h.entry("L2", true, kNaN, kNaN, 10.0); + }; + pine.run_bars(bars); + const auto refused = rejections(pine); + std::printf(" adapter position %.17g, %zu rejection(s)%s\n", pine.position(), refused.size(), + !refused.empty() && refused[0].reason == no::MatchRejectReason::HostPrecommit + ? " HostPrecommit" : ""); + KernelSide kernel; + kernel.config = c; + kernel.script = [](KernelSide& h, int bar) { + if (bar == 0) (void)put(h, tx(10.0, "L1")); + if (bar == 1) (void)put(h, tx(10.0, "L2")); + }; + auto spec = pine_margin_spec("m11-b", c, 0.0); + spec.margin.reset(); + spec.initial_margin_fraction = 0.5; + kernel.run_bars(spec, bars); + std::printf(" kernel position %.17g, %zu rejection(s)\n", kernel.position(), + rejections(kernel).size()); + CHECK(kernel.position() == 20.0); + CHECK(rejections(kernel).empty()); + if (adapter_admits) { + CHECK(pine.position() == 20.0); + CHECK(refused.empty()); + } else { + CHECK(pine.position() == 10.0); + REQUIRE(refused.size() == 1); + CHECK(refused[0].reason == no::MatchRejectReason::HostPrecommit); + CHECK(refused[0].request().label == "L2"); + } +} + +// ================================================================= E20 f1 +// The probe-suppressed tail (set_probe_suppress_tail_logic): the forming last +// bar is matched and settled but not calculated. Ruled 2026-09-22 +// (docs/pages/live-surface.md §3.2): a margin call due against it is booked +// on it. Two routes reach that bar, and the pin holds both: +// path (E20's own two-bar tape): the kernel's resting liquidation, admitted +// at the bar's open by on_bar_open's scheduling, matched on the tail's +// path -- booked with or without the post-script checkpoint; +// close (a carried POOC short with a fee): the post-script checkpoint +// adapter_.on_bar_close takes after the (suppressed) script -- the +// route the ruling is about. Under the pre-ruling promise ("surfaces +// only at settlement", what ab9714be did) the tail books nothing on +// either route: the book the forming bar found, the one-bar run. +// The third run shows why it matters: with a script that would close the +// short on that bar, the settled bar books the close and no margin call, the +// forming bar (script suppressed) books the broker's margin call. +struct TailRun { + std::vector fills; + double position = 0.0; + int calls = 0; + int rows = 0; +}; +TailRun tail_run(const Config& c, const std::vector& bars, double qty, bool probe, + bool close_on_bar_1) { + PineSide pine(c); + pine.script = [qty, close_on_bar_1](PineSide& h, int bar) { + if (bar == 0) h.entry("S", false, kNaN, kNaN, qty); + if (close_on_bar_1 && bar == 1) h.close("S"); + }; + if (probe) pine.set_probe_suppress_tail_logic(true); + pine.run_bars(bars); + return {margin_fills(pine), pine.position(), pine.calls, pine.trade_count()}; +} +void print_tail(const char* label, const TailRun& r) { + std::printf(" %-18s calls=%d rows=%d position %.17g", label, r.calls, r.rows, r.position); + for (const auto& f : r.fills) { + std::printf(" | %s %.17g @ %.17g bar %d %s", f.kernel ? "kernel" : "adapter", f.units, + f.price, f.bar, phase_name(f.phase)); + } + std::printf("\n"); +} +void e20_probe_tail_margin_call() { + std::printf("-- E20 f1 margin call on the probe-suppressed tail\n"); + const auto two = tape({{100, 100, 99, 100}, {100, 105, 99.5, 104}}); + const std::vector one(two.begin(), two.begin() + 1); + + Config path; + path.margin_long = 100.0; path.margin_short = 100.0; path.pooc = true; + path.qty_type = QtyType::PERCENT_OF_EQUITY; path.qty_value = 100.0; + const auto path_plain = tail_run(path, two, kNaN, false, false); + const auto path_probe = tail_run(path, two, kNaN, true, false); + print_tail("path plain", path_plain); + print_tail("path probe", path_probe); + CHECK(path_plain.calls == 2); + CHECK(path_probe.calls == 1); + for (const auto* run : {&path_plain, &path_probe}) { + REQUIRE(run->fills.size() == 1); + check_fill(run->fills[0], 1, NativePathPhase::High, 3.8095238095238093, 105.0, true); + CHECK(run->fills[0].time == two[1].timestamp); + } + + Config close = path; + close.qty_type = QtyType::FIXED; close.qty_value = 1.0; close.commission_value = 0.1; + const auto close_plain = tail_run(close, two, 9.5, false, false); + const auto close_probe = tail_run(close, two, 9.5, true, false); + const auto before_tail = tail_run(close, one, 9.5, false, false); + print_tail("close plain", close_plain); + print_tail("close probe", close_probe); + print_tail("close pre-ruling", before_tail); + CHECK(close_probe.calls == 1); + for (const auto* run : {&close_plain, &close_probe}) { + REQUIRE(run->fills.size() == 1); + // phase None: the post-script checkpoint, after the calculation point. + check_fill(run->fills[0], 1, NativePathPhase::None, 1.7504761904761923, 105.0, false); + CHECK(run->fills[0].time == two[1].timestamp); + CHECK(same_value(run->position, -7.7495238095238079)); + } + CHECK(before_tail.fills.empty()); + CHECK(before_tail.position == -9.5); + // The path route's pre-ruling book is the one-bar book too (ab9714be's + // probe tail books exactly this for both routes). + const auto path_before = tail_run(path, one, kNaN, false, false); + print_tail("path pre-ruling", path_before); + CHECK(path_before.fills.empty()); + CHECK(path_before.position == -10.0); + + const auto closing_plain = tail_run(close, two, 9.5, false, true); + const auto closing_probe = tail_run(close, two, 9.5, true, true); + print_tail("closing plain", closing_plain); + print_tail("closing probe", closing_probe); + CHECK(closing_plain.fills.empty()); + CHECK(closing_plain.position == 0.0); + REQUIRE(closing_probe.fills.size() == 1); + check_fill(closing_probe.fills[0], 1, NativePathPhase::None, 1.7504761904761923, 105.0, false); + CHECK(same_value(closing_probe.position, -7.7495238095238079)); +} + +// ================================================================= M7 on tapes +// M7-A's class measured against TradingView: four `lab tv` tapes +// (tests/fixtures/margin_entry_bar, BINANCE:ETHUSDT.P 15m, ws-report-v1, +// rangeProof covered), each one leveraged long market entry placed at a +// signal close and filled at the next bar's open, whose low breaches the +// requirement (margin_long 5 at 16x the equity, and 20 at 4x). The script +// closes what is left three bars after the entry bar ("timeout"). +// TradingView books the margin call ON THE ENTRY BAR, at that bar's low, 4 +// of 4 (the whole book on the three 16x tapes, 0.4544 of 1.074 on the 4x); +// the kernel's AfterApplied point, with TradingView's money and slice (the +// twin's hooks above), books the same rows; +// the adapter never checks the entry bar of a leveraged opening +// (margin_check_allowed refuses the opening's AfterApplied point): it books +// its first margin call at the NEXT bar's open, or none at all when that bar +// has recovered (the 4x tape). These are RECORDED divergences: every adapter +// row is pinned at exactly its bar, price and quantity, and asserted to +// differ from the tape, so the rows fail the day the adapter checks the +// entry bar and must be re-pinned deliberately. +struct TapeFill { + int bar; + double price; + double qty; + std::string signal; +}; +struct M7Tape { + const char* slug; + const FeedBar* bars; + double margin; + double leverage; // quantity = leverage x equity / signal close + std::vector adapter; // the adapter's pinned rows (exit bar, price, qty) +}; + +// "YYYY-MM-DD HH:MM" in the tape's UTC+8 -> UTC milliseconds. +std::int64_t tape_ms(const std::string& text) { + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + if (std::sscanf(text.c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) return -1; + y -= mo <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const int yoe = y - era * 400; + const int doy = (153 * (mo + (mo > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + const std::int64_t days = static_cast(era) * 146097 + doe - 719468; + return ((days * 24 + h - 8) * 60 + mi) * 60000; +} + +// The tape's exits, each as {exit bar, price, qty, signal}; bars counted +// from the signal bar. +std::vector tape_exits(const char* slug, const FeedBar* bars) { + std::ifstream in(std::string(PINEFORGE_HM_M7A_FIXTURE_DIR) + "/" + slug + "/tv_trades.csv"); + std::vector out; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 6 || cell[1].rfind("Exit", 0) != 0) continue; + const std::int64_t ms = tape_ms(cell[2]); + int bar = -1; + for (int i = 0; i < 6; ++i) + if (bars[i].ts == ms) bar = i; + out.push_back({bar, std::stod(cell[4]), std::stod(cell[5]), cell[3]}); + } + // The tape lists the newest trade first. + std::sort(out.begin(), out.end(), [](const TapeFill& a, const TapeFill& b) { + return a.bar != b.bar ? a.bar < b.bar : a.qty < b.qty; + }); + return out; +} + +std::vector engine_exits(const NativeStrategyHost& host) { + std::vector out; + for (int i = 0; i < host.trade_count(); ++i) { + const Trade& t = host.get_trade(i); + out.push_back({t.exit_bar_index, t.exit_price, t.qty, t.exit_comment}); + } + std::sort(out.begin(), out.end(), [](const TapeFill& a, const TapeFill& b) { + return a.bar != b.bar ? a.bar < b.bar : a.qty < b.qty; + }); + return out; +} + +bool same_rows(const std::vector& a, const std::vector& b) { + if (a.size() != b.size()) return false; + for (std::size_t i = 0; i < a.size(); ++i) { + // TradingView prints four quantity decimals and two price decimals. + if (a[i].bar != b[i].bar || std::abs(a[i].price - b[i].price) > 5e-3 + || std::abs(a[i].qty - b[i].qty) > 5e-5) { + return false; + } + } + return true; +} + +void print_rows(const char* side, const std::vector& rows) { + std::printf(" %-8s", side); + for (const auto& r : rows) + std::printf(" | bar %d %.17g @ %.2f '%s'", r.bar, r.qty, r.price, r.signal.c_str()); + std::printf("\n"); +} + +void m7_tradingview_tapes() { + const M7Tape tapes[] = { + {"hm-m7a-eth-m5-k16-0621", kM7a_m5_k16_0621, 5.0, 16.0, + {{2, 2297.51, 2.2747999999999999, "Margin call"}, {5, 2301.62, 4.5671999999999997, ""}}}, + {"hm-m7a-eth-m5-k16-0922", kM7a_m5_k16_0922, 5.0, 16.0, + {{2, 4152.54, 0.66800000000000004, "Margin call"}, {5, 4190.01, 3.125, ""}}}, + {"hm-m7a-eth-m5-k16-1121", kM7a_m5_k16_1121, 5.0, 16.0, + {{2, 2701.42, 2.9012000000000002, "Margin call"}, {5, 2734.82, 2.9047999999999998, ""}}}, + {"hm-m7a-eth-m20-k4-1010", kM7a_m20_k4_1010, 20.0, 4.0, + {{5, 3895.0, 1.0740000000000001, ""}}}, + }; + for (const M7Tape& tape : tapes) { + std::printf("-- M7 tape %s (margin %.0f%%, %.0fx)\n", tape.slug, tape.margin, tape.leverage); + std::vector bars; + for (int i = 0; i < 6; ++i) { + const FeedBar& r = tape.bars[i]; + bars.push_back({r.open, r.high, r.low, r.close, 1.0, r.ts}); + } + const double qty = std::floor(tape.leverage * 1000.0 / bars[0].close * 1000.0) / 1000.0; + Config c; + c.capital = 1000.0; c.margin_long = tape.margin; c.margin_short = tape.margin; + // TradingView floors the margin call's minimum to 0.0001 of a contract + // on this symbol before the x4 (the 4x tape: 4 x 0.1136 = 0.4544, where + // the unfloored rule gives 0.45456...). + c.qty_step = 0.0001; + PineSide pine(c); + pine.script = [qty](PineSide& h, int bar) { + if (bar == 0) h.entry("L", true, kNaN, kNaN, qty); + if (h.position() > 0.0 && bar - h.entry_bar_index() >= 3) h.close("L"); + }; + pine.run_bars(bars); + const auto adapter = engine_exits(pine); + + KernelSide kernel; + kernel.config = c; + kernel.script = [qty](KernelSide& h, int bar) { + if (bar == 0) (void)put(h, tx(qty, "L")); + if (bar == 4 && h.position() > 0.0) (void)put(h, flat("timeout")); + }; + auto spec = pine_margin_spec(tape.slug, c, 0.0); + spec.input_tf = "15"; + spec.script_tf = "15"; + kernel.run_bars(spec, bars); + const auto native = engine_exits(kernel); + + const auto tv = tape_exits(tape.slug, tape.bars); + print_rows("tape", tv); + print_rows("adapter", adapter); + print_rows("kernel", native); + + // TradingView: a margin call on the entry bar (bar 1) at its low. + REQUIRE(!tv.empty()); + CHECK(tv[0].bar == 1); + CHECK(tv[0].signal == "Margin call"); + CHECK(std::abs(tv[0].price - bars[1].low) <= 5e-3); + // The kernel's own AfterApplied point books TradingView's rows. + CHECK(same_rows(native, tv)); + // The adapter: pinned, and a recorded divergence from the tape. + CHECK(!same_rows(adapter, tv)); + REQUIRE(adapter.size() == tape.adapter.size()); + for (std::size_t i = 0; i < adapter.size(); ++i) { + CHECK(adapter[i].bar == tape.adapter[i].bar); + CHECK(same_value(adapter[i].price, tape.adapter[i].price)); + CHECK(std::abs(adapter[i].qty - tape.adapter[i].qty) <= 1e-9); + CHECK(adapter[i].signal == tape.adapter[i].signal); + CHECK(adapter[i].bar != 1); // never on the entry bar + } + } +} + +} // namespace + +int main() { + test("M7-A leveraged opening", m7_leveraged_opening_entry_bar); + test("M7-B mid-bar add", m7_mid_bar_add); + test("M7-C carried POOC short fee", m7_carried_pooc_short_fee); + test("M8 gap open", m8_gap_open); + test("M10 plain", [] { m10_same_money({"plain", 1000.0, 85.0, 4, 4}); }); + test("M10 band", [] { m10_same_money({"band", 1000.0, 90.0, 0, 1}); }); + test("M10 fudge", [] { m10_same_money({"fudge", 1000.000022, 88.0, 3, 1}); }); + test("M10 frozen", m10_frozen_units); + test("M11-A fee opening", m11_fee_opening); + test("M11-B add 110", [] { m11_add(110.0, false); }); + test("M11-B add 104", [] { m11_add(104.0, true); }); + test("E20 f1 probe tail", e20_probe_tail_margin_call); + test("M7 TradingView tapes", m7_tradingview_tapes); + std::printf("%d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} From e1d0fa3a5b0b13e7edd6b056f9797ae3a21b869c Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:16:01 +0800 Subject: [PATCH 02/83] H-MEASURE: tape the pyramiding count, the sibling stop and pending-entry trails AUDIT4 X14 rows G2-15 (M13), the M16 sibling stop (G2-18), E14's anchored-leg seed (AUDIT3 H6 tapes) and E5's pending-entry arm, each measured against the kernel on the same bars and against TradingView's own trades (lab tv, NYSE:F 15m, ws-report-v1, rangeProof covered; each fixture README names its tapes and sha256): - tests/test_pyramiding_count_differential.cpp (M13): fifteen pyramiding=2 scenarios through the adapter, a bare host with max_open_lots=2 and the tapes. TradingView checks an entry once, at its first eligible point, against the trades then open; max_open_lots books 14 of 15 as TradingView does, the adapter's per-cycle count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). A default strategy.exit(from_entry) under FIFO closes the book in the adapter and one lot in TradingView (P10). All pinned as recorded divergences; the retention reason in ADR-0001 is refuted. - tests/test_zero_trail_sibling_stop.cpp (M16): the bare host with both legs is the adapter bit for bit; the Trail alone rides past the open. TradingView books the touch at the open on 16 of 16 taped trades. - tests/test_pending_entry_trail_tapes.cpp (E5, E14): a trailing exit written while its entry is pending. The quantized arm holds, 28 of 28 (E5's claim, taped); TradingView's running best starts at the activation 13 of 13 while the adopted anchored child names no best_seed and exits 1 to 24 bars later -- recorded divergences, the adoption compare leaving best_seed out. Docs: ADR-0001's max_open_lots row, design MG3 and the rulings summary, the sibling-stop and anchored-leg paragraphs (the latter no longer "an open measurement"), native-engine.md and pine-to-native.md on adoption and the sibling stop, contributing-llm.md's pyramiding row; two design anchors into native-engine.md re-anchored. No src/ or include/ change. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 99d3b9506af49a3c9bdfc5bfd947c12fd9988839) INT26 pick note: the ADR, design and pine-to-native pages conflicted on citation digits only (int16_digit_merge: 5 blocks, no text refused). The two design citations of native-engine.md both sides moved are carried through the exact line map from the pre-pick tree (2963-3031 -> 2970-3038, 1642-1643 -> 1647-1648). hunkcheck: every hunk of 99d3b950 carried; the three pages differ from its diff in anchor digits only. --- docs/adr/0001-kernel-adapter-boundary.md | 2 +- docs/design/native-feature-parity.md | 47 +- docs/pages/contributing-llm.md | 2 +- docs/pages/native-engine.md | 11 +- docs/pages/pine-to-native.md | 7 +- tests/CMakeLists.txt | 9 + tests/fixtures/pending_entry_trail/README.md | 37 + tests/fixtures/pending_entry_trail/bars.inc | 1291 +++++++++++++++++ .../meta.json | 16 + .../metrics.json | 40 + .../strategy.pine | 68 + .../tv_trades.csv | 17 + .../meta.json | 16 + .../metrics.json | 40 + .../strategy.pine | 53 + .../tv_trades.csv | 11 + .../hm-orders-h4-f-long-pending-arm/meta.json | 16 + .../metrics.json | 40 + .../strategy.pine | 68 + .../tv_trades.csv | 17 + .../meta.json | 16 + .../metrics.json | 40 + .../strategy.pine | 55 + .../tv_trades.csv | 13 + .../meta.json | 16 + .../metrics.json | 40 + .../strategy.pine | 68 + .../tv_trades.csv | 17 + .../meta.json | 16 + .../metrics.json | 40 + .../strategy.pine | 55 + .../tv_trades.csv | 13 + tests/fixtures/pyramiding_count/README.md | 53 + tests/fixtures/pyramiding_count/bars.inc | 215 +++ .../pyramiding_count/expectations.inc | 80 + .../meta.json | 16 + .../metrics.json | 40 + .../strategy.pine | 32 + .../tv_trades.csv | 9 + .../hm-orders-m13-f-pyramiding-rule/meta.json | 16 + .../metrics.json | 40 + .../strategy.pine | 57 + .../tv_trades.csv | 19 + .../hm-orders-m13-f-pyramiding/meta.json | 16 + .../hm-orders-m13-f-pyramiding/metrics.json | 40 + .../hm-orders-m13-f-pyramiding/strategy.pine | 120 ++ .../hm-orders-m13-f-pyramiding/tv_trades.csv | 49 + .../zero_trail_sibling_stop/README.md | 25 + .../fixtures/zero_trail_sibling_stop/bars.inc | 491 +++++++ .../meta.json | 16 + .../metrics.json | 40 + .../strategy.pine | 54 + .../tv_trades.csv | 17 + .../meta.json | 16 + .../metrics.json | 40 + .../strategy.pine | 54 + .../tv_trades.csv | 17 + tests/test_pending_entry_trail_tapes.cpp | 412 ++++++ tests/test_pyramiding_count_differential.cpp | 377 +++++ tests/test_zero_trail_sibling_stop.cpp | 401 +++++ 60 files changed, 4937 insertions(+), 22 deletions(-) create mode 100644 tests/fixtures/pending_entry_trail/README.md create mode 100644 tests/fixtures/pending_entry_trail/bars.inc create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/meta.json create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/metrics.json create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/strategy.pine create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/tv_trades.csv create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/meta.json create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/metrics.json create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/strategy.pine create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/tv_trades.csv create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/meta.json create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/metrics.json create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/strategy.pine create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/tv_trades.csv create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/meta.json create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/metrics.json create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/strategy.pine create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/tv_trades.csv create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/meta.json create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/metrics.json create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/strategy.pine create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/tv_trades.csv create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/meta.json create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/metrics.json create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/strategy.pine create mode 100644 tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/tv_trades.csv create mode 100644 tests/fixtures/pyramiding_count/README.md create mode 100644 tests/fixtures/pyramiding_count/bars.inc create mode 100644 tests/fixtures/pyramiding_count/expectations.inc create mode 100644 tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/meta.json create mode 100644 tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/metrics.json create mode 100644 tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/strategy.pine create mode 100644 tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/tv_trades.csv create mode 100644 tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/meta.json create mode 100644 tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/metrics.json create mode 100644 tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/strategy.pine create mode 100644 tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/tv_trades.csv create mode 100644 tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/meta.json create mode 100644 tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/metrics.json create mode 100644 tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/strategy.pine create mode 100644 tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/tv_trades.csv create mode 100644 tests/fixtures/zero_trail_sibling_stop/README.md create mode 100644 tests/fixtures/zero_trail_sibling_stop/bars.inc create mode 100644 tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/meta.json create mode 100644 tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/metrics.json create mode 100644 tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/strategy.pine create mode 100644 tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/tv_trades.csv create mode 100644 tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/meta.json create mode 100644 tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/metrics.json create mode 100644 tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/strategy.pine create mode 100644 tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/tv_trades.csv create mode 100644 tests/test_pending_entry_trail_tapes.cpp create mode 100644 tests/test_pyramiding_count_differential.cpp create mode 100644 tests/test_zero_trail_sibling_stop.cpp diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 4fee08d4..ae0cc105 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -577,7 +577,7 @@ Every line number in this section names its symbol on this tree, which | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:342`), `source_level_on_price_grid` (`:328`), `nearest_tick` (`:266`) / `source_bar_fill_tick` (`:287`) / `directional_tick` (`:311`), 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`, `tests/test_native_price_grid.cpp` | | `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13867`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14922`), `chart_day_key` (`pine_adapter.cpp:13764`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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). `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:13226-13227`): 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:2360-2362`) | design row MG3 (R5-1); the contract comment in `project()` | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_margin_model.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:2360-2362`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end`): report shape, not a mark-to-market row (`sha256:71a6a8622f560fca3a27d37e098ea8b6f68f831abcca0b6c46ad004d3ffe9358` pine_adapter.cpp:2334-2341) | 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:2325-2327`) | 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` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 49b4d96e..70c1d917 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -96,7 +96,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2361) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2947-2955 | 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:594 tests the resulting book native_execution_consumer.cpp:2961-2964 | 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:13226-13229) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6180-6226, cap left unset `max_open_lots` pine_adapter.cpp:2360-2362) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6180-6226, cap left unset `max_open_lots` pine_adapter.cpp:2360-2362) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | | MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13280-13345 and leaves the kernel gate unset pine_adapter.cpp:13279-13281) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13935-13936, `margin_pct` pine_adapter.cpp:13343) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14035-14213, `submit_margin_call_units` pine_adapter.cpp:14174-14220, `schedule_margin_call_path` pine_adapter.cpp:18185-18262) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2943 **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 | @@ -293,7 +293,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2963-3031) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2925-2992) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `sha256:60a689e9bbb16d77370ee18557929c420ee846d162b7d2bcd5472ffd74409e69` runner/CMakeLists.txt:37-45, `sha256:e7b0011fb5e730a159692b9f8ab97b2d43bd386e7c4802f13a4ae4c1db083c80` check_native_include_independence.py:36-39, the tests at `sha256:2c88205b43a02dafe8915e74417743b3b3faa0c65595686ccd305386505bdf55` runner/CMakeLists.txt:98-128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -655,18 +655,33 @@ there); the sub-tick sweep is the gate. What is NOT redundant afterwards is the sibling stop an explicit-zero trail rests beside its `Trail`: a `Stop` is reached by a touch and a zero-distance ride needs a move strictly past the best, and a print landing ON the carried best separates them in the booked -price's last bits. - -**Anchored relative legs: an open measurement (lane F7).** A trailing -`strategy.exit` issued while its entry is still pending is materialized as the -kernel's anchored child at the fill (`relative_leg_shapes`, M17) and names no -`best_seed`: its ride starts where the kernel arms it. None of the 23 E5 / E9 / -E14 tapes measures that shape — every probe issues its exit once the entry has -filled (`strategy.opentrades.entry_bar_index(0) == bar_index`) — so whether -TradingView's best starts at the activation there too is unknown, and F7 -records it rather than changing it. The tape it needs is the E14 shape with the -exit written on the entry's signal bar: a fill bar that reaches the activation -on its tick, a shallow next bar. +price's last bits. **Measured (R5 lane H-MEASURE, +`tests/test_zero_trail_sibling_stop.cpp`):** a bare kernel host given both +legs is the adapter bit for bit; given the `Trail` alone it rides past the +open and fills on a later leg of the bar, half a tick to a tick and a half +away. On two `lab tv` tapes (NYSE:F 15m, sixteen trades of that shape) +TradingView books the touch at the open 16 of 16: the adapter's bar and price +16 of 16, the `Trail` alone 0 of 16. + +**Anchored relative legs: measured (lane F7 recorded it, R5 lane H-MEASURE +taped it).** A trailing `strategy.exit` issued while its entry is still pending +is materialized as the kernel's anchored child at the fill +(`relative_leg_shapes`, M17) and names no `best_seed`: its ride starts where the +kernel arms it. Six `lab tv` tapes of that shape (NYSE:F 15m, the exit written +on the entry's signal bar; `tests/fixtures/pending_entry_trail`, replayed by +`tests/test_pending_entry_trail_tapes.cpp`) settle both halves. The arm: on 28 +trades whose arming bar's raw extreme stops inside the activation's tick cell +(16 market entries, 12 limit entries filling mid-bar), TradingView exits on +that bar at the activation ∓ 1 tick 28 of 28, and so does the adapter's +anchored child (E5's quantized arm holds for a pending entry). The running +best: on 13 trades whose prints after the arm fall between the two stops a best +can give, TradingView's best starts AT the activation 13 of 13, as E14 measured +for a filled entry; the anchored child's starts at the raw arm print, and the +adapter exits 1 to 24 bars later on all 13. The fill point's adoption compare +leaves `best_seed` out, so it adopts the unseeded child where `exit()` would +have submitted a seeded leg. These are recorded divergences (each row pinned at +the engine's bar and price); refusing to adopt a seeded fill-point leg closes +all 13 in a scratch build. **The trail stop's own ULP (lane E16, closing E14's finding).** E14's tape left one trade of `e14-f-long-shallow-next` pinned as a recorded divergence: @@ -760,7 +775,7 @@ still in the table. | `price_grid`, `grid_rounding` | native-only | §3.6.2 | | `risk` | native-only | §3.6.1 | | `max_abs_units` | adapter-policy: the pre-fill live-book gate is TradingView's | row MG2; N12's `PS` measurement (book 4 against 2) | -| `max_open_lots` | adapter-policy: Pine pyramiding is a per-cycle entry count | row MG3; the contract comment in `project()` | +| `max_open_lots` | adapter-policy: Pine pyramiding is a per-cycle entry count (measured against TradingView: not its rule, row MG3) | row MG3; the contract comment in `project()`; `tests/test_pyramiding_count_differential.cpp` | | `initial_margin_fraction` | adapter-policy: TradingView's money admission answers `AdmitWithHostMargin`; the declared `margin` model is maintenance-only | row MG4; the wave-4 ruling in `project()` | | `report_open_position_at_end` | adapter-policy: TradingView's range-end report is report shape, not a mark-to-market row | row RP5 | | `open_bar_view` | adapter-policy: TradingView's open scheduling and its fill callback read the whole bar | rows CT4, E5 | @@ -1352,7 +1367,7 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1642-1643) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6982-6994) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1630-1631) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6982-6994) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). 2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`sha256:d2bc11d04fb37fc9c5d8b87bef2f5438be7a5f5c8ff72da5fcf255500d92f5cb` native_execution_consumer.cpp:3491-3500). `born_on_remaining_path` (native_execution_consumer.cpp:5616-5620) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1659-1666) is calibrated against it. 3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is native_execution_consumer.cpp:6835-6842). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2557); valid in bar-open, applied and tick callbacks. diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 3ab8405e..645ea88f 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -222,7 +222,7 @@ decision, and removing one is a regression: | the kernel's price grid (`NativeRunSpec::price_grid`) and the adapter's own tick rules | TradingView quantizes per *order kind* — stop and limit legs on the quantized bar, the trail stop and the `calc_on_order_fills` cursors raw — and the kernel grid is one rule for the run. A per-kind mask would spell that inconsistency into the kernel. | ADR 0001 ruling table, row `price_grid`; design `native-feature-parity.md:527` | | the kernel's risk limits (`NativeRunSpec::risk`) and the adapter's `strategy.risk.*` | structurally, Pine's risk calls are per-bar statements that arrive after the spec has been digested; substantively, four measured divergences in the latch, the streak, the close price and the day key. | ADR 0001 ruling table, row `risk`; design `native-feature-parity.md:438` | | the kernel's `max_abs_units` and the adapter's `max_position_size` | the kernel caps the *resulting* book, TradingView gates the *live* book before the fill. | ADR 0001 ruling table, row `max_abs_units` | -| the kernel's `max_open_lots` and Pine's `pyramiding` | Pine counts *entries per cycle*, the kernel counts *physical lots*; a resting source entry must not consume a lot slot before it fills. | ADR 0001 ruling table, row `max_open_lots` | +| the kernel's `max_open_lots` and Pine's `pyramiding` | the adapter counts *entries per cycle*, the kernel counts *physical lots*. Measured against TradingView the kernel's count is the closer one (14 of 15 tape scenarios against the adapter's 8, `tests/test_pyramiding_count_differential.cpp`): TradingView checks an entry once, at its first eligible point, against the trades then open. The adapter keeps its count pending a re-lowering ruling. | ADR 0001 ruling table, row `max_open_lots` | | the kernel's margin model and the adapter's money admission | the adapter answers TradingView's ten-significant-digit admission itself and declares a *maintenance-only* model, because a positive initial requirement would decline openings TradingView takes and admit adds it refuses (measured both ways, `tests/test_adapter_margin_schedule_differential.cpp`). | ADR 0001 ruling table, row `initial_margin_fraction` | | `NativeRunSpec::report_open_position_at_end` and the adapter's range-end rows | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it: report *shape*, not a mark-to-market row. | ADR 0001 ruling table, row `report_open_position_at_end`; design `native-feature-parity.md:615` | | `subscriptions` in the spec and the adapter's begin-time declaration | the adapter declares the same kernel subscriptions through a hook instead of the field, so a plain `request.security` site really is a kernel subscription. | ADR 0001 ruling table, row `subscriptions` | diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 75a5dc25..8ed0e8b2 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -977,7 +977,12 @@ functions `exit()` uses. The parent's fill point still runs the source same-bar ordering are facts of that point and have no kernel analogue; it *adopts* the armed child instead of submitting whenever that child is the request it was about to submit: the same trigger bits, the same OCA group, a -host-sized close of the book. Because the child is host-sized and +host-sized close of the book. The comparison leaves out a trail's +`best_seed`, so a pending entry's trailing exit adopts a child whose best +starts at the raw arm print where the request `exit()` builds would start it +at the activation; TradingView starts it at the activation, and the adapter +exits later on 13 of 13 taped trades (`tests/test_pending_entry_trail_tapes.cpp`, +recorded divergences). The quantized arm itself holds there, 28 of 28. Because the child is host-sized and book-scoped, that holds for every quantity the source resolves at the match (a percentage, a sibling's remainder) and for every book the parent's fill leaves (flat, a reversal, a later same-id add), under `calc_on_order_fills` @@ -1810,7 +1815,9 @@ already armed it. The kernel's own crossing is then the price the adapter books, which is what the three trail twin suites now assert. The sibling stop an explicit-zero trail rests beside its `Trail` stays: a `Stop` is reached by a touch and a zero-distance ride needs a move strictly past the best, so they -differ exactly on a print that lands ON the carried best. +differ exactly on a print that lands ON the carried best. TradingView books +the touch at the open: 16 of 16 taped trades of that shape, which a kernel +host reproduces only with both legs (`tests/test_zero_trail_sibling_stop.cpp`). Two kernel helpers went with it. `BacktestEngine::round_to_mintick_directional` and `BacktestEngine::apply_slippage` were the pre-R5 spelling of the diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 678d6718..685ca02f 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -126,7 +126,7 @@ rather than defaulted. | `commission_value` | `fee_value` native_run_spec.hpp:589 | `fee_value` pineforge.h:540 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | | `slippage` | `slippage_ticks` native_run_spec.hpp:583 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3958-3959. TradingView's round-then-slip order is adapter-only. | | `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7524-7525, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | -| `pyramiding` | `max_open_lots` native_run_spec.hpp:595 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2121-2124. Pine's per-cycle *entry count* is a TradingView rule, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`. | +| `pyramiding` | `max_open_lots` native_run_spec.hpp:595 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2121-2124. The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). | | `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:344 with `size_basis` native_c_api.h:2104 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | | `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2108 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | | `calc_on_order_fills` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2278 | `native_calc_on_fills_strategy.cpp` | `BarCloseAndFills` recalculates once at the cursor of each applied execution, bounded by `max_recalculations_per_point` native_run_spec.hpp:626. The generic cadence only: TradingView's waypoint-only refill, its two-fills-at-open rule and its script-state rollback stay in the adapter. | @@ -382,7 +382,10 @@ The Pine adapter lowers its own queued relative legs exactly this way — a host-sized `Book` / `AfterArmPrint` child, with `resolve_anchored_level` carrying only TradingView's half-tick threshold — and adopts the armed child at the parent's fill. An explicit `qty=`, `close_entries_rule="ANY"` and stream -runs keep the adapter's fill-point submission. +runs keep the adapter's fill-point submission. The adopted child of a trailing +exit names no `best_seed`, so its ride starts at the raw arm print where +TradingView's starts at the activation: a recorded divergence on 13 of 13 taped +trades (`tests/test_pending_entry_trail_tapes.cpp`). ## Risk limits {#pine_to_native_map_risk} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fd9bf3b2..361bc814 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1324,6 +1324,12 @@ list(APPEND TEST_SOURCES test_adapter_dual_entry_tie) # G2-09/-10/-12/-13 margin scheduling, market-execution checkpoints, the # pre-open slice, opening admission; E20 f1's probe-tail margin call. list(APPEND TEST_SOURCES test_adapter_margin_schedule_differential) +# G2-15 pyramiding as a per-cycle count against max_open_lots and the tape. +list(APPEND TEST_SOURCES test_pyramiding_count_differential) +# G2-18 the sibling Stop beside an explicit-zero trail. +list(APPEND TEST_SOURCES test_zero_trail_sibling_stop) +# E5's pending-entry arm and E14's anchored-leg seed (AUDIT3 H6 tapes). +list(APPEND TEST_SOURCES test_pending_entry_trail_tapes) # R4-D L4b-fix restores every removed ShortSeed, affordability, sizing and # deferred-close oracle as an individually compiled native-route twin. Keep @@ -2057,6 +2063,9 @@ endif() # R5 lane H-MEASURE: the rows that read TradingView tapes from the fixture tree. foreach(_pf_hm_row IN ITEMS + "test_pyramiding_count_differential|PINEFORGE_PYRAMIDING_FIXTURE_DIR|pyramiding_count" + "test_zero_trail_sibling_stop|PINEFORGE_ZERO_TRAIL_FIXTURE_DIR|zero_trail_sibling_stop" + "test_pending_entry_trail_tapes|PINEFORGE_PENDING_TRAIL_FIXTURE_DIR|pending_entry_trail" "test_adapter_margin_schedule_differential|PINEFORGE_HM_M7A_FIXTURE_DIR|margin_entry_bar") string(REPLACE "|" ";" _pf_hm_fields "${_pf_hm_row}") list(GET _pf_hm_fields 0 _pf_hm_target) diff --git a/tests/fixtures/pending_entry_trail/README.md b/tests/fixtures/pending_entry_trail/README.md new file mode 100644 index 00000000..8a0fa5e4 --- /dev/null +++ b/tests/fixtures/pending_entry_trail/README.md @@ -0,0 +1,37 @@ +# Pending-entry trailing-exit tapes (H-MEASURE rows E5 pending-entry arm, E14 anchored-leg seed) + +TradingView's own trades for a `strategy.exit(trail_points=n, trail_offset=k)` +issued on the SIGNAL bar together with the `strategy.entry` it names, so the +exit is placed while its entry is still pending: a MARKET entry that fills at +the next bar's open, or (the two `-limit-` tapes) a LIMIT entry that fills +mid-bar one or two bars later. The Pine adapter lowers that exit to the kernel's anchored +child (`relative_leg_shapes`, armed through `resolve_anchored_level`) and +adopts it at the fill. `tests/test_pending_entry_trail_tapes.cpp` replays every +trade below through the adapter and reads the kernel's record. + +Each directory is one `lab tv` export (pineforge-workflow, channel +`ws-report-v1`, `rangeProof: covered`, NYSE:F 15, 2025-04-01 .. 2026-04-10), +byte-identical: `strategy.pine`, `tv_trades.csv` (times UTC+8), `meta.json`, +`metrics.json`. + +| probe | trades | tvTradesSha256 | TradingView | adapter | +|---|---|---|---|---| +| `hm-orders-h4-f-long-pending-arm` | 8 | `620a55e297f6bd8b40a4a9ed04195f456bbeb24e2f02468a0fe29fa2ced4b9a3` | the arm bar's raw high stops inside the activation's tick cell (10.175, 10.305, 10.615, 10.125, 10.325, 11.899, 13.049, 13.419); every trade exits on that bar at activation - 1 tick (seven on the fill bar itself) | 8/8 | +| `hm-orders-h4-f-short-pending-arm` | 8 | `41d1837adb0ebcc096e544de4abbd65eb2b0abf56553257ae2e004380a221521` | the short twin (lows 9.995, 9.915, 10.145, 10.165, 9.895, 13.041, 13.665, 13.665): activation + 1 tick on the arm bar (four on the fill bar) | 8/8 | +| `hm-orders-h4-f-long-pending-limit-arm` | 6 | `886d8cc2e2caa9134073cbb0910121aaa844824e66e9170e6615ab7128cb6c13` | the entry is a LIMIT (`strategy.entry(limit=X)`, issued with the exit) that fills mid-bar one or two bars later; the high after the fill stops in the activation's cell (10.585, 11.455, 11.635, 12.515, 12.935, 14.055) and every trade exits on that same fill bar at activation - 1 tick | 6/6 | +| `hm-orders-h4-f-short-pending-limit-arm` | 6 | `f88f872fe583f0b85a33909f5bc113fbd3e0c529798f5c21f2e359b95501cf2c` | the short twin (lows 9.825, 9.905, 10.165, 10.165, 9.895, 13.041; arms 0 to 3 bars after the fill): activation + 1 tick on the arm bar | 6/6 | +| `hm-orders-h3-f-long-pending-seed` | 8 | `95b1e8309f3b2f93645f36d68b3b479517b7c303d0b7330b50622364a0787252` | E14's shape with the exit on the signal bar: the fill bar's high reaches the activation only on its tick, the next bar is shallow; every trade exits on that next bar at activation - k ticks (k 3 or 5): the running best starts AT the activation | 0/8 (recorded: exits 5 to 24 bars later) | +| `hm-orders-h3-f-short-pending-seed` | 5 | `03654a5b3f98b45be25f6c9be212943a43ad4e1683c5d4b7d126567ba226966a` | the short set (k 1 to 3; arms on the fill bar or later): activation + k ticks | 0/5 (recorded: 1 to 3 bars later) | + +`bars.inc` holds, per event, the feed bars from the signal bar through the bar +the probe's 25-bar timeout close would fill on (28 bars; 30 for the limit +tapes, whose entry fills one or two bars after the signal), with the per-event +`trail_points`, `trail_offset` and limit level: the registry feed `lab bars NYSE:F 15`, +evidence sha256 `80f404ae85ef0b6a0d8056a90997e92fa1236f1ae68b75c1b2d3a6f182558e32` +(`lab evidence get --out `). The events were chosen with the +search scripts of the H-MEASURE `orders` scratch (`pending_trail_search.py`, +`select_events.py`), which model three readings: a quantized arm with the best +seeded at the activation (TradingView: 29/29), the adapter's anchored child +(best from the raw arm print: 16/29, bit-identical to the adapter), and a raw +arm (0/29); `pending_limit_search.py` does the same for the limit tapes +(TradingView 12/12 = the adapter; raw arm 0/12). diff --git a/tests/fixtures/pending_entry_trail/bars.inc b/tests/fixtures/pending_entry_trail/bars.inc new file mode 100644 index 00000000..36ac4a4f --- /dev/null +++ b/tests/fixtures/pending_entry_trail/bars.inc @@ -0,0 +1,1291 @@ +// Generated by gen_inc.py from the NYSE:F 15m registry feed +// (evidence sha256 80f404ae85ef0b6a0d8056a90997e92fa1236f1ae68b75c1b2d3a6f182558e32). +// Per event: the SIGNAL bar, where strategy.entry(market) and the trailing +// strategy.exit are both issued (the exit while its entry is pending), +// through the bar the 25-bar timeout close fills on. {ts_ms, o, h, l, c} +// H4-L: 8 events, 28 bars each +constexpr double kH4LongPoints[] = {2.0, 2.0, 1.0, 1.0, 2.0, 10.0, 2.0, 1.0}; +constexpr double kH4LongOffsets[] = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}; +constexpr FeedBar kH4Long[8][28] = { + { // signal 2025-04-28 14:00Z, fill 2025-04-28 14:15Z, trail_points 2, trail_offset 1 + {1745848800000LL, 10.105, 10.165, 10.095, 10.16}, + {1745849700000LL, 10.16, 10.175, 10.125, 10.125}, + {1745850600000LL, 10.125, 10.135, 10.1, 10.12}, + {1745851500000LL, 10.12, 10.13, 10.05, 10.065}, + {1745852400000LL, 10.065, 10.07, 10.02, 10.05}, + {1745853300000LL, 10.045, 10.055, 9.99, 10.005}, + {1745854200000LL, 10.005, 10.04, 10, 10.02}, + {1745855100000LL, 10.02, 10.045, 10.01, 10.025}, + {1745856000000LL, 10.025, 10.025, 10, 10.02}, + {1745856900000LL, 10.02, 10.03, 10.005, 10.015}, + {1745857800000LL, 10.015, 10.045, 10.015, 10.035}, + {1745858700000LL, 10.035, 10.04, 10.02, 10.025}, + {1745859600000LL, 10.03, 10.03, 9.99, 9.995}, + {1745860500000LL, 9.995, 10.02, 9.99, 10}, + {1745861400000LL, 9.995, 10, 9.98, 9.995}, + {1745862300000LL, 9.995, 10.015, 9.99, 10.015}, + {1745863200000LL, 10.015, 10.025, 10, 10.005}, + {1745864100000LL, 10.01, 10.03, 10, 10.025}, + {1745865000000LL, 10.025, 10.025, 10.01, 10.02}, + {1745865900000LL, 10.02, 10.02, 10.01, 10.015}, + {1745866800000LL, 10.015, 10.045, 10.015, 10.019}, + {1745867700000LL, 10.02, 10.03, 10.01, 10.025}, + {1745868600000LL, 10.025, 10.03, 10.01, 10.02}, + {1745869500000LL, 10.02, 10.05, 10, 10.015}, + {1745933400000LL, 10.05, 10.065, 9.93, 9.99}, + {1745934300000LL, 9.985, 10.12, 9.985, 10.05}, + {1745935200000LL, 10.04, 10.09, 10.015, 10.06}, + {1745936100000LL, 10.06, 10.075, 10.02, 10.025}, + }, + { // signal 2025-05-02 18:45Z, fill 2025-05-02 19:00Z, trail_points 2, trail_offset 1 + {1746211500000LL, 10.325, 10.33, 10.285, 10.29}, + {1746212400000LL, 10.29, 10.305, 10.285, 10.285}, + {1746213300000LL, 10.285, 10.29, 10.28, 10.285}, + {1746214200000LL, 10.285, 10.285, 10.26, 10.275}, + {1746215100000LL, 10.275, 10.3, 10.25, 10.275}, + {1746451800000LL, 10.23, 10.24, 10.125, 10.155}, + {1746452700000LL, 10.155, 10.16, 10.095, 10.14}, + {1746453600000LL, 10.14, 10.19, 10.14, 10.165}, + {1746454500000LL, 10.165, 10.175, 10.15, 10.17}, + {1746455400000LL, 10.165, 10.21, 10.155, 10.195}, + {1746456300000LL, 10.195, 10.21, 10.18, 10.19}, + {1746457200000LL, 10.19, 10.19, 10.175, 10.185}, + {1746458100000LL, 10.18, 10.195, 10.17, 10.17}, + {1746459000000LL, 10.175, 10.175, 10.14, 10.145}, + {1746459900000LL, 10.15, 10.2, 10.145, 10.185}, + {1746460800000LL, 10.185, 10.19, 10.17, 10.17}, + {1746461700000LL, 10.175, 10.185, 10.17, 10.18}, + {1746462600000LL, 10.18, 10.19, 10.165, 10.185}, + {1746463500000LL, 10.185, 10.195, 10.175, 10.185}, + {1746464400000LL, 10.18, 10.185, 10.16, 10.165}, + {1746465300000LL, 10.16, 10.16, 10.13, 10.135}, + {1746466200000LL, 10.135, 10.15, 10.12, 10.14}, + {1746467100000LL, 10.14, 10.155, 10.13, 10.13}, + {1746468000000LL, 10.13, 10.17, 10.13, 10.17}, + {1746468900000LL, 10.17, 10.175, 10.16, 10.165}, + {1746469800000LL, 10.165, 10.17, 10.15, 10.155}, + {1746470700000LL, 10.155, 10.16, 10.13, 10.13}, + {1746471600000LL, 10.13, 10.14, 10.12, 10.13}, + }, + { // signal 2025-05-21 17:00Z, fill 2025-05-21 17:15Z, trail_points 1, trail_offset 1 + {1747846800000LL, 10.645, 10.65, 10.61, 10.61}, + {1747847700000LL, 10.61, 10.615, 10.515, 10.525}, + {1747848600000LL, 10.52, 10.585, 10.5, 10.56}, + {1747849500000LL, 10.56, 10.56, 10.53, 10.54}, + {1747850400000LL, 10.54, 10.54, 10.51, 10.515}, + {1747851300000LL, 10.515, 10.52, 10.485, 10.505}, + {1747852200000LL, 10.505, 10.54, 10.505, 10.535}, + {1747853100000LL, 10.535, 10.54, 10.495, 10.5}, + {1747854000000LL, 10.5, 10.51, 10.475, 10.5}, + {1747854900000LL, 10.495, 10.5, 10.45, 10.475}, + {1747855800000LL, 10.48, 10.51, 10.475, 10.505}, + {1747856700000LL, 10.51, 10.52, 10.47, 10.475}, + {1747920600000LL, 10.48, 10.48, 10.355, 10.39}, + {1747921500000LL, 10.39, 10.39, 10.345, 10.365}, + {1747922400000LL, 10.365, 10.44, 10.365, 10.37}, + {1747923300000LL, 10.37, 10.39, 10.355, 10.365}, + {1747924200000LL, 10.37, 10.39, 10.365, 10.38}, + {1747925100000LL, 10.375, 10.415, 10.375, 10.415}, + {1747926000000LL, 10.415, 10.45, 10.41, 10.435}, + {1747926900000LL, 10.435, 10.445, 10.415, 10.415}, + {1747927800000LL, 10.415, 10.45, 10.415, 10.445}, + {1747928700000LL, 10.445, 10.45, 10.425, 10.44}, + {1747929600000LL, 10.44, 10.46, 10.43, 10.455}, + {1747930500000LL, 10.46, 10.47, 10.435, 10.435}, + {1747931400000LL, 10.435, 10.44, 10.41, 10.415}, + {1747932300000LL, 10.41, 10.425, 10.41, 10.425}, + {1747933200000LL, 10.425, 10.44, 10.42, 10.435}, + {1747934100000LL, 10.435, 10.465, 10.435, 10.46}, + }, + { // signal 2025-06-02 13:30Z, fill 2025-06-02 13:45Z, trail_points 1, trail_offset 1 + {1748871000000LL, 10.25, 10.3, 10.065, 10.115}, + {1748871900000LL, 10.12, 10.125, 10.04, 10.05}, + {1748872800000LL, 10.05, 10.05, 9.95, 9.96}, + {1748873700000LL, 9.955, 9.965, 9.885, 9.91}, + {1748874600000LL, 9.915, 9.945, 9.9, 9.94}, + {1748875500000LL, 9.94, 9.985, 9.9, 9.985}, + {1748876400000LL, 9.985, 10.02, 9.97, 9.97}, + {1748877300000LL, 9.97, 9.97, 9.9, 9.91}, + {1748878200000LL, 9.91, 9.94, 9.9, 9.935}, + {1748879100000LL, 9.935, 9.935, 9.915, 9.92}, + {1748880000000LL, 9.92, 9.93, 9.9, 9.91}, + {1748880900000LL, 9.915, 9.945, 9.91, 9.945}, + {1748881800000LL, 9.94, 9.94, 9.92, 9.93}, + {1748882700000LL, 9.93, 9.96, 9.925, 9.95}, + {1748883600000LL, 9.945, 9.95, 9.93, 9.93}, + {1748884500000LL, 9.93, 9.945, 9.9, 9.905}, + {1748885400000LL, 9.905, 9.925, 9.905, 9.905}, + {1748886300000LL, 9.905, 9.92, 9.9, 9.915}, + {1748887200000LL, 9.915, 9.93, 9.895, 9.915}, + {1748888100000LL, 9.915, 9.945, 9.915, 9.935}, + {1748889000000LL, 9.93, 9.95, 9.92, 9.95}, + {1748889900000LL, 9.945, 9.97, 9.94, 9.96}, + {1748890800000LL, 9.965, 9.98, 9.96, 9.96}, + {1748891700000LL, 9.955, 9.98, 9.955, 9.965}, + {1748892600000LL, 9.965, 9.99, 9.96, 9.96}, + {1748893500000LL, 9.96, 9.98, 9.945, 9.98}, + {1748957400000LL, 9.965, 9.97, 9.895, 9.955}, + {1748958300000LL, 9.96, 10.01, 9.95, 9.97}, + }, + { // signal 2025-06-04 14:15Z, fill 2025-06-04 14:30Z, trail_points 2, trail_offset 1 + {1749046500000LL, 10.26, 10.325, 10.26, 10.315}, + {1749047400000LL, 10.31, 10.325, 10.27, 10.285}, + {1749048300000LL, 10.28, 10.29, 10.21, 10.235}, + {1749049200000LL, 10.24, 10.245, 10.22, 10.22}, + {1749050100000LL, 10.21, 10.22, 10.19, 10.21}, + {1749051000000LL, 10.215, 10.23, 10.2, 10.21}, + {1749051900000LL, 10.22, 10.255, 10.215, 10.24}, + {1749052800000LL, 10.24, 10.245, 10.21, 10.215}, + {1749053700000LL, 10.215, 10.215, 10.185, 10.185}, + {1749054600000LL, 10.18, 10.2, 10.18, 10.195}, + {1749055500000LL, 10.2, 10.22, 10.2, 10.21}, + {1749056400000LL, 10.205, 10.225, 10.205, 10.215}, + {1749057300000LL, 10.215, 10.22, 10.21, 10.215}, + {1749058200000LL, 10.215, 10.24, 10.21, 10.235}, + {1749059100000LL, 10.235, 10.245, 10.23, 10.23}, + {1749060000000LL, 10.235, 10.24, 10.21, 10.21}, + {1749060900000LL, 10.21, 10.215, 10.2, 10.205}, + {1749061800000LL, 10.21, 10.215, 10.205, 10.205}, + {1749062700000LL, 10.205, 10.22, 10.19, 10.215}, + {1749063600000LL, 10.215, 10.225, 10.21, 10.225}, + {1749064500000LL, 10.225, 10.25, 10.225, 10.25}, + {1749065400000LL, 10.25, 10.265, 10.245, 10.26}, + {1749066300000LL, 10.26, 10.265, 10.225, 10.235}, + {1749130200000LL, 10.2, 10.22, 10.14, 10.14}, + {1749131100000LL, 10.14, 10.15, 10.055, 10.075}, + {1749132000000LL, 10.07, 10.12, 10.07, 10.115}, + {1749132900000LL, 10.12, 10.16, 10.1, 10.155}, + {1749133800000LL, 10.155, 10.185, 10.11, 10.17}, + }, + { // signal 2025-09-26 14:00Z, fill 2025-09-26 14:15Z, trail_points 10, trail_offset 1 + {1758895200000LL, 11.74, 11.81, 11.73, 11.805}, + {1758896100000LL, 11.8, 11.899, 11.8, 11.87}, + {1758897000000LL, 11.88, 11.93, 11.875, 11.93}, + {1758897900000LL, 11.925, 11.97, 11.925, 11.955}, + {1758898800000LL, 11.955, 12.11, 11.95, 12.105}, + {1758899700000LL, 12.11, 12.235, 12.095, 12.225}, + {1758900600000LL, 12.23, 12.31, 12.205, 12.295}, + {1758901500000LL, 12.295, 12.305, 12.045, 12.05}, + {1758902400000LL, 12.055, 12.055, 11.995, 12.03}, + {1758903300000LL, 12.04, 12.08, 12.025, 12.025}, + {1758904200000LL, 12.025, 12.045, 12.01, 12.035}, + {1758905100000LL, 12.03, 12.04, 12.01, 12.03}, + {1758906000000LL, 12.025, 12.04, 12.005, 12.03}, + {1758906900000LL, 12.04, 12.06, 12.02, 12.02}, + {1758907800000LL, 12.025, 12.025, 11.995, 12.02}, + {1758908700000LL, 12.015, 12.02, 11.98, 12.02}, + {1758909600000LL, 12.015, 12.025, 12.005, 12.02}, + {1758910500000LL, 12.02, 12.03, 11.995, 12}, + {1758911400000LL, 12, 12.01, 11.99, 11.995}, + {1758912300000LL, 11.995, 12.05, 11.99, 12.04}, + {1758913200000LL, 12.035, 12.04, 12.01, 12.015}, + {1758914100000LL, 12.01, 12.03, 12, 12.005}, + {1758915000000LL, 12.005, 12.04, 12, 12}, + {1758915900000LL, 12.005, 12.03, 12.005, 12.015}, + {1759152600000LL, 12.075, 12.115, 11.9, 11.925}, + {1759153500000LL, 11.92, 11.95, 11.895, 11.94}, + {1759154400000LL, 11.94, 11.98, 11.93, 11.94}, + {1759155300000LL, 11.935, 11.955, 11.92, 11.935}, + }, + { // signal 2025-11-18 18:45Z, fill 2025-11-18 19:00Z, trail_points 2, trail_offset 1 + {1763491500000LL, 13.065, 13.07, 13.015, 13.02}, + {1763492400000LL, 13.03, 13.04, 13.015, 13.03}, + {1763493300000LL, 13.03, 13.04, 13.01, 13.02}, + {1763494200000LL, 13.02, 13.049, 13.005, 13.025}, + {1763495100000LL, 13.03, 13.05, 13.01, 13.035}, + {1763496000000LL, 13.035, 13.055, 13.03, 13.04}, + {1763496900000LL, 13.045, 13.055, 13.03, 13.045}, + {1763497800000LL, 13.04, 13.08, 13.04, 13.05}, + {1763498700000LL, 13.055, 13.06, 13.02, 13.035}, + {1763562600000LL, 13.04, 13.06, 13, 13}, + {1763563500000LL, 13, 13.05, 13, 13.045}, + {1763564400000LL, 13.045, 13.085, 13.015, 13.085}, + {1763565300000LL, 13.08, 13.13, 13.08, 13.125}, + {1763566200000LL, 13.13, 13.15, 13.1, 13.105}, + {1763567100000LL, 13.11, 13.115, 13.04, 13.04}, + {1763568000000LL, 13.04, 13.06, 13.035, 13.055}, + {1763568900000LL, 13.055, 13.07, 13.025, 13.045}, + {1763569800000LL, 13.05, 13.065, 13.02, 13.04}, + {1763570700000LL, 13.04, 13.05, 13.02, 13.05}, + {1763571600000LL, 13.06, 13.07, 13, 13.01}, + {1763572500000LL, 13.01, 13.02, 12.995, 13.005}, + {1763573400000LL, 13.01, 13.02, 12.965, 13.01}, + {1763574300000LL, 13.01, 13.01, 12.97, 12.985}, + {1763575200000LL, 12.985, 12.99, 12.96, 12.965}, + {1763576100000LL, 12.97, 12.98, 12.945, 12.945}, + {1763577000000LL, 12.94, 12.99, 12.94, 12.95}, + {1763577900000LL, 12.945, 12.99, 12.92, 12.975}, + {1763578800000LL, 12.975, 12.975, 12.925, 12.94}, + }, + { // signal 2026-01-20 17:00Z, fill 2026-01-20 17:15Z, trail_points 1, trail_offset 1 + {1768928400000LL, 13.37, 13.415, 13.35, 13.41}, + {1768929300000LL, 13.41, 13.419, 13.355, 13.36}, + {1768930200000LL, 13.355, 13.355, 13.32, 13.325}, + {1768931100000LL, 13.32, 13.325, 13.29, 13.305}, + {1768932000000LL, 13.3, 13.31, 13.28, 13.295}, + {1768932900000LL, 13.295, 13.31, 13.285, 13.295}, + {1768933800000LL, 13.3, 13.315, 13.275, 13.285}, + {1768934700000LL, 13.285, 13.29, 13.25, 13.26}, + {1768935600000LL, 13.27, 13.3, 13.255, 13.285}, + {1768936500000LL, 13.28, 13.29, 13.26, 13.265}, + {1768937400000LL, 13.265, 13.27, 13.24, 13.26}, + {1768938300000LL, 13.26, 13.26, 13.24, 13.26}, + {1768939200000LL, 13.26, 13.26, 13.235, 13.255}, + {1768940100000LL, 13.25, 13.28, 13.24, 13.275}, + {1768941000000LL, 13.27, 13.28, 13.265, 13.27}, + {1768941900000LL, 13.265, 13.295, 13.265, 13.275}, + {1769005800000LL, 13.405, 13.565, 13.405, 13.54}, + {1769006700000LL, 13.55, 13.555, 13.49, 13.495}, + {1769007600000LL, 13.495, 13.56, 13.485, 13.555}, + {1769008500000LL, 13.555, 13.575, 13.545, 13.575}, + {1769009400000LL, 13.58, 13.605, 13.58, 13.595}, + {1769010300000LL, 13.59, 13.605, 13.575, 13.59}, + {1769011200000LL, 13.585, 13.6, 13.555, 13.595}, + {1769012100000LL, 13.595, 13.62, 13.57, 13.6}, + {1769013000000LL, 13.595, 13.61, 13.585, 13.6}, + {1769013900000LL, 13.595, 13.61, 13.555, 13.57}, + {1769014800000LL, 13.57, 13.58, 13.535, 13.54}, + {1769015700000LL, 13.535, 13.59, 13.53, 13.58}, + }, +}; +// H3-L: 8 events, 28 bars each +constexpr double kH3LongPoints[] = {6.0, 1.0, 3.0, 4.0, 1.0, 4.0, 4.0, 2.0}; +constexpr double kH3LongOffsets[] = {5.0, 3.0, 5.0, 3.0, 5.0, 5.0, 5.0, 5.0}; +constexpr FeedBar kH3Long[8][28] = { + { // signal 2025-05-27 13:45Z, fill 2025-05-27 14:00Z, trail_points 6, trail_offset 5 + {1748353500000LL, 10.36, 10.38, 10.27, 10.28}, + {1748354400000LL, 10.28, 10.335, 10.27, 10.305}, + {1748355300000LL, 10.3, 10.355, 10.29, 10.325}, + {1748356200000LL, 10.33, 10.35, 10.32, 10.35}, + {1748357100000LL, 10.345, 10.36, 10.34, 10.35}, + {1748358000000LL, 10.355, 10.36, 10.33, 10.33}, + {1748358900000LL, 10.33, 10.34, 10.315, 10.32}, + {1748359800000LL, 10.325, 10.345, 10.32, 10.34}, + {1748360700000LL, 10.335, 10.34, 10.31, 10.31}, + {1748361600000LL, 10.31, 10.32, 10.3, 10.3}, + {1748362500000LL, 10.305, 10.32, 10.3, 10.3}, + {1748363400000LL, 10.3, 10.31, 10.28, 10.285}, + {1748364300000LL, 10.285, 10.285, 10.27, 10.28}, + {1748365200000LL, 10.285, 10.285, 10.22, 10.225}, + {1748366100000LL, 10.225, 10.24, 10.215, 10.22}, + {1748367000000LL, 10.225, 10.27, 10.22, 10.265}, + {1748367900000LL, 10.265, 10.28, 10.25, 10.28}, + {1748368800000LL, 10.28, 10.285, 10.26, 10.28}, + {1748369700000LL, 10.28, 10.28, 10.27, 10.28}, + {1748370600000LL, 10.28, 10.3, 10.275, 10.3}, + {1748371500000LL, 10.295, 10.31, 10.28, 10.28}, + {1748372400000LL, 10.285, 10.305, 10.28, 10.29}, + {1748373300000LL, 10.285, 10.31, 10.285, 10.295}, + {1748374200000LL, 10.295, 10.31, 10.29, 10.305}, + {1748375100000LL, 10.305, 10.315, 10.295, 10.31}, + {1748439000000LL, 10.32, 10.33, 10.22, 10.225}, + {1748439900000LL, 10.23, 10.265, 10.205, 10.235}, + {1748440800000LL, 10.235, 10.26, 10.21, 10.24}, + }, + { // signal 2025-06-26 16:30Z, fill 2025-06-26 16:45Z, trail_points 1, trail_offset 3 + {1750955400000LL, 10.57, 10.59, 10.57, 10.585}, + {1750956300000LL, 10.58, 10.585, 10.565, 10.575}, + {1750957200000LL, 10.57, 10.6, 10.56, 10.59}, + {1750958100000LL, 10.6, 10.605, 10.585, 10.59}, + {1750959000000LL, 10.595, 10.615, 10.595, 10.615}, + {1750959900000LL, 10.61, 10.62, 10.605, 10.61}, + {1750960800000LL, 10.605, 10.63, 10.605, 10.625}, + {1750961700000LL, 10.625, 10.64, 10.62, 10.63}, + {1750962600000LL, 10.625, 10.625, 10.61, 10.615}, + {1750963500000LL, 10.615, 10.64, 10.605, 10.635}, + {1750964400000LL, 10.635, 10.65, 10.635, 10.64}, + {1750965300000LL, 10.645, 10.645, 10.62, 10.625}, + {1750966200000LL, 10.625, 10.625, 10.605, 10.605}, + {1750967100000LL, 10.605, 10.645, 10.6, 10.635}, + {1751031000000LL, 10.7, 10.81, 10.68, 10.765}, + {1751031900000LL, 10.76, 10.77, 10.73, 10.735}, + {1751032800000LL, 10.74, 10.775, 10.73, 10.775}, + {1751033700000LL, 10.775, 10.79, 10.765, 10.77}, + {1751034600000LL, 10.77, 10.8, 10.75, 10.79}, + {1751035500000LL, 10.795, 10.83, 10.77, 10.79}, + {1751036400000LL, 10.79, 10.81, 10.77, 10.805}, + {1751037300000LL, 10.8, 10.87, 10.8, 10.86}, + {1751038200000LL, 10.865, 10.88, 10.85, 10.875}, + {1751039100000LL, 10.87, 10.9, 10.86, 10.885}, + {1751040000000LL, 10.89, 10.91, 10.88, 10.89}, + {1751040900000LL, 10.89, 10.92, 10.885, 10.91}, + {1751041800000LL, 10.91, 10.91, 10.87, 10.87}, + {1751042700000LL, 10.87, 10.875, 10.85, 10.85}, + }, + { // signal 2025-07-25 14:00Z, fill 2025-07-25 14:15Z, trail_points 3, trail_offset 5 + {1753452000000LL, 11.275, 11.305, 11.245, 11.265}, + {1753452900000LL, 11.27, 11.295, 11.25, 11.255}, + {1753453800000LL, 11.255, 11.29, 11.25, 11.275}, + {1753454700000LL, 11.275, 11.295, 11.27, 11.285}, + {1753455600000LL, 11.285, 11.305, 11.26, 11.27}, + {1753456500000LL, 11.275, 11.3, 11.265, 11.285}, + {1753457400000LL, 11.29, 11.3, 11.28, 11.28}, + {1753458300000LL, 11.285, 11.29, 11.28, 11.29}, + {1753459200000LL, 11.285, 11.3, 11.28, 11.285}, + {1753460100000LL, 11.29, 11.29, 11.275, 11.28}, + {1753461000000LL, 11.28, 11.325, 11.27, 11.325}, + {1753461900000LL, 11.325, 11.33, 11.32, 11.325}, + {1753462800000LL, 11.325, 11.35, 11.32, 11.35}, + {1753463700000LL, 11.35, 11.37, 11.325, 11.36}, + {1753464600000LL, 11.355, 11.38, 11.355, 11.38}, + {1753465500000LL, 11.38, 11.39, 11.375, 11.39}, + {1753466400000LL, 11.39, 11.39, 11.38, 11.39}, + {1753467300000LL, 11.39, 11.4, 11.385, 11.4}, + {1753468200000LL, 11.395, 11.4, 11.39, 11.395}, + {1753469100000LL, 11.395, 11.42, 11.38, 11.415}, + {1753470000000LL, 11.415, 11.48, 11.415, 11.475}, + {1753470900000LL, 11.475, 11.48, 11.44, 11.445}, + {1753471800000LL, 11.445, 11.46, 11.44, 11.455}, + {1753472700000LL, 11.455, 11.49, 11.45, 11.465}, + {1753709400000LL, 11.48, 11.48, 11.37, 11.4}, + {1753710300000LL, 11.405, 11.41, 11.33, 11.34}, + {1753711200000LL, 11.34, 11.38, 11.33, 11.375}, + {1753712100000LL, 11.375, 11.375, 11.355, 11.365}, + }, + { // signal 2025-08-25 14:30Z, fill 2025-08-25 14:45Z, trail_points 4, trail_offset 3 + {1756132200000LL, 11.7, 11.71, 11.68, 11.7}, + {1756133100000LL, 11.71, 11.745, 11.71, 11.74}, + {1756134000000LL, 11.74, 11.745, 11.72, 11.74}, + {1756134900000LL, 11.74, 11.74, 11.725, 11.725}, + {1756135800000LL, 11.73, 11.77, 11.73, 11.755}, + {1756136700000LL, 11.755, 11.76, 11.745, 11.755}, + {1756137600000LL, 11.755, 11.755, 11.745, 11.745}, + {1756138500000LL, 11.745, 11.75, 11.73, 11.74}, + {1756139400000LL, 11.735, 11.745, 11.735, 11.745}, + {1756140300000LL, 11.745, 11.78, 11.745, 11.78}, + {1756141200000LL, 11.78, 11.79, 11.77, 11.785}, + {1756142100000LL, 11.785, 11.81, 11.785, 11.805}, + {1756143000000LL, 11.805, 11.82, 11.795, 11.82}, + {1756143900000LL, 11.82, 11.845, 11.82, 11.82}, + {1756144800000LL, 11.82, 11.82, 11.8, 11.805}, + {1756145700000LL, 11.805, 11.805, 11.765, 11.785}, + {1756146600000LL, 11.78, 11.79, 11.775, 11.79}, + {1756147500000LL, 11.79, 11.805, 11.785, 11.79}, + {1756148400000LL, 11.795, 11.8, 11.79, 11.8}, + {1756149300000LL, 11.795, 11.795, 11.77, 11.785}, + {1756150200000LL, 11.78, 11.795, 11.775, 11.795}, + {1756151100000LL, 11.795, 11.825, 11.78, 11.785}, + {1756215000000LL, 11.82, 11.85, 11.785, 11.845}, + {1756215900000LL, 11.845, 11.87, 11.825, 11.85}, + {1756216800000LL, 11.845, 11.87, 11.845, 11.85}, + {1756217700000LL, 11.855, 11.855, 11.815, 11.82}, + {1756218600000LL, 11.83, 11.83, 11.8, 11.8}, + {1756219500000LL, 11.8, 11.815, 11.775, 11.815}, + }, + { // signal 2025-09-11 18:00Z, fill 2025-09-11 18:15Z, trail_points 1, trail_offset 5 + {1757613600000LL, 11.7, 11.73, 11.7, 11.73}, + {1757614500000LL, 11.73, 11.735, 11.705, 11.72}, + {1757615400000LL, 11.72, 11.725, 11.69, 11.695}, + {1757616300000LL, 11.695, 11.715, 11.695, 11.715}, + {1757617200000LL, 11.715, 11.74, 11.715, 11.72}, + {1757618100000LL, 11.725, 11.735, 11.725, 11.73}, + {1757619000000LL, 11.73, 11.745, 11.72, 11.745}, + {1757619900000LL, 11.745, 11.765, 11.735, 11.755}, + {1757683800000LL, 11.76, 11.79, 11.73, 11.745}, + {1757684700000LL, 11.745, 11.78, 11.725, 11.755}, + {1757685600000LL, 11.755, 11.755, 11.715, 11.735}, + {1757686500000LL, 11.74, 11.74, 11.7, 11.705}, + {1757687400000LL, 11.705, 11.71, 11.67, 11.675}, + {1757688300000LL, 11.675, 11.685, 11.67, 11.675}, + {1757689200000LL, 11.675, 11.735, 11.66, 11.735}, + {1757690100000LL, 11.73, 11.745, 11.69, 11.69}, + {1757691000000LL, 11.695, 11.695, 11.675, 11.685}, + {1757691900000LL, 11.685, 11.685, 11.67, 11.68}, + {1757692800000LL, 11.68, 11.71, 11.675, 11.69}, + {1757693700000LL, 11.69, 11.7, 11.675, 11.68}, + {1757694600000LL, 11.68, 11.71, 11.68, 11.71}, + {1757695500000LL, 11.71, 11.73, 11.71, 11.725}, + {1757696400000LL, 11.725, 11.805, 11.72, 11.75}, + {1757697300000LL, 11.745, 11.765, 11.745, 11.755}, + {1757698200000LL, 11.755, 11.77, 11.75, 11.75}, + {1757699100000LL, 11.755, 11.76, 11.735, 11.745}, + {1757700000000LL, 11.75, 11.76, 11.735, 11.735}, + {1757700900000LL, 11.735, 11.76, 11.715, 11.76}, + }, + { // signal 2025-10-02 16:45Z, fill 2025-10-02 17:00Z, trail_points 4, trail_offset 5 + {1759423500000LL, 12.14, 12.19, 12.125, 12.175}, + {1759424400000LL, 12.18, 12.215, 12.17, 12.21}, + {1759425300000LL, 12.2, 12.205, 12.17, 12.195}, + {1759426200000LL, 12.195, 12.205, 12.185, 12.19}, + {1759427100000LL, 12.185, 12.205, 12.185, 12.19}, + {1759428000000LL, 12.19, 12.205, 12.17, 12.19}, + {1759428900000LL, 12.19, 12.2, 12.18, 12.2}, + {1759429800000LL, 12.2, 12.205, 12.18, 12.185}, + {1759430700000LL, 12.185, 12.195, 12.18, 12.18}, + {1759431600000LL, 12.185, 12.22, 12.185, 12.21}, + {1759432500000LL, 12.215, 12.24, 12.205, 12.24}, + {1759433400000LL, 12.23, 12.24, 12.22, 12.23}, + {1759434300000LL, 12.23, 12.24, 12.215, 12.235}, + {1759498200000LL, 12.245, 12.295, 12.2, 12.28}, + {1759499100000LL, 12.27, 12.31, 12.25, 12.31}, + {1759500000000LL, 12.31, 12.32, 12.255, 12.29}, + {1759500900000LL, 12.29, 12.31, 12.26, 12.275}, + {1759501800000LL, 12.275, 12.295, 12.275, 12.285}, + {1759502700000LL, 12.285, 12.31, 12.285, 12.295}, + {1759503600000LL, 12.29, 12.295, 12.27, 12.275}, + {1759504500000LL, 12.27, 12.3, 12.26, 12.275}, + {1759505400000LL, 12.28, 12.28, 12.26, 12.28}, + {1759506300000LL, 12.28, 12.31, 12.24, 12.3}, + {1759507200000LL, 12.3, 12.35, 12.3, 12.35}, + {1759508100000LL, 12.35, 12.35, 12.32, 12.32}, + {1759509000000LL, 12.32, 12.325, 12.31, 12.325}, + {1759509900000LL, 12.32, 12.345, 12.32, 12.34}, + {1759510800000LL, 12.34, 12.35, 12.315, 12.34}, + }, + { // signal 2025-12-29 14:30Z, fill 2025-12-29 14:45Z, trail_points 4, trail_offset 5 + {1767018600000LL, 13.28, 13.31, 13.25, 13.25}, + {1767019500000LL, 13.26, 13.295, 13.26, 13.29}, + {1767020400000LL, 13.285, 13.29, 13.25, 13.26}, + {1767021300000LL, 13.26, 13.275, 13.255, 13.265}, + {1767022200000LL, 13.27, 13.28, 13.26, 13.28}, + {1767023100000LL, 13.275, 13.285, 13.26, 13.27}, + {1767024000000LL, 13.27, 13.3, 13.26, 13.29}, + {1767024900000LL, 13.28, 13.28, 13.26, 13.265}, + {1767025800000LL, 13.265, 13.295, 13.26, 13.275}, + {1767026700000LL, 13.275, 13.295, 13.275, 13.285}, + {1767027600000LL, 13.285, 13.295, 13.28, 13.285}, + {1767028500000LL, 13.285, 13.3, 13.28, 13.285}, + {1767029400000LL, 13.285, 13.29, 13.28, 13.29}, + {1767030300000LL, 13.29, 13.3, 13.275, 13.285}, + {1767031200000LL, 13.285, 13.295, 13.28, 13.29}, + {1767032100000LL, 13.285, 13.31, 13.285, 13.295}, + {1767033000000LL, 13.295, 13.3, 13.28, 13.285}, + {1767033900000LL, 13.28, 13.29, 13.27, 13.28}, + {1767034800000LL, 13.28, 13.28, 13.265, 13.275}, + {1767035700000LL, 13.275, 13.28, 13.27, 13.28}, + {1767036600000LL, 13.28, 13.295, 13.28, 13.295}, + {1767037500000LL, 13.295, 13.295, 13.28, 13.285}, + {1767038400000LL, 13.285, 13.3, 13.28, 13.295}, + {1767039300000LL, 13.3, 13.3, 13.275, 13.275}, + {1767040200000LL, 13.28, 13.29, 13.27, 13.275}, + {1767041100000LL, 13.275, 13.285, 13.265, 13.275}, + {1767105000000LL, 13.28, 13.29, 13.23, 13.245}, + {1767105900000LL, 13.245, 13.27, 13.24, 13.25}, + }, + { // signal 2026-03-25 15:45Z, fill 2026-03-25 16:00Z, trail_points 2, trail_offset 5 + {1774453500000LL, 11.715, 11.73, 11.695, 11.72}, + {1774454400000LL, 11.72, 11.735, 11.7, 11.705}, + {1774455300000LL, 11.705, 11.73, 11.69, 11.71}, + {1774456200000LL, 11.71, 11.745, 11.71, 11.72}, + {1774457100000LL, 11.725, 11.75, 11.715, 11.73}, + {1774458000000LL, 11.735, 11.745, 11.71, 11.725}, + {1774458900000LL, 11.725, 11.73, 11.705, 11.725}, + {1774459800000LL, 11.72, 11.775, 11.715, 11.75}, + {1774460700000LL, 11.75, 11.75, 11.71, 11.725}, + {1774461600000LL, 11.72, 11.75, 11.72, 11.75}, + {1774462500000LL, 11.745, 11.76, 11.72, 11.725}, + {1774463400000LL, 11.725, 11.735, 11.72, 11.73}, + {1774464300000LL, 11.73, 11.75, 11.71, 11.735}, + {1774465200000LL, 11.74, 11.74, 11.71, 11.72}, + {1774466100000LL, 11.715, 11.73, 11.71, 11.725}, + {1774467000000LL, 11.725, 11.73, 11.71, 11.71}, + {1774467900000LL, 11.715, 11.72, 11.66, 11.67}, + {1774531800000LL, 11.55, 11.65, 11.49, 11.645}, + {1774532700000LL, 11.65, 11.7, 11.645, 11.7}, + {1774533600000LL, 11.7, 11.71, 11.66, 11.7}, + {1774534500000LL, 11.7, 11.705, 11.67, 11.67}, + {1774535400000LL, 11.67, 11.69, 11.64, 11.645}, + {1774536300000LL, 11.64, 11.665, 11.61, 11.64}, + {1774537200000LL, 11.64, 11.655, 11.63, 11.635}, + {1774538100000LL, 11.631, 11.64, 11.615, 11.62}, + {1774539000000LL, 11.615, 11.63, 11.6, 11.615}, + {1774539900000LL, 11.615, 11.64, 11.605, 11.62}, + {1774540800000LL, 11.62, 11.625, 11.61, 11.62}, + }, +}; +// H4-S: 8 events, 28 bars each +constexpr double kH4ShortPoints[] = {1.0, 4.0, 1.0, 2.0, 7.0, 5.0, 2.0, 1.0}; +constexpr double kH4ShortOffsets[] = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}; +constexpr FeedBar kH4Short[8][28] = { + { // signal 2025-04-24 15:30Z, fill 2025-04-24 15:45Z, trail_points 1, trail_offset 1 + {1745508600000LL, 9.995, 10.02, 9.99, 10}, + {1745509500000LL, 10, 10.045, 9.995, 10.025}, + {1745510400000LL, 10.025, 10.06, 10.025, 10.055}, + {1745511300000LL, 10.055, 10.065, 10.04, 10.055}, + {1745512200000LL, 10.055, 10.06, 10.045, 10.055}, + {1745513100000LL, 10.055, 10.06, 10.04, 10.05}, + {1745514000000LL, 10.05, 10.055, 10.015, 10.015}, + {1745514900000LL, 10.015, 10.03, 10, 10.02}, + {1745515800000LL, 10.025, 10.04, 10.025, 10.035}, + {1745516700000LL, 10.035, 10.045, 10.015, 10.045}, + {1745517600000LL, 10.04, 10.07, 10.04, 10.045}, + {1745518500000LL, 10.045, 10.045, 10.025, 10.025}, + {1745519400000LL, 10.025, 10.055, 10.02, 10.045}, + {1745520300000LL, 10.045, 10.05, 10.03, 10.03}, + {1745521200000LL, 10.03, 10.03, 10, 10.01}, + {1745522100000LL, 10.005, 10.03, 10, 10.015}, + {1745523000000LL, 10.015, 10.09, 10.015, 10.075}, + {1745523900000LL, 10.08, 10.085, 10.05, 10.065}, + {1745587800000LL, 10.04, 10.075, 10.03, 10.045}, + {1745588700000LL, 10.05, 10.08, 10.02, 10.03}, + {1745589600000LL, 10.03, 10.095, 10.03, 10.085}, + {1745590500000LL, 10.08, 10.09, 10.04, 10.055}, + {1745591400000LL, 10.055, 10.095, 10.045, 10.095}, + {1745592300000LL, 10.09, 10.1, 10.08, 10.09}, + {1745593200000LL, 10.09, 10.09, 10.06, 10.075}, + {1745594100000LL, 10.075, 10.09, 10.06, 10.075}, + {1745595000000LL, 10.075, 10.08, 10.05, 10.055}, + {1745595900000LL, 10.055, 10.07, 10.05, 10.06}, + }, + { // signal 2025-04-30 15:00Z, fill 2025-04-30 15:15Z, trail_points 4, trail_offset 1 + {1746025200000LL, 9.935, 9.97, 9.935, 9.955}, + {1746026100000LL, 9.95, 9.96, 9.92, 9.94}, + {1746027000000LL, 9.935, 9.955, 9.92, 9.945}, + {1746027900000LL, 9.945, 9.955, 9.915, 9.93}, + {1746028800000LL, 9.94, 9.95, 9.925, 9.925}, + {1746029700000LL, 9.93, 9.94, 9.9, 9.905}, + {1746030600000LL, 9.9, 9.92, 9.89, 9.905}, + {1746031500000LL, 9.91, 9.93, 9.9, 9.92}, + {1746032400000LL, 9.915, 9.93, 9.865, 9.89}, + {1746033300000LL, 9.885, 9.93, 9.885, 9.93}, + {1746034200000LL, 9.925, 9.95, 9.925, 9.945}, + {1746035100000LL, 9.94, 9.95, 9.92, 9.935}, + {1746036000000LL, 9.935, 9.955, 9.9, 9.935}, + {1746036900000LL, 9.93, 9.95, 9.92, 9.93}, + {1746037800000LL, 9.93, 9.945, 9.905, 9.915}, + {1746038700000LL, 9.915, 9.94, 9.91, 9.915}, + {1746039600000LL, 9.92, 9.93, 9.91, 9.93}, + {1746040500000LL, 9.93, 9.94, 9.91, 9.91}, + {1746041400000LL, 9.915, 9.945, 9.91, 9.945}, + {1746042300000LL, 9.945, 10.04, 9.945, 9.99}, + {1746106200000LL, 10.15, 10.23, 10.025, 10.07}, + {1746107100000LL, 10.07, 10.12, 10.05, 10.1}, + {1746108000000LL, 10.095, 10.245, 10.095, 10.225}, + {1746108900000LL, 10.225, 10.245, 10.22, 10.24}, + {1746109800000LL, 10.235, 10.255, 10.195, 10.245}, + {1746110700000LL, 10.245, 10.3, 10.245, 10.285}, + {1746111600000LL, 10.28, 10.315, 10.26, 10.31}, + {1746112500000LL, 10.31, 10.32, 10.27, 10.28}, + }, + { // signal 2025-05-05 15:30Z, fill 2025-05-05 15:45Z, trail_points 1, trail_offset 1 + {1746459000000LL, 10.175, 10.175, 10.14, 10.145}, + {1746459900000LL, 10.15, 10.2, 10.145, 10.185}, + {1746460800000LL, 10.185, 10.19, 10.17, 10.17}, + {1746461700000LL, 10.175, 10.185, 10.17, 10.18}, + {1746462600000LL, 10.18, 10.19, 10.165, 10.185}, + {1746463500000LL, 10.185, 10.195, 10.175, 10.185}, + {1746464400000LL, 10.18, 10.185, 10.16, 10.165}, + {1746465300000LL, 10.16, 10.16, 10.13, 10.135}, + {1746466200000LL, 10.135, 10.15, 10.12, 10.14}, + {1746467100000LL, 10.14, 10.155, 10.13, 10.13}, + {1746468000000LL, 10.13, 10.17, 10.13, 10.17}, + {1746468900000LL, 10.17, 10.175, 10.16, 10.165}, + {1746469800000LL, 10.165, 10.17, 10.15, 10.155}, + {1746470700000LL, 10.155, 10.16, 10.13, 10.13}, + {1746471600000LL, 10.13, 10.14, 10.12, 10.13}, + {1746472500000LL, 10.125, 10.13, 10.11, 10.125}, + {1746473400000LL, 10.125, 10.13, 10.105, 10.105}, + {1746474300000LL, 10.105, 10.2, 10.1, 10.2}, + {1746538200000LL, 10.09, 10.41, 10.055, 10.34}, + {1746539100000LL, 10.34, 10.38, 10.215, 10.285}, + {1746540000000LL, 10.295, 10.57, 10.29, 10.555}, + {1746540900000LL, 10.555, 10.6, 10.52, 10.6}, + {1746541800000LL, 10.595, 10.62, 10.56, 10.62}, + {1746542700000LL, 10.62, 10.62, 10.525, 10.575}, + {1746543600000LL, 10.575, 10.595, 10.5, 10.5}, + {1746544500000LL, 10.505, 10.56, 10.505, 10.56}, + {1746545400000LL, 10.55, 10.585, 10.535, 10.54}, + {1746546300000LL, 10.535, 10.535, 10.46, 10.465}, + }, + { // signal 2025-05-28 16:00Z, fill 2025-05-28 16:15Z, trail_points 2, trail_offset 1 + {1748448000000LL, 10.16, 10.18, 10.15, 10.175}, + {1748448900000LL, 10.18, 10.185, 10.165, 10.185}, + {1748449800000LL, 10.18, 10.2, 10.18, 10.2}, + {1748450700000LL, 10.195, 10.21, 10.195, 10.205}, + {1748451600000LL, 10.205, 10.24, 10.205, 10.24}, + {1748452500000LL, 10.235, 10.25, 10.225, 10.23}, + {1748453400000LL, 10.23, 10.23, 10.22, 10.23}, + {1748454300000LL, 10.23, 10.235, 10.225, 10.23}, + {1748455200000LL, 10.23, 10.25, 10.23, 10.245}, + {1748456100000LL, 10.245, 10.25, 10.225, 10.235}, + {1748457000000LL, 10.235, 10.24, 10.22, 10.225}, + {1748457900000LL, 10.22, 10.225, 10.18, 10.215}, + {1748458800000LL, 10.215, 10.225, 10.2, 10.21}, + {1748459700000LL, 10.21, 10.215, 10.19, 10.195}, + {1748460600000LL, 10.19, 10.21, 10.185, 10.205}, + {1748461500000LL, 10.205, 10.205, 10.17, 10.18}, + {1748525400000LL, 10.215, 10.22, 10.17, 10.19}, + {1748526300000LL, 10.185, 10.255, 10.185, 10.25}, + {1748527200000LL, 10.255, 10.275, 10.22, 10.235}, + {1748528100000LL, 10.23, 10.23, 10.2, 10.21}, + {1748529000000LL, 10.21, 10.215, 10.17, 10.205}, + {1748529900000LL, 10.205, 10.22, 10.195, 10.215}, + {1748530800000LL, 10.215, 10.22, 10.2, 10.205}, + {1748531700000LL, 10.2, 10.235, 10.2, 10.235}, + {1748532600000LL, 10.23, 10.23, 10.165, 10.185}, + {1748533500000LL, 10.18, 10.195, 10.12, 10.125}, + {1748534400000LL, 10.125, 10.16, 10.12, 10.15}, + {1748535300000LL, 10.16, 10.185, 10.155, 10.18}, + }, + { // signal 2025-06-02 19:30Z, fill 2025-06-02 19:45Z, trail_points 7, trail_offset 1 + {1748892600000LL, 9.965, 9.99, 9.96, 9.96}, + {1748893500000LL, 9.96, 9.98, 9.945, 9.98}, + {1748957400000LL, 9.965, 9.97, 9.895, 9.955}, + {1748958300000LL, 9.96, 10.01, 9.95, 9.97}, + {1748959200000LL, 9.975, 10.02, 9.97, 10.02}, + {1748960100000LL, 10.015, 10.065, 10.005, 10.065}, + {1748961000000LL, 10.065, 10.11, 10.045, 10.105}, + {1748961900000LL, 10.105, 10.16, 10.105, 10.125}, + {1748962800000LL, 10.13, 10.155, 10.125, 10.145}, + {1748963700000LL, 10.14, 10.155, 10.13, 10.14}, + {1748964600000LL, 10.14, 10.16, 10.135, 10.145}, + {1748965500000LL, 10.14, 10.165, 10.13, 10.15}, + {1748966400000LL, 10.145, 10.2, 10.145, 10.195}, + {1748967300000LL, 10.195, 10.21, 10.18, 10.205}, + {1748968200000LL, 10.2, 10.215, 10.185, 10.185}, + {1748969100000LL, 10.185, 10.205, 10.18, 10.2}, + {1748970000000LL, 10.195, 10.2, 10.18, 10.19}, + {1748970900000LL, 10.185, 10.2, 10.17, 10.175}, + {1748971800000LL, 10.175, 10.195, 10.175, 10.19}, + {1748972700000LL, 10.185, 10.21, 10.18, 10.21}, + {1748973600000LL, 10.21, 10.215, 10.18, 10.205}, + {1748974500000LL, 10.21, 10.22, 10.2, 10.215}, + {1748975400000LL, 10.22, 10.22, 10.19, 10.195}, + {1748976300000LL, 10.2, 10.205, 10.19, 10.195}, + {1748977200000LL, 10.195, 10.195, 10.185, 10.19}, + {1748978100000LL, 10.19, 10.2, 10.17, 10.175}, + {1748979000000LL, 10.17, 10.185, 10.155, 10.16}, + {1748979900000LL, 10.165, 10.195, 10.15, 10.19}, + }, + { // signal 2025-12-08 14:45Z, fill 2025-12-08 15:00Z, trail_points 5, trail_offset 1 + {1765205100000LL, 13.035, 13.105, 13.02, 13.1}, + {1765206000000LL, 13.09, 13.13, 13.09, 13.11}, + {1765206900000LL, 13.105, 13.14, 13.1, 13.125}, + {1765207800000LL, 13.125, 13.135, 13.085, 13.085}, + {1765208700000LL, 13.085, 13.1, 13.06, 13.06}, + {1765209600000LL, 13.065, 13.07, 13.041, 13.055}, + {1765210500000LL, 13.06, 13.085, 13.05, 13.07}, + {1765211400000LL, 13.07, 13.09, 13.07, 13.08}, + {1765212300000LL, 13.085, 13.095, 13.075, 13.09}, + {1765213200000LL, 13.085, 13.12, 13.07, 13.11}, + {1765214100000LL, 13.1, 13.11, 13.085, 13.105}, + {1765215000000LL, 13.11, 13.14, 13.105, 13.135}, + {1765215900000LL, 13.13, 13.15, 13.12, 13.14}, + {1765216800000LL, 13.14, 13.14, 13.13, 13.14}, + {1765217700000LL, 13.14, 13.145, 13.12, 13.125}, + {1765218600000LL, 13.12, 13.16, 13.12, 13.16}, + {1765219500000LL, 13.16, 13.16, 13.125, 13.13}, + {1765220400000LL, 13.13, 13.135, 13.115, 13.125}, + {1765221300000LL, 13.13, 13.13, 13.12, 13.13}, + {1765222200000LL, 13.13, 13.15, 13.125, 13.125}, + {1765223100000LL, 13.125, 13.125, 13.1, 13.11}, + {1765224000000LL, 13.11, 13.14, 13.11, 13.135}, + {1765224900000LL, 13.135, 13.15, 13.13, 13.13}, + {1765225800000LL, 13.135, 13.155, 13.13, 13.15}, + {1765226700000LL, 13.145, 13.15, 13.13, 13.145}, + {1765290600000LL, 13.13, 13.22, 13.13, 13.17}, + {1765291500000LL, 13.16, 13.21, 13.135, 13.19}, + {1765292400000LL, 13.2, 13.2, 13.11, 13.125}, + }, + { // signal 2025-12-15 19:45Z, fill 2025-12-15 20:00Z, trail_points 2, trail_offset 1 + {1765827900000LL, 13.625, 13.685, 13.625, 13.685}, + {1765828800000LL, 13.68, 13.71, 13.675, 13.675}, + {1765829700000LL, 13.675, 13.69, 13.665, 13.685}, + {1765830600000LL, 13.685, 13.69, 13.655, 13.66}, + {1765831500000LL, 13.655, 13.69, 13.64, 13.64}, + {1765895400000LL, 13.72, 13.99, 13.71, 13.9}, + {1765896300000LL, 13.9, 13.94, 13.85, 13.855}, + {1765897200000LL, 13.855, 13.865, 13.731, 13.741}, + {1765898100000LL, 13.741, 13.75, 13.575, 13.635}, + {1765899000000LL, 13.63, 13.7, 13.615, 13.7}, + {1765899900000LL, 13.7, 13.75, 13.7, 13.725}, + {1765900800000LL, 13.725, 13.725, 13.65, 13.69}, + {1765901700000LL, 13.69, 13.7, 13.66, 13.66}, + {1765902600000LL, 13.665, 13.69, 13.65, 13.67}, + {1765903500000LL, 13.665, 13.67, 13.61, 13.61}, + {1765904400000LL, 13.61, 13.64, 13.61, 13.62}, + {1765905300000LL, 13.625, 13.65, 13.62, 13.63}, + {1765906200000LL, 13.63, 13.65, 13.61, 13.615}, + {1765907100000LL, 13.62, 13.62, 13.59, 13.6}, + {1765908000000LL, 13.6, 13.63, 13.6, 13.61}, + {1765908900000LL, 13.61, 13.655, 13.61, 13.635}, + {1765909800000LL, 13.64, 13.665, 13.62, 13.655}, + {1765910700000LL, 13.66, 13.67, 13.62, 13.64}, + {1765911600000LL, 13.63, 13.635, 13.6, 13.605}, + {1765912500000LL, 13.609, 13.62, 13.6, 13.62}, + {1765913400000LL, 13.62, 13.64, 13.62, 13.64}, + {1765914300000LL, 13.635, 13.655, 13.62, 13.625}, + {1765915200000LL, 13.62, 13.64, 13.62, 13.625}, + }, + { // signal 2026-02-09 17:15Z, fill 2026-02-09 17:30Z, trail_points 1, trail_offset 1 + {1770657300000LL, 13.675, 13.685, 13.65, 13.67}, + {1770658200000LL, 13.67, 13.72, 13.665, 13.68}, + {1770659100000LL, 13.675, 13.71, 13.67, 13.7}, + {1770660000000LL, 13.7, 13.715, 13.7, 13.7}, + {1770660900000LL, 13.7, 13.705, 13.655, 13.67}, + {1770661800000LL, 13.67, 13.7, 13.67, 13.7}, + {1770662700000LL, 13.7, 13.7, 13.655, 13.665}, + {1770663600000LL, 13.66, 13.67, 13.645, 13.655}, + {1770664500000LL, 13.66, 13.675, 13.65, 13.665}, + {1770665400000LL, 13.665, 13.67, 13.645, 13.66}, + {1770666300000LL, 13.66, 13.665, 13.65, 13.65}, + {1770667200000LL, 13.66, 13.68, 13.65, 13.655}, + {1770668100000LL, 13.65, 13.655, 13.63, 13.635}, + {1770669000000LL, 13.635, 13.65, 13.63, 13.645}, + {1770669900000LL, 13.645, 13.645, 13.585, 13.595}, + {1770733800000LL, 13.635, 13.7, 13.615, 13.63}, + {1770734700000LL, 13.63, 13.67, 13.615, 13.665}, + {1770735600000LL, 13.66, 13.71, 13.66, 13.685}, + {1770736500000LL, 13.69, 13.72, 13.68, 13.695}, + {1770737400000LL, 13.7, 13.72, 13.685, 13.715}, + {1770738300000LL, 13.715, 13.725, 13.7, 13.705}, + {1770739200000LL, 13.705, 13.73, 13.695, 13.725}, + {1770740100000LL, 13.725, 13.735, 13.705, 13.715}, + {1770741000000LL, 13.715, 13.73, 13.7, 13.72}, + {1770741900000LL, 13.725, 13.73, 13.715, 13.725}, + {1770742800000LL, 13.725, 13.73, 13.7, 13.715}, + {1770743700000LL, 13.72, 13.73, 13.68, 13.695}, + {1770744600000LL, 13.695, 13.7, 13.665, 13.67}, + }, +}; +// H3-S: 5 events, 28 bars each +constexpr double kH3ShortPoints[] = {4.0, 12.0, 2.0, 3.0, 3.0}; +constexpr double kH3ShortOffsets[] = {2.0, 1.0, 3.0, 1.0, 3.0}; +constexpr FeedBar kH3Short[5][28] = { + { // signal 2025-04-30 15:00Z, fill 2025-04-30 15:15Z, trail_points 4, trail_offset 2 + {1746025200000LL, 9.935, 9.97, 9.935, 9.955}, + {1746026100000LL, 9.95, 9.96, 9.92, 9.94}, + {1746027000000LL, 9.935, 9.955, 9.92, 9.945}, + {1746027900000LL, 9.945, 9.955, 9.915, 9.93}, + {1746028800000LL, 9.94, 9.95, 9.925, 9.925}, + {1746029700000LL, 9.93, 9.94, 9.9, 9.905}, + {1746030600000LL, 9.9, 9.92, 9.89, 9.905}, + {1746031500000LL, 9.91, 9.93, 9.9, 9.92}, + {1746032400000LL, 9.915, 9.93, 9.865, 9.89}, + {1746033300000LL, 9.885, 9.93, 9.885, 9.93}, + {1746034200000LL, 9.925, 9.95, 9.925, 9.945}, + {1746035100000LL, 9.94, 9.95, 9.92, 9.935}, + {1746036000000LL, 9.935, 9.955, 9.9, 9.935}, + {1746036900000LL, 9.93, 9.95, 9.92, 9.93}, + {1746037800000LL, 9.93, 9.945, 9.905, 9.915}, + {1746038700000LL, 9.915, 9.94, 9.91, 9.915}, + {1746039600000LL, 9.92, 9.93, 9.91, 9.93}, + {1746040500000LL, 9.93, 9.94, 9.91, 9.91}, + {1746041400000LL, 9.915, 9.945, 9.91, 9.945}, + {1746042300000LL, 9.945, 10.04, 9.945, 9.99}, + {1746106200000LL, 10.15, 10.23, 10.025, 10.07}, + {1746107100000LL, 10.07, 10.12, 10.05, 10.1}, + {1746108000000LL, 10.095, 10.245, 10.095, 10.225}, + {1746108900000LL, 10.225, 10.245, 10.22, 10.24}, + {1746109800000LL, 10.235, 10.255, 10.195, 10.245}, + {1746110700000LL, 10.245, 10.3, 10.245, 10.285}, + {1746111600000LL, 10.28, 10.315, 10.26, 10.31}, + {1746112500000LL, 10.31, 10.32, 10.27, 10.28}, + }, + { // signal 2025-05-27 17:45Z, fill 2025-05-27 18:00Z, trail_points 12, trail_offset 1 + {1748367900000LL, 10.265, 10.28, 10.25, 10.28}, + {1748368800000LL, 10.28, 10.285, 10.26, 10.28}, + {1748369700000LL, 10.28, 10.28, 10.27, 10.28}, + {1748370600000LL, 10.28, 10.3, 10.275, 10.3}, + {1748371500000LL, 10.295, 10.31, 10.28, 10.28}, + {1748372400000LL, 10.285, 10.305, 10.28, 10.29}, + {1748373300000LL, 10.285, 10.31, 10.285, 10.295}, + {1748374200000LL, 10.295, 10.31, 10.29, 10.305}, + {1748375100000LL, 10.305, 10.315, 10.295, 10.31}, + {1748439000000LL, 10.32, 10.33, 10.22, 10.225}, + {1748439900000LL, 10.23, 10.265, 10.205, 10.235}, + {1748440800000LL, 10.235, 10.26, 10.21, 10.24}, + {1748441700000LL, 10.235, 10.285, 10.225, 10.275}, + {1748442600000LL, 10.275, 10.29, 10.245, 10.245}, + {1748443500000LL, 10.245, 10.25, 10.21, 10.23}, + {1748444400000LL, 10.235, 10.24, 10.22, 10.22}, + {1748445300000LL, 10.22, 10.23, 10.175, 10.175}, + {1748446200000LL, 10.175, 10.18, 10.165, 10.17}, + {1748447100000LL, 10.165, 10.18, 10.155, 10.155}, + {1748448000000LL, 10.16, 10.18, 10.15, 10.175}, + {1748448900000LL, 10.18, 10.185, 10.165, 10.185}, + {1748449800000LL, 10.18, 10.2, 10.18, 10.2}, + {1748450700000LL, 10.195, 10.21, 10.195, 10.205}, + {1748451600000LL, 10.205, 10.24, 10.205, 10.24}, + {1748452500000LL, 10.235, 10.25, 10.225, 10.23}, + {1748453400000LL, 10.23, 10.23, 10.22, 10.23}, + {1748454300000LL, 10.23, 10.235, 10.225, 10.23}, + {1748455200000LL, 10.23, 10.25, 10.23, 10.245}, + }, + { // signal 2025-12-15 19:45Z, fill 2025-12-15 20:00Z, trail_points 2, trail_offset 3 + {1765827900000LL, 13.625, 13.685, 13.625, 13.685}, + {1765828800000LL, 13.68, 13.71, 13.675, 13.675}, + {1765829700000LL, 13.675, 13.69, 13.665, 13.685}, + {1765830600000LL, 13.685, 13.69, 13.655, 13.66}, + {1765831500000LL, 13.655, 13.69, 13.64, 13.64}, + {1765895400000LL, 13.72, 13.99, 13.71, 13.9}, + {1765896300000LL, 13.9, 13.94, 13.85, 13.855}, + {1765897200000LL, 13.855, 13.865, 13.731, 13.741}, + {1765898100000LL, 13.741, 13.75, 13.575, 13.635}, + {1765899000000LL, 13.63, 13.7, 13.615, 13.7}, + {1765899900000LL, 13.7, 13.75, 13.7, 13.725}, + {1765900800000LL, 13.725, 13.725, 13.65, 13.69}, + {1765901700000LL, 13.69, 13.7, 13.66, 13.66}, + {1765902600000LL, 13.665, 13.69, 13.65, 13.67}, + {1765903500000LL, 13.665, 13.67, 13.61, 13.61}, + {1765904400000LL, 13.61, 13.64, 13.61, 13.62}, + {1765905300000LL, 13.625, 13.65, 13.62, 13.63}, + {1765906200000LL, 13.63, 13.65, 13.61, 13.615}, + {1765907100000LL, 13.62, 13.62, 13.59, 13.6}, + {1765908000000LL, 13.6, 13.63, 13.6, 13.61}, + {1765908900000LL, 13.61, 13.655, 13.61, 13.635}, + {1765909800000LL, 13.64, 13.665, 13.62, 13.655}, + {1765910700000LL, 13.66, 13.67, 13.62, 13.64}, + {1765911600000LL, 13.63, 13.635, 13.6, 13.605}, + {1765912500000LL, 13.609, 13.62, 13.6, 13.62}, + {1765913400000LL, 13.62, 13.64, 13.62, 13.64}, + {1765914300000LL, 13.635, 13.655, 13.62, 13.625}, + {1765915200000LL, 13.62, 13.64, 13.62, 13.625}, + }, + { // signal 2026-01-16 18:15Z, fill 2026-01-16 18:30Z, trail_points 3, trail_offset 1 + {1768587300000LL, 13.69, 13.705, 13.69, 13.69}, + {1768588200000LL, 13.69, 13.695, 13.665, 13.665}, + {1768589100000LL, 13.665, 13.67, 13.655, 13.66}, + {1768590000000LL, 13.66, 13.79, 13.65, 13.66}, + {1768590900000LL, 13.66, 13.67, 13.64, 13.64}, + {1768591800000LL, 13.64, 13.66, 13.63, 13.655}, + {1768592700000LL, 13.655, 13.67, 13.64, 13.64}, + {1768593600000LL, 13.645, 13.665, 13.625, 13.66}, + {1768594500000LL, 13.665, 13.665, 13.64, 13.645}, + {1768595400000LL, 13.645, 13.645, 13.62, 13.62}, + {1768596300000LL, 13.62, 13.63, 13.605, 13.605}, + {1768919400000LL, 13.47, 13.605, 13.41, 13.58}, + {1768920300000LL, 13.57, 13.57, 13.5, 13.51}, + {1768921200000LL, 13.515, 13.53, 13.48, 13.5}, + {1768922100000LL, 13.5, 13.505, 13.465, 13.495}, + {1768923000000LL, 13.5, 13.5, 13.445, 13.46}, + {1768923900000LL, 13.455, 13.46, 13.43, 13.44}, + {1768924800000LL, 13.43, 13.43, 13.385, 13.405}, + {1768925700000LL, 13.41, 13.42, 13.385, 13.385}, + {1768926600000LL, 13.38, 13.38, 13.32, 13.34}, + {1768927500000LL, 13.34, 13.37, 13.32, 13.37}, + {1768928400000LL, 13.37, 13.415, 13.35, 13.41}, + {1768929300000LL, 13.41, 13.419, 13.355, 13.36}, + {1768930200000LL, 13.355, 13.355, 13.32, 13.325}, + {1768931100000LL, 13.32, 13.325, 13.29, 13.305}, + {1768932000000LL, 13.3, 13.31, 13.28, 13.295}, + {1768932900000LL, 13.295, 13.31, 13.285, 13.295}, + {1768933800000LL, 13.3, 13.315, 13.275, 13.285}, + }, + { // signal 2026-02-10 15:00Z, fill 2026-02-10 15:15Z, trail_points 3, trail_offset 3 + {1770735600000LL, 13.66, 13.71, 13.66, 13.685}, + {1770736500000LL, 13.69, 13.72, 13.68, 13.695}, + {1770737400000LL, 13.7, 13.72, 13.685, 13.715}, + {1770738300000LL, 13.715, 13.725, 13.7, 13.705}, + {1770739200000LL, 13.705, 13.73, 13.695, 13.725}, + {1770740100000LL, 13.725, 13.735, 13.705, 13.715}, + {1770741000000LL, 13.715, 13.73, 13.7, 13.72}, + {1770741900000LL, 13.725, 13.73, 13.715, 13.725}, + {1770742800000LL, 13.725, 13.73, 13.7, 13.715}, + {1770743700000LL, 13.72, 13.73, 13.68, 13.695}, + {1770744600000LL, 13.695, 13.7, 13.665, 13.67}, + {1770745500000LL, 13.67, 13.69, 13.67, 13.685}, + {1770746400000LL, 13.685, 13.685, 13.66, 13.675}, + {1770747300000LL, 13.675, 13.685, 13.655, 13.655}, + {1770748200000LL, 13.655, 13.695, 13.65, 13.695}, + {1770749100000LL, 13.695, 13.695, 13.66, 13.67}, + {1770750000000LL, 13.67, 13.675, 13.665, 13.67}, + {1770750900000LL, 13.67, 13.7, 13.665, 13.665}, + {1770751800000LL, 13.665, 13.69, 13.66, 13.66}, + {1770752700000LL, 13.665, 13.67, 13.64, 13.66}, + {1770753600000LL, 13.655, 13.675, 13.65, 13.655}, + {1770754500000LL, 13.655, 13.66, 13.635, 13.645}, + {1770755400000LL, 13.645, 13.655, 13.61, 13.615}, + {1770756300000LL, 13.61, 13.62, 13.575, 13.59}, + {1770820200000LL, 13.61, 13.88, 13.52, 13.705}, + {1770821100000LL, 13.69, 13.75, 13.55, 13.555}, + {1770822000000LL, 13.56, 13.635, 13.55, 13.56}, + {1770822900000LL, 13.57, 13.66, 13.525, 13.65}, + }, +}; + +// The LIMIT-entry variant: per event the signal bar, where +// strategy.entry(limit=X) and the trailing strategy.exit are issued together, +// then 29 more bars (the limit fills mid-bar later). trail_offset is 1 throughout. +constexpr double kH4LimitLongPoints[] = {8.0, 5.0, 6.0, 7.0, 4.0, 8.0}; +constexpr double kH4LimitLongLevels[] = {10.51, 11.41, 11.58, 12.45, 12.90, 13.98}; +constexpr FeedBar kH4LimitLong[6][30] = { + { // signal 2025-05-21 17:15Z, limit 10.51 fills 2025-05-21 17:30Z, trail_points 8, trail_offset 1 + {1747847700000LL, 10.61, 10.615, 10.515, 10.525}, + {1747848600000LL, 10.52, 10.585, 10.5, 10.56}, + {1747849500000LL, 10.56, 10.56, 10.53, 10.54}, + {1747850400000LL, 10.54, 10.54, 10.51, 10.515}, + {1747851300000LL, 10.515, 10.52, 10.485, 10.505}, + {1747852200000LL, 10.505, 10.54, 10.505, 10.535}, + {1747853100000LL, 10.535, 10.54, 10.495, 10.5}, + {1747854000000LL, 10.5, 10.51, 10.475, 10.5}, + {1747854900000LL, 10.495, 10.5, 10.45, 10.475}, + {1747855800000LL, 10.48, 10.51, 10.475, 10.505}, + {1747856700000LL, 10.51, 10.52, 10.47, 10.475}, + {1747920600000LL, 10.48, 10.48, 10.355, 10.39}, + {1747921500000LL, 10.39, 10.39, 10.345, 10.365}, + {1747922400000LL, 10.365, 10.44, 10.365, 10.37}, + {1747923300000LL, 10.37, 10.39, 10.355, 10.365}, + {1747924200000LL, 10.37, 10.39, 10.365, 10.38}, + {1747925100000LL, 10.375, 10.415, 10.375, 10.415}, + {1747926000000LL, 10.415, 10.45, 10.41, 10.435}, + {1747926900000LL, 10.435, 10.445, 10.415, 10.415}, + {1747927800000LL, 10.415, 10.45, 10.415, 10.445}, + {1747928700000LL, 10.445, 10.45, 10.425, 10.44}, + {1747929600000LL, 10.44, 10.46, 10.43, 10.455}, + {1747930500000LL, 10.46, 10.47, 10.435, 10.435}, + {1747931400000LL, 10.435, 10.44, 10.41, 10.415}, + {1747932300000LL, 10.41, 10.425, 10.41, 10.425}, + {1747933200000LL, 10.425, 10.44, 10.42, 10.435}, + {1747934100000LL, 10.435, 10.465, 10.435, 10.46}, + {1747935000000LL, 10.455, 10.48, 10.44, 10.48}, + {1747935900000LL, 10.47, 10.485, 10.465, 10.465}, + {1747936800000LL, 10.465, 10.485, 10.46, 10.485}, + }, + { // signal 2025-07-23 15:45Z, limit 11.41 fills 2025-07-23 16:00Z, trail_points 5, trail_offset 1 + {1753285500000LL, 11.35, 11.435, 11.35, 11.43}, + {1753286400000LL, 11.42, 11.455, 11.4, 11.435}, + {1753287300000LL, 11.44, 11.455, 11.425, 11.445}, + {1753288200000LL, 11.445, 11.445, 11.42, 11.425}, + {1753289100000LL, 11.425, 11.45, 11.425, 11.445}, + {1753290000000LL, 11.445, 11.445, 11.4, 11.405}, + {1753290900000LL, 11.405, 11.405, 11.36, 11.39}, + {1753291800000LL, 11.395, 11.42, 11.39, 11.395}, + {1753292700000LL, 11.395, 11.41, 11.34, 11.365}, + {1753293600000LL, 11.365, 11.37, 11.335, 11.355}, + {1753294500000LL, 11.355, 11.385, 11.355, 11.375}, + {1753295400000LL, 11.375, 11.395, 11.37, 11.395}, + {1753296300000LL, 11.395, 11.395, 11.37, 11.375}, + {1753297200000LL, 11.375, 11.39, 11.375, 11.375}, + {1753298100000LL, 11.375, 11.38, 11.37, 11.375}, + {1753299000000LL, 11.375, 11.38, 11.37, 11.375}, + {1753299900000LL, 11.375, 11.41, 11.37, 11.385}, + {1753363800000LL, 11.29, 11.3, 11.23, 11.28}, + {1753364700000LL, 11.29, 11.37, 11.29, 11.365}, + {1753365600000LL, 11.37, 11.425, 11.36, 11.36}, + {1753366500000LL, 11.365, 11.38, 11.35, 11.38}, + {1753367400000LL, 11.375, 11.415, 11.37, 11.395}, + {1753368300000LL, 11.39, 11.4, 11.37, 11.4}, + {1753369200000LL, 11.395, 11.4, 11.36, 11.385}, + {1753370100000LL, 11.385, 11.385, 11.325, 11.36}, + {1753371000000LL, 11.355, 11.375, 11.335, 11.335}, + {1753371900000LL, 11.335, 11.35, 11.33, 11.345}, + {1753372800000LL, 11.345, 11.345, 11.32, 11.32}, + {1753373700000LL, 11.32, 11.325, 11.3, 11.305}, + {1753374600000LL, 11.3, 11.31, 11.275, 11.295}, + }, + { // signal 2025-09-09 13:30Z, limit 11.58 fills 2025-09-09 13:45Z, trail_points 6, trail_offset 1 + {1757424600000LL, 11.68, 11.68, 11.6, 11.61}, + {1757425500000LL, 11.6, 11.635, 11.57, 11.575}, + {1757426400000LL, 11.58, 11.6, 11.555, 11.58}, + {1757427300000LL, 11.585, 11.585, 11.535, 11.55}, + {1757428200000LL, 11.55, 11.56, 11.53, 11.54}, + {1757429100000LL, 11.54, 11.55, 11.52, 11.535}, + {1757430000000LL, 11.54, 11.555, 11.51, 11.525}, + {1757430900000LL, 11.52, 11.545, 11.5, 11.535}, + {1757431800000LL, 11.54, 11.55, 11.52, 11.52}, + {1757432700000LL, 11.515, 11.52, 11.51, 11.515}, + {1757433600000LL, 11.515, 11.53, 11.515, 11.52}, + {1757434500000LL, 11.515, 11.525, 11.49, 11.525}, + {1757435400000LL, 11.525, 11.53, 11.5, 11.505}, + {1757436300000LL, 11.505, 11.51, 11.495, 11.5}, + {1757437200000LL, 11.495, 11.5, 11.47, 11.47}, + {1757438100000LL, 11.47, 11.48, 11.45, 11.475}, + {1757439000000LL, 11.47, 11.475, 11.46, 11.465}, + {1757439900000LL, 11.465, 11.475, 11.45, 11.465}, + {1757440800000LL, 11.465, 11.49, 11.46, 11.48}, + {1757441700000LL, 11.48, 11.51, 11.48, 11.505}, + {1757442600000LL, 11.505, 11.525, 11.49, 11.51}, + {1757443500000LL, 11.51, 11.52, 11.485, 11.485}, + {1757444400000LL, 11.49, 11.51, 11.49, 11.495}, + {1757445300000LL, 11.495, 11.5, 11.495, 11.495}, + {1757446200000LL, 11.495, 11.505, 11.485, 11.495}, + {1757447100000LL, 11.495, 11.51, 11.485, 11.495}, + {1757511000000LL, 11.49, 11.49, 11.37, 11.375}, + {1757511900000LL, 11.375, 11.4, 11.345, 11.36}, + {1757512800000LL, 11.36, 11.4, 11.36, 11.385}, + {1757513700000LL, 11.39, 11.41, 11.38, 11.385}, + }, + { // signal 2025-10-22 14:00Z, limit 12.45 fills 2025-10-22 14:15Z, trail_points 7, trail_offset 1 + {1761141600000LL, 12.515, 12.515, 12.45, 12.475}, + {1761142500000LL, 12.475, 12.515, 12.44, 12.465}, + {1761143400000LL, 12.465, 12.47, 12.41, 12.43}, + {1761144300000LL, 12.43, 12.5, 12.42, 12.49}, + {1761145200000LL, 12.48, 12.48, 12.42, 12.43}, + {1761146100000LL, 12.425, 12.43, 12.37, 12.39}, + {1761147000000LL, 12.39, 12.425, 12.375, 12.39}, + {1761147900000LL, 12.39, 12.415, 12.375, 12.395}, + {1761148800000LL, 12.4, 12.415, 12.385, 12.4}, + {1761149700000LL, 12.39, 12.41, 12.35, 12.37}, + {1761150600000LL, 12.37, 12.38, 12.29, 12.375}, + {1761151500000LL, 12.375, 12.485, 12.375, 12.42}, + {1761152400000LL, 12.415, 12.42, 12.375, 12.385}, + {1761153300000LL, 12.385, 12.395, 12.365, 12.38}, + {1761154200000LL, 12.38, 12.39, 12.335, 12.345}, + {1761155100000LL, 12.35, 12.385, 12.345, 12.38}, + {1761156000000LL, 12.385, 12.4, 12.325, 12.34}, + {1761156900000LL, 12.335, 12.39, 12.315, 12.375}, + {1761157800000LL, 12.38, 12.395, 12.355, 12.365}, + {1761158700000LL, 12.365, 12.405, 12.35, 12.4}, + {1761159600000LL, 12.4, 12.42, 12.375, 12.415}, + {1761160500000LL, 12.42, 12.47, 12.42, 12.465}, + {1761161400000LL, 12.465, 12.48, 12.455, 12.46}, + {1761162300000LL, 12.46, 12.465, 12.43, 12.455}, + {1761226200000LL, 12.48, 12.5, 12.31, 12.395}, + {1761227100000LL, 12.395, 12.445, 12.32, 12.32}, + {1761228000000LL, 12.315, 12.34, 12.28, 12.29}, + {1761228900000LL, 12.29, 12.33, 12.28, 12.315}, + {1761229800000LL, 12.315, 12.36, 12.285, 12.33}, + {1761230700000LL, 12.33, 12.415, 12.32, 12.405}, + }, + { // signal 2025-11-20 14:30Z, limit 12.90 fills 2025-11-20 14:45Z, trail_points 4, trail_offset 1 + {1763649000000LL, 12.91, 13.01, 12.855, 12.905}, + {1763649900000LL, 12.91, 12.935, 12.885, 12.91}, + {1763650800000LL, 12.905, 12.91, 12.85, 12.86}, + {1763651700000LL, 12.865, 12.88, 12.69, 12.795}, + {1763652600000LL, 12.795, 12.805, 12.66, 12.715}, + {1763653500000LL, 12.72, 12.73, 12.625, 12.65}, + {1763654400000LL, 12.655, 12.705, 12.59, 12.59}, + {1763655300000LL, 12.59, 12.59, 12.485, 12.485}, + {1763656200000LL, 12.49, 12.51, 12.445, 12.455}, + {1763657100000LL, 12.455, 12.575, 12.45, 12.465}, + {1763658000000LL, 12.46, 12.505, 12.43, 12.45}, + {1763658900000LL, 12.44, 12.45, 12.4, 12.43}, + {1763659800000LL, 12.43, 12.46, 12.395, 12.395}, + {1763660700000LL, 12.395, 12.445, 12.385, 12.415}, + {1763661600000LL, 12.415, 12.555, 12.415, 12.555}, + {1763662500000LL, 12.56, 12.57, 12.515, 12.55}, + {1763663400000LL, 12.55, 12.575, 12.515, 12.53}, + {1763664300000LL, 12.53, 12.55, 12.5, 12.52}, + {1763665200000LL, 12.52, 12.53, 12.49, 12.51}, + {1763666100000LL, 12.505, 12.53, 12.485, 12.52}, + {1763667000000LL, 12.52, 12.52, 12.48, 12.52}, + {1763667900000LL, 12.52, 12.555, 12.49, 12.555}, + {1763668800000LL, 12.55, 12.555, 12.515, 12.52}, + {1763669700000LL, 12.52, 12.545, 12.46, 12.46}, + {1763670600000LL, 12.465, 12.48, 12.42, 12.43}, + {1763671500000LL, 12.44, 12.44, 12.39, 12.405}, + {1763735400000LL, 12.51, 12.59, 12.5, 12.52}, + {1763736300000LL, 12.52, 12.555, 12.495, 12.535}, + {1763737200000LL, 12.54, 12.64, 12.54, 12.585}, + {1763738100000LL, 12.59, 12.625, 12.575, 12.615}, + }, + { // signal 2026-01-29 19:30Z, limit 13.98 fills 2026-01-29 20:00Z, trail_points 8, trail_offset 1 + {1769715000000LL, 14.075, 14.075, 14.01, 14.04}, + {1769715900000LL, 14.045, 14.06, 14.005, 14.005}, + {1769716800000LL, 14.01, 14.055, 13.965, 13.965}, + {1769717700000LL, 13.96, 13.98, 13.92, 13.92}, + {1769718600000LL, 13.925, 13.97, 13.925, 13.95}, + {1769719500000LL, 13.95, 14.01, 13.945, 13.995}, + {1769783400000LL, 13.93, 13.975, 13.875, 13.89}, + {1769784300000LL, 13.89, 13.915, 13.87, 13.89}, + {1769785200000LL, 13.885, 13.91, 13.875, 13.9}, + {1769786100000LL, 13.895, 13.9, 13.83, 13.84}, + {1769787000000LL, 13.835, 13.885, 13.82, 13.885}, + {1769787900000LL, 13.885, 13.92, 13.885, 13.89}, + {1769788800000LL, 13.895, 13.92, 13.89, 13.905}, + {1769789700000LL, 13.91, 13.925, 13.88, 13.91}, + {1769790600000LL, 13.905, 13.93, 13.88, 13.925}, + {1769791500000LL, 13.925, 13.93, 13.905, 13.91}, + {1769792400000LL, 13.91, 13.93, 13.86, 13.875}, + {1769793300000LL, 13.88, 13.94, 13.86, 13.925}, + {1769794200000LL, 13.925, 13.93, 13.89, 13.915}, + {1769795100000LL, 13.91, 13.925, 13.87, 13.87}, + {1769796000000LL, 13.87, 13.9, 13.865, 13.87}, + {1769796900000LL, 13.87, 13.875, 13.82, 13.875}, + {1769797800000LL, 13.87, 13.87, 13.83, 13.85}, + {1769798700000LL, 13.845, 13.89, 13.84, 13.87}, + {1769799600000LL, 13.87, 13.9, 13.865, 13.865}, + {1769800500000LL, 13.865, 13.89, 13.82, 13.825}, + {1769801400000LL, 13.825, 13.86, 13.79, 13.845}, + {1769802300000LL, 13.845, 13.865, 13.83, 13.83}, + {1769803200000LL, 13.83, 13.84, 13.81, 13.83}, + {1769804100000LL, 13.83, 13.87, 13.815, 13.855}, + }, +}; +constexpr double kH4LimitShortPoints[] = {7.0, 4.0, 3.0, 1.0, 8.0, 9.0}; +constexpr double kH4LimitShortLevels[] = {9.89, 9.94, 10.19, 10.17, 9.97, 13.13}; +constexpr FeedBar kH4LimitShort[6][30] = { + { // signal 2025-04-23 14:45Z, limit 9.89 fills 2025-04-23 15:00Z, trail_points 7, trail_offset 1 + {1745419500000LL, 9.89, 9.895, 9.86, 9.88}, + {1745420400000LL, 9.875, 9.9, 9.825, 9.9}, + {1745421300000LL, 9.895, 9.895, 9.81, 9.83}, + {1745422200000LL, 9.835, 9.87, 9.77, 9.835}, + {1745423100000LL, 9.84, 9.845, 9.75, 9.785}, + {1745424000000LL, 9.785, 9.8, 9.755, 9.77}, + {1745424900000LL, 9.775, 9.775, 9.715, 9.765}, + {1745425800000LL, 9.77, 9.835, 9.765, 9.83}, + {1745426700000LL, 9.82, 9.835, 9.8, 9.835}, + {1745427600000LL, 9.83, 9.87, 9.77, 9.865}, + {1745428500000LL, 9.865, 9.885, 9.855, 9.86}, + {1745429400000LL, 9.86, 9.865, 9.81, 9.84}, + {1745430300000LL, 9.845, 9.89, 9.835, 9.88}, + {1745431200000LL, 9.875, 9.9, 9.85, 9.88}, + {1745432100000LL, 9.88, 9.895, 9.83, 9.835}, + {1745433000000LL, 9.84, 9.87, 9.83, 9.83}, + {1745433900000LL, 9.835, 9.84, 9.81, 9.81}, + {1745434800000LL, 9.815, 9.835, 9.8, 9.81}, + {1745435700000LL, 9.81, 9.81, 9.75, 9.755}, + {1745436600000LL, 9.76, 9.785, 9.755, 9.77}, + {1745437500000LL, 9.77, 9.795, 9.755, 9.765}, + {1745501400000LL, 9.88, 9.97, 9.815, 9.955}, + {1745502300000LL, 9.955, 10.015, 9.94, 10}, + {1745503200000LL, 9.99, 10.06, 9.985, 10}, + {1745504100000LL, 9.995, 10.035, 9.97, 10}, + {1745505000000LL, 9.99, 10.01, 9.97, 10.01}, + {1745505900000LL, 10.015, 10.015, 9.97, 10.005}, + {1745506800000LL, 10, 10.015, 9.975, 9.995}, + {1745507700000LL, 9.995, 10.015, 9.99, 9.995}, + {1745508600000LL, 9.995, 10.02, 9.99, 10}, + }, + { // signal 2025-04-30 18:00Z, limit 9.94 fills 2025-04-30 18:15Z, trail_points 4, trail_offset 1 + {1746036000000LL, 9.935, 9.955, 9.9, 9.935}, + {1746036900000LL, 9.93, 9.95, 9.92, 9.93}, + {1746037800000LL, 9.93, 9.945, 9.905, 9.915}, + {1746038700000LL, 9.915, 9.94, 9.91, 9.915}, + {1746039600000LL, 9.92, 9.93, 9.91, 9.93}, + {1746040500000LL, 9.93, 9.94, 9.91, 9.91}, + {1746041400000LL, 9.915, 9.945, 9.91, 9.945}, + {1746042300000LL, 9.945, 10.04, 9.945, 9.99}, + {1746106200000LL, 10.15, 10.23, 10.025, 10.07}, + {1746107100000LL, 10.07, 10.12, 10.05, 10.1}, + {1746108000000LL, 10.095, 10.245, 10.095, 10.225}, + {1746108900000LL, 10.225, 10.245, 10.22, 10.24}, + {1746109800000LL, 10.235, 10.255, 10.195, 10.245}, + {1746110700000LL, 10.245, 10.3, 10.245, 10.285}, + {1746111600000LL, 10.28, 10.315, 10.26, 10.31}, + {1746112500000LL, 10.31, 10.32, 10.27, 10.28}, + {1746113400000LL, 10.28, 10.29, 10.25, 10.25}, + {1746114300000LL, 10.25, 10.25, 10.21, 10.22}, + {1746115200000LL, 10.215, 10.235, 10.195, 10.225}, + {1746116100000LL, 10.22, 10.225, 10.19, 10.195}, + {1746117000000LL, 10.195, 10.205, 10.17, 10.2}, + {1746117900000LL, 10.195, 10.21, 10.18, 10.18}, + {1746118800000LL, 10.18, 10.22, 10.18, 10.215}, + {1746119700000LL, 10.215, 10.225, 10.21, 10.22}, + {1746120600000LL, 10.22, 10.23, 10.21, 10.22}, + {1746121500000LL, 10.215, 10.245, 10.21, 10.21}, + {1746122400000LL, 10.215, 10.25, 10.21, 10.245}, + {1746123300000LL, 10.245, 10.25, 10.215, 10.23}, + {1746124200000LL, 10.23, 10.245, 10.23, 10.235}, + {1746125100000LL, 10.24, 10.26, 10.235, 10.245}, + }, + { // signal 2025-05-05 15:15Z, limit 10.19 fills 2025-05-05 15:45Z, trail_points 3, trail_offset 1 + {1746458100000LL, 10.18, 10.195, 10.17, 10.17}, + {1746459000000LL, 10.175, 10.175, 10.14, 10.145}, + {1746459900000LL, 10.15, 10.2, 10.145, 10.185}, + {1746460800000LL, 10.185, 10.19, 10.17, 10.17}, + {1746461700000LL, 10.175, 10.185, 10.17, 10.18}, + {1746462600000LL, 10.18, 10.19, 10.165, 10.185}, + {1746463500000LL, 10.185, 10.195, 10.175, 10.185}, + {1746464400000LL, 10.18, 10.185, 10.16, 10.165}, + {1746465300000LL, 10.16, 10.16, 10.13, 10.135}, + {1746466200000LL, 10.135, 10.15, 10.12, 10.14}, + {1746467100000LL, 10.14, 10.155, 10.13, 10.13}, + {1746468000000LL, 10.13, 10.17, 10.13, 10.17}, + {1746468900000LL, 10.17, 10.175, 10.16, 10.165}, + {1746469800000LL, 10.165, 10.17, 10.15, 10.155}, + {1746470700000LL, 10.155, 10.16, 10.13, 10.13}, + {1746471600000LL, 10.13, 10.14, 10.12, 10.13}, + {1746472500000LL, 10.125, 10.13, 10.11, 10.125}, + {1746473400000LL, 10.125, 10.13, 10.105, 10.105}, + {1746474300000LL, 10.105, 10.2, 10.1, 10.2}, + {1746538200000LL, 10.09, 10.41, 10.055, 10.34}, + {1746539100000LL, 10.34, 10.38, 10.215, 10.285}, + {1746540000000LL, 10.295, 10.57, 10.29, 10.555}, + {1746540900000LL, 10.555, 10.6, 10.52, 10.6}, + {1746541800000LL, 10.595, 10.62, 10.56, 10.62}, + {1746542700000LL, 10.62, 10.62, 10.525, 10.575}, + {1746543600000LL, 10.575, 10.595, 10.5, 10.5}, + {1746544500000LL, 10.505, 10.56, 10.505, 10.56}, + {1746545400000LL, 10.55, 10.585, 10.535, 10.54}, + {1746546300000LL, 10.535, 10.535, 10.46, 10.465}, + {1746547200000LL, 10.47, 10.505, 10.445, 10.5}, + }, + { // signal 2025-05-28 15:45Z, limit 10.17 fills 2025-05-28 16:00Z, trail_points 1, trail_offset 1 + {1748447100000LL, 10.165, 10.18, 10.155, 10.155}, + {1748448000000LL, 10.16, 10.18, 10.15, 10.175}, + {1748448900000LL, 10.18, 10.185, 10.165, 10.185}, + {1748449800000LL, 10.18, 10.2, 10.18, 10.2}, + {1748450700000LL, 10.195, 10.21, 10.195, 10.205}, + {1748451600000LL, 10.205, 10.24, 10.205, 10.24}, + {1748452500000LL, 10.235, 10.25, 10.225, 10.23}, + {1748453400000LL, 10.23, 10.23, 10.22, 10.23}, + {1748454300000LL, 10.23, 10.235, 10.225, 10.23}, + {1748455200000LL, 10.23, 10.25, 10.23, 10.245}, + {1748456100000LL, 10.245, 10.25, 10.225, 10.235}, + {1748457000000LL, 10.235, 10.24, 10.22, 10.225}, + {1748457900000LL, 10.22, 10.225, 10.18, 10.215}, + {1748458800000LL, 10.215, 10.225, 10.2, 10.21}, + {1748459700000LL, 10.21, 10.215, 10.19, 10.195}, + {1748460600000LL, 10.19, 10.21, 10.185, 10.205}, + {1748461500000LL, 10.205, 10.205, 10.17, 10.18}, + {1748525400000LL, 10.215, 10.22, 10.17, 10.19}, + {1748526300000LL, 10.185, 10.255, 10.185, 10.25}, + {1748527200000LL, 10.255, 10.275, 10.22, 10.235}, + {1748528100000LL, 10.23, 10.23, 10.2, 10.21}, + {1748529000000LL, 10.21, 10.215, 10.17, 10.205}, + {1748529900000LL, 10.205, 10.22, 10.195, 10.215}, + {1748530800000LL, 10.215, 10.22, 10.2, 10.205}, + {1748531700000LL, 10.2, 10.235, 10.2, 10.235}, + {1748532600000LL, 10.23, 10.23, 10.165, 10.185}, + {1748533500000LL, 10.18, 10.195, 10.12, 10.125}, + {1748534400000LL, 10.125, 10.16, 10.12, 10.15}, + {1748535300000LL, 10.16, 10.185, 10.155, 10.18}, + {1748536200000LL, 10.18, 10.19, 10.175, 10.19}, + }, + { // signal 2025-06-02 19:30Z, limit 9.97 fills 2025-06-02 19:45Z, trail_points 8, trail_offset 1 + {1748892600000LL, 9.965, 9.99, 9.96, 9.96}, + {1748893500000LL, 9.96, 9.98, 9.945, 9.98}, + {1748957400000LL, 9.965, 9.97, 9.895, 9.955}, + {1748958300000LL, 9.96, 10.01, 9.95, 9.97}, + {1748959200000LL, 9.975, 10.02, 9.97, 10.02}, + {1748960100000LL, 10.015, 10.065, 10.005, 10.065}, + {1748961000000LL, 10.065, 10.11, 10.045, 10.105}, + {1748961900000LL, 10.105, 10.16, 10.105, 10.125}, + {1748962800000LL, 10.13, 10.155, 10.125, 10.145}, + {1748963700000LL, 10.14, 10.155, 10.13, 10.14}, + {1748964600000LL, 10.14, 10.16, 10.135, 10.145}, + {1748965500000LL, 10.14, 10.165, 10.13, 10.15}, + {1748966400000LL, 10.145, 10.2, 10.145, 10.195}, + {1748967300000LL, 10.195, 10.21, 10.18, 10.205}, + {1748968200000LL, 10.2, 10.215, 10.185, 10.185}, + {1748969100000LL, 10.185, 10.205, 10.18, 10.2}, + {1748970000000LL, 10.195, 10.2, 10.18, 10.19}, + {1748970900000LL, 10.185, 10.2, 10.17, 10.175}, + {1748971800000LL, 10.175, 10.195, 10.175, 10.19}, + {1748972700000LL, 10.185, 10.21, 10.18, 10.21}, + {1748973600000LL, 10.21, 10.215, 10.18, 10.205}, + {1748974500000LL, 10.21, 10.22, 10.2, 10.215}, + {1748975400000LL, 10.22, 10.22, 10.19, 10.195}, + {1748976300000LL, 10.2, 10.205, 10.19, 10.195}, + {1748977200000LL, 10.195, 10.195, 10.185, 10.19}, + {1748978100000LL, 10.19, 10.2, 10.17, 10.175}, + {1748979000000LL, 10.17, 10.185, 10.155, 10.16}, + {1748979900000LL, 10.165, 10.195, 10.15, 10.19}, + {1749043800000LL, 10.19, 10.34, 10.19, 10.305}, + {1749044700000LL, 10.305, 10.36, 10.29, 10.29}, + }, + { // signal 2025-12-08 15:00Z, limit 13.13 fills 2025-12-08 15:15Z, trail_points 9, trail_offset 1 + {1765206000000LL, 13.09, 13.13, 13.09, 13.11}, + {1765206900000LL, 13.105, 13.14, 13.1, 13.125}, + {1765207800000LL, 13.125, 13.135, 13.085, 13.085}, + {1765208700000LL, 13.085, 13.1, 13.06, 13.06}, + {1765209600000LL, 13.065, 13.07, 13.041, 13.055}, + {1765210500000LL, 13.06, 13.085, 13.05, 13.07}, + {1765211400000LL, 13.07, 13.09, 13.07, 13.08}, + {1765212300000LL, 13.085, 13.095, 13.075, 13.09}, + {1765213200000LL, 13.085, 13.12, 13.07, 13.11}, + {1765214100000LL, 13.1, 13.11, 13.085, 13.105}, + {1765215000000LL, 13.11, 13.14, 13.105, 13.135}, + {1765215900000LL, 13.13, 13.15, 13.12, 13.14}, + {1765216800000LL, 13.14, 13.14, 13.13, 13.14}, + {1765217700000LL, 13.14, 13.145, 13.12, 13.125}, + {1765218600000LL, 13.12, 13.16, 13.12, 13.16}, + {1765219500000LL, 13.16, 13.16, 13.125, 13.13}, + {1765220400000LL, 13.13, 13.135, 13.115, 13.125}, + {1765221300000LL, 13.13, 13.13, 13.12, 13.13}, + {1765222200000LL, 13.13, 13.15, 13.125, 13.125}, + {1765223100000LL, 13.125, 13.125, 13.1, 13.11}, + {1765224000000LL, 13.11, 13.14, 13.11, 13.135}, + {1765224900000LL, 13.135, 13.15, 13.13, 13.13}, + {1765225800000LL, 13.135, 13.155, 13.13, 13.15}, + {1765226700000LL, 13.145, 13.15, 13.13, 13.145}, + {1765290600000LL, 13.13, 13.22, 13.13, 13.17}, + {1765291500000LL, 13.16, 13.21, 13.135, 13.19}, + {1765292400000LL, 13.2, 13.2, 13.11, 13.125}, + {1765293300000LL, 13.125, 13.13, 13.09, 13.095}, + {1765294200000LL, 13.095, 13.17, 13.095, 13.155}, + {1765295100000LL, 13.16, 13.17, 13.15, 13.16}, + }, +}; diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/meta.json b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/meta.json new file mode 100644 index 00000000..e7b24695 --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-MEASURE H3 NYSE:F long pending-entry trail seed", + "slug": "hm-orders-h3-f-long-pending-seed", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:45:53.287Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10" + }, + "pine_sha256": "2072eee2b09dd4d56c7b3da20dd729fc7d589dd795d8ed1f5998ca368b843ce3", + "tv_trades_sha256": "95b1e8309f3b2f93645f36d68b3b479517b7c303d0b7330b50622364a0787252", + "tv_trades_rows": 16 +} diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/metrics.json b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/metrics.json new file mode 100644 index 00000000..05b4e71e --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-orders-h3-f-long-pending-seed", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10", + "deepBacktesting": true, + "trades": 8, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "2072eee2b09dd4d56c7b3da20dd729fc7d589dd795d8ed1f5998ca368b843ce3", + "tvTradesCsvHash": "95b1e8309f3b2f93645f36d68b3b479517b7c303d0b7330b50622364a0787252", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74389", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1775779200000 + }, + "returnedRange": { + "from": 1743514200000, + "to": 1775763900000 + }, + "rangeProof": "covered", + "rawFrameSha256": "963ce61372763736a8248e2d70590484e969d0d981327b29dd22c700a3222500", + "decodedReportSha256": "c2931bdec056e1ed2663a12e47eb89a4e9b2fe717d589dddc5b5ed266b83c238", + "closedTrades": 8, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:45:53.286Z" +} diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/strategy.pine b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/strategy.pine new file mode 100644 index 00000000..d712569a --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/strategy.pine @@ -0,0 +1,68 @@ +// H-MEASURE H3 NYSE:F long pending-entry trail seed (H-MEASURE research, group orders). +// Row 3 (E14 anchored-leg seed): where does the running best of a trail whose +// strategy.exit was issued while its entry was PENDING start? The arm bar's raw +// extreme stops inside the activation's tick cell; a best that starts AT the +// activation (lane E14, filled entries) exits on the event bar, a best that +// starts at the raw arm print exits later. +// NYSE:F 15m (sub-cent prints). Each entry is a MARKET strategy.entry issued on +// its signal bar together with strategy.exit(trail_points, trail_offset); the +// entry fills at the next bar's open. Events (UTC signal bar -> fill bar): +// 2025-05-27 13:45 -> 2025-05-27 14:00 trail_points 6 trail_offset 5 +// 2025-06-26 16:30 -> 2025-06-26 16:45 trail_points 1 trail_offset 3 +// 2025-07-25 14:00 -> 2025-07-25 14:15 trail_points 3 trail_offset 5 +// 2025-08-25 14:30 -> 2025-08-25 14:45 trail_points 4 trail_offset 3 +// 2025-09-11 18:00 -> 2025-09-11 18:15 trail_points 1 trail_offset 5 +// 2025-10-02 16:45 -> 2025-10-02 17:00 trail_points 4 trail_offset 5 +// 2025-12-29 14:30 -> 2025-12-29 14:45 trail_points 4 trail_offset 5 +// 2026-03-25 15:45 -> 2026-03-25 16:00 trail_points 2 trail_offset 5 +//@version=6 +strategy("H-MEASURE H3 NYSE:F long pending-entry trail seed", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +var float pts = na +var float off = na +bool go = false +if strategy.position_size == 0 + if time == timestamp("UTC", 2025, 5, 27, 13, 45) + pts := 6 + off := 5 + go := true + else if time == timestamp("UTC", 2025, 6, 26, 16, 30) + pts := 1 + off := 3 + go := true + else if time == timestamp("UTC", 2025, 7, 25, 14, 0) + pts := 3 + off := 5 + go := true + else if time == timestamp("UTC", 2025, 8, 25, 14, 30) + pts := 4 + off := 3 + go := true + else if time == timestamp("UTC", 2025, 9, 11, 18, 0) + pts := 1 + off := 5 + go := true + else if time == timestamp("UTC", 2025, 10, 2, 16, 45) + pts := 4 + off := 5 + go := true + else if time == timestamp("UTC", 2025, 12, 29, 14, 30) + pts := 4 + off := 5 + go := true + else if time == timestamp("UTC", 2026, 3, 25, 15, 45) + pts := 2 + off := 5 + go := true + +// The entry and its trailing exit are issued on the same (signal) bar: the exit +// is placed while the entry is still pending. +if go + strategy.entry("L", strategy.long) + strategy.exit("LX", "L", trail_points=pts, trail_offset=off) + +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 25 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/tv_trades.csv b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/tv_trades.csv new file mode 100644 index 00000000..1a4d878d --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-long-pending-seed/tv_trades.csv @@ -0,0 +1,17 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-05-27 22:15,LX,10.29,100,1028,1,0.10,0,6,0.58,-1,-0.10,1,0.00,1 +1,Entry long,2025-05-27 22:00,L,10.28,100,1028,1,0.10,0,6,0.58,-1,-0.10,1,0.00,1 +2,Exit long,2025-06-27 01:00,LX,10.56,100,1058,-2,-0.19,0,1,0.09,-2,-0.19,-1,0.00,1 +2,Entry long,2025-06-27 00:45,L,10.58,100,1058,-2,-0.19,0,1,0.09,-2,-0.19,-1,0.00,1 +3,Exit long,2025-07-25 22:30,LX,11.25,100,1127,-2,-0.18,0,3,0.27,-2,-0.18,-3,0.00,1 +3,Entry long,2025-07-25 22:15,L,11.27,100,1127,-2,-0.18,0,3,0.27,-2,-0.18,-3,0.00,1 +4,Exit long,2025-08-25 23:00,LX,11.72,100,1171,1,0.09,0,4,0.34,0,0.00,-2,0.00,1 +4,Entry long,2025-08-25 22:45,L,11.71,100,1171,1,0.09,0,4,0.34,0,0.00,-2,0.00,1 +5,Exit long,2025-09-12 02:30,LX,11.69,100,1173,-4,-0.34,0,1,0.09,-4,-0.34,-6,-0.01,1 +5,Entry long,2025-09-12 02:15,L,11.73,100,1173,-4,-0.34,0,1,0.09,-4,-0.34,-6,-0.01,1 +6,Exit long,2025-10-03 01:15,LX,12.17,100,1218,-1,-0.08,0,4,0.33,-1,-0.08,-7,-0.01,1 +6,Entry long,2025-10-03 01:00,L,12.18,100,1218,-1,-0.08,0,4,0.33,-1,-0.08,-7,-0.01,1 +7,Exit long,2025-12-29 23:00,LX,13.25,100,1326,-1,-0.08,0,4,0.30,-1,-0.08,-8,-0.01,1 +7,Entry long,2025-12-29 22:45,L,13.26,100,1326,-1,-0.08,0,4,0.30,-1,-0.08,-8,-0.01,1 +8,Exit long,2026-03-26 00:15,LX,11.69,100,1172,-3,-0.26,0,2,0.17,-3,-0.26,-11,-0.01,1 +8,Entry long,2026-03-26 00:00,L,11.72,100,1172,-3,-0.26,0,2,0.17,-3,-0.26,-11,-0.01,1 \ No newline at end of file diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/meta.json b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/meta.json new file mode 100644 index 00000000..a5d23cda --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-MEASURE H3 NYSE:F short pending-entry trail seed", + "slug": "hm-orders-h3-f-short-pending-seed", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:45:57.480Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10" + }, + "pine_sha256": "b817d8c1f4217fd4241ee52f55f7f8b9d48d7a46b40a2087ebdd6f376aaf2f01", + "tv_trades_sha256": "03654a5b3f98b45be25f6c9be212943a43ad4e1683c5d4b7d126567ba226966a", + "tv_trades_rows": 10 +} diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/metrics.json b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/metrics.json new file mode 100644 index 00000000..48f049ea --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-orders-h3-f-short-pending-seed", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10", + "deepBacktesting": true, + "trades": 5, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "b817d8c1f4217fd4241ee52f55f7f8b9d48d7a46b40a2087ebdd6f376aaf2f01", + "tvTradesCsvHash": "03654a5b3f98b45be25f6c9be212943a43ad4e1683c5d4b7d126567ba226966a", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74390", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1775779200000 + }, + "returnedRange": { + "from": 1743514200000, + "to": 1775763900000 + }, + "rangeProof": "covered", + "rawFrameSha256": "771d8d97fe172244e596eed34345a2010438fb34312e526912c62343f5435318", + "decodedReportSha256": "637c06f902b328c3e00404b1db1baee99fad3c97007b567d223e3bce4e24207c", + "closedTrades": 5, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:45:57.479Z" +} diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/strategy.pine b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/strategy.pine new file mode 100644 index 00000000..e4df1b52 --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/strategy.pine @@ -0,0 +1,53 @@ +// H-MEASURE H3 NYSE:F short pending-entry trail seed (H-MEASURE research, group orders). +// Row 3 (E14 anchored-leg seed): where does the running best of a trail whose +// strategy.exit was issued while its entry was PENDING start? The arm bar's raw +// extreme stops inside the activation's tick cell; a best that starts AT the +// activation (lane E14, filled entries) exits on the event bar, a best that +// starts at the raw arm print exits later. +// NYSE:F 15m (sub-cent prints). Each entry is a MARKET strategy.entry issued on +// its signal bar together with strategy.exit(trail_points, trail_offset); the +// entry fills at the next bar's open. Events (UTC signal bar -> fill bar): +// 2025-04-30 15:00 -> 2025-04-30 15:15 trail_points 4 trail_offset 2 +// 2025-05-27 17:45 -> 2025-05-27 18:00 trail_points 12 trail_offset 1 +// 2025-12-15 19:45 -> 2025-12-15 20:00 trail_points 2 trail_offset 3 +// 2026-01-16 18:15 -> 2026-01-16 18:30 trail_points 3 trail_offset 1 +// 2026-02-10 15:00 -> 2026-02-10 15:15 trail_points 3 trail_offset 3 +//@version=6 +strategy("H-MEASURE H3 NYSE:F short pending-entry trail seed", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +var float pts = na +var float off = na +bool go = false +if strategy.position_size == 0 + if time == timestamp("UTC", 2025, 4, 30, 15, 0) + pts := 4 + off := 2 + go := true + else if time == timestamp("UTC", 2025, 5, 27, 17, 45) + pts := 12 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 12, 15, 19, 45) + pts := 2 + off := 3 + go := true + else if time == timestamp("UTC", 2026, 1, 16, 18, 15) + pts := 3 + off := 1 + go := true + else if time == timestamp("UTC", 2026, 2, 10, 15, 0) + pts := 3 + off := 3 + go := true + +// The entry and its trailing exit are issued on the same (signal) bar: the exit +// is placed while the entry is still pending. +if go + strategy.entry("S", strategy.short) + strategy.exit("SX", "S", trail_points=pts, trail_offset=off) + +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 25 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/tv_trades.csv b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/tv_trades.csv new file mode 100644 index 00000000..a0605d1e --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h3-f-short-pending-seed/tv_trades.csv @@ -0,0 +1,11 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-04-30 23:45,SX,9.93,100,995,2,0.20,0,4,0.40,-1,-0.10,2,0.00,2 +1,Entry short,2025-04-30 23:15,S,9.95,100,995,2,0.20,0,4,0.40,-1,-0.10,2,0.00,2 +2,Exit short,2025-05-28 23:30,SX,10.17,100,1028,11,1.07,0,12,1.17,-5,-0.49,13,0.01,16 +2,Entry short,2025-05-28 02:00,S,10.28,100,1028,11,1.07,0,12,1.17,-5,-0.49,13,0.01,16 +3,Exit short,2025-12-16 04:15,SX,13.69,100,1368,-1,-0.07,0,2,0.15,-3,-0.22,12,0.01,1 +3,Entry short,2025-12-16 04:00,S,13.68,100,1368,-1,-0.07,0,2,0.15,-3,-0.22,12,0.01,1 +4,Exit short,2026-01-17 02:45,SX,13.67,100,1369,2,0.15,0,3,0.22,-1,-0.07,14,0.01,1 +4,Entry short,2026-01-17 02:30,S,13.69,100,1369,2,0.15,0,3,0.22,-1,-0.07,14,0.01,1 +5,Exit short,2026-02-11 01:45,SX,13.69,100,1369,0,0.00,0,3,0.22,-5,-0.37,14,0.01,10 +5,Entry short,2026-02-10 23:15,S,13.69,100,1369,0,0.00,0,3,0.22,-5,-0.37,14,0.01,10 \ No newline at end of file diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/meta.json b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/meta.json new file mode 100644 index 00000000..a50a5c6c --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-MEASURE H4 NYSE:F long pending-entry trail arm", + "slug": "hm-orders-h4-f-long-pending-arm", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:45:30.944Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10" + }, + "pine_sha256": "2a96c3cbec176825d2c3c2b0fa7e031877fc603b741ec882f903d0ad8c9f1193", + "tv_trades_sha256": "620a55e297f6bd8b40a4a9ed04195f456bbeb24e2f02468a0fe29fa2ced4b9a3", + "tv_trades_rows": 16 +} diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/metrics.json b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/metrics.json new file mode 100644 index 00000000..83bbccdd --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-orders-h4-f-long-pending-arm", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10", + "deepBacktesting": true, + "trades": 8, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "2a96c3cbec176825d2c3c2b0fa7e031877fc603b741ec882f903d0ad8c9f1193", + "tvTradesCsvHash": "620a55e297f6bd8b40a4a9ed04195f456bbeb24e2f02468a0fe29fa2ced4b9a3", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74387", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1775779200000 + }, + "returnedRange": { + "from": 1743514200000, + "to": 1775763900000 + }, + "rangeProof": "covered", + "rawFrameSha256": "5d0757812fc0943e915396bc5d66c09243a4a018934228ae67d3a5b94b26e584", + "decodedReportSha256": "fef7caf0ce94447f9920d0aa59d798b3478c79a3cd30268a5b3d83ed0a567788", + "closedTrades": 8, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:45:30.942Z" +} diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/strategy.pine b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/strategy.pine new file mode 100644 index 00000000..9b8a590c --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/strategy.pine @@ -0,0 +1,68 @@ +// H-MEASURE H4 NYSE:F long pending-entry trail arm (H-MEASURE research, group orders). +// Row 4 (E5 pending-entry arm): does a trail whose strategy.exit was issued while its +// entry was PENDING arm on the tick-QUANTIZED path, as lane E5 measured for a filled +// entry? On every event the arm bar's raw extreme stops inside the activation's +// tick cell; a quantized arm exits on that bar at activation -/+ 1 tick, a raw arm +// later. +// NYSE:F 15m (sub-cent prints). Each entry is a MARKET strategy.entry issued on +// its signal bar together with strategy.exit(trail_points, trail_offset); the +// entry fills at the next bar's open. Events (UTC signal bar -> fill bar): +// 2025-04-28 14:00 -> 2025-04-28 14:15 trail_points 2 trail_offset 1 +// 2025-05-02 18:45 -> 2025-05-02 19:00 trail_points 2 trail_offset 1 +// 2025-05-21 17:00 -> 2025-05-21 17:15 trail_points 1 trail_offset 1 +// 2025-06-02 13:30 -> 2025-06-02 13:45 trail_points 1 trail_offset 1 +// 2025-06-04 14:15 -> 2025-06-04 14:30 trail_points 2 trail_offset 1 +// 2025-09-26 14:00 -> 2025-09-26 14:15 trail_points 10 trail_offset 1 +// 2025-11-18 18:45 -> 2025-11-18 19:00 trail_points 2 trail_offset 1 +// 2026-01-20 17:00 -> 2026-01-20 17:15 trail_points 1 trail_offset 1 +//@version=6 +strategy("H-MEASURE H4 NYSE:F long pending-entry trail arm", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +var float pts = na +var float off = na +bool go = false +if strategy.position_size == 0 + if time == timestamp("UTC", 2025, 4, 28, 14, 0) + pts := 2 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 5, 2, 18, 45) + pts := 2 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 5, 21, 17, 0) + pts := 1 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 6, 2, 13, 30) + pts := 1 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 6, 4, 14, 15) + pts := 2 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 9, 26, 14, 0) + pts := 10 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 11, 18, 18, 45) + pts := 2 + off := 1 + go := true + else if time == timestamp("UTC", 2026, 1, 20, 17, 0) + pts := 1 + off := 1 + go := true + +// The entry and its trailing exit are issued on the same (signal) bar: the exit +// is placed while the entry is still pending. +if go + strategy.entry("L", strategy.long) + strategy.exit("LX", "L", trail_points=pts, trail_offset=off) + +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 25 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/tv_trades.csv b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/tv_trades.csv new file mode 100644 index 00000000..26cc8d05 --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-arm/tv_trades.csv @@ -0,0 +1,17 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-28 22:15,LX,10.17,100,1016,1,0.10,0,2,0.20,0,0.00,1,0.00,0 +1,Entry long,2025-04-28 22:15,L,10.16,100,1016,1,0.10,0,2,0.20,0,0.00,1,0.00,0 +2,Exit long,2025-05-03 03:00,LX,10.3,100,1029,1,0.10,0,2,0.19,0,0.00,2,0.00,0 +2,Entry long,2025-05-03 03:00,L,10.29,100,1029,1,0.10,0,2,0.19,0,0.00,2,0.00,0 +3,Exit long,2025-05-22 01:15,LX,10.61,100,1061,0,0.00,0,1,0.09,0,0.00,2,0.00,0 +3,Entry long,2025-05-22 01:15,L,10.61,100,1061,0,0.00,0,1,0.09,0,0.00,2,0.00,0 +4,Exit long,2025-06-02 21:45,LX,10.12,100,1012,0,0.00,0,1,0.10,0,0.00,2,0.00,0 +4,Entry long,2025-06-02 21:45,L,10.12,100,1012,0,0.00,0,1,0.10,0,0.00,2,0.00,0 +5,Exit long,2025-06-04 22:30,LX,10.32,100,1031,1,0.10,0,2,0.19,0,0.00,3,0.00,0 +5,Entry long,2025-06-04 22:30,L,10.31,100,1031,1,0.10,0,2,0.19,0,0.00,3,0.00,0 +6,Exit long,2025-09-26 22:15,LX,11.89,100,1180,9,0.76,0,10,0.85,0,0.00,12,0.01,0 +6,Entry long,2025-09-26 22:15,L,11.8,100,1180,9,0.76,0,10,0.85,0,0.00,12,0.01,0 +7,Exit long,2025-11-19 03:30,LX,13.04,100,1303,1,0.08,0,2,0.15,-2,-0.15,13,0.01,2 +7,Entry long,2025-11-19 03:00,L,13.03,100,1303,1,0.08,0,2,0.15,-2,-0.15,13,0.01,2 +8,Exit long,2026-01-21 01:15,LX,13.41,100,1341,0,0.00,0,1,0.07,0,0.00,13,0.01,0 +8,Entry long,2026-01-21 01:15,L,13.41,100,1341,0,0.00,0,1,0.07,0,0.00,13,0.01,0 \ No newline at end of file diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/meta.json b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/meta.json new file mode 100644 index 00000000..e730bce2 --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-MEASURE H4 NYSE:F long pending limit-entry trail arm", + "slug": "hm-orders-h4-f-long-pending-limit-arm", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:18:11.817Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10" + }, + "pine_sha256": "c429493c6186e3f16afdc5fe207011e8b27637c996a702260f87994fa3cd1e8a", + "tv_trades_sha256": "886d8cc2e2caa9134073cbb0910121aaa844824e66e9170e6615ab7128cb6c13", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/metrics.json b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/metrics.json new file mode 100644 index 00000000..d7eb3c5d --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-orders-h4-f-long-pending-limit-arm", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "c429493c6186e3f16afdc5fe207011e8b27637c996a702260f87994fa3cd1e8a", + "tvTradesCsvHash": "886d8cc2e2caa9134073cbb0910121aaa844824e66e9170e6615ab7128cb6c13", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74421", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1775779200000 + }, + "returnedRange": { + "from": 1743514200000, + "to": 1775763900000 + }, + "rangeProof": "covered", + "rawFrameSha256": "21d88ca609e919f8ccabb0c15294fccb0fe05dbadf45e9367b1412b90465dbfe", + "decodedReportSha256": "f07fe0e67ecc306a94af7cacc94d203b3c90d3dbbca9c391764e44a268b46201", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:18:11.817Z" +} diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/strategy.pine b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/strategy.pine new file mode 100644 index 00000000..95bbc490 --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/strategy.pine @@ -0,0 +1,55 @@ +// H-MEASURE H4 NYSE:F long pending limit-entry trail arm (H-MEASURE research, group orders). +// Row 4 (E5 pending-entry arm), LIMIT-entry variant: strategy.entry(limit=X) and +// strategy.exit(trail_points=n, trail_offset=1) are issued together on the signal bar; +// the limit fills later MID-BAR (the path crosses X; open and earlier extremes stay a +// tick away, so no gap and no quantization ambiguity), and the trail may arm on the +// rest of the fill bar. The arm bar's raw extreme stops inside the activation's tick +// cell: a quantized arm exits there at activation -/+ 1 tick, a raw arm later. (UTC): +// 2025-05-21 17:15 limit 10.51 (fills 2025-05-21 17:30) trail_points 8 +// 2025-07-23 15:45 limit 11.41 (fills 2025-07-23 16:00) trail_points 5 +// 2025-09-09 13:30 limit 11.58 (fills 2025-09-09 13:45) trail_points 6 +// 2025-10-22 14:00 limit 12.45 (fills 2025-10-22 14:15) trail_points 7 +// 2025-11-20 14:30 limit 12.90 (fills 2025-11-20 14:45) trail_points 4 +// 2026-01-29 19:30 limit 13.98 (fills 2026-01-29 20:00) trail_points 8 +//@version=6 +strategy("H-MEASURE H4 NYSE:F long pending limit-entry trail arm", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +var float pts = na +var float lvl = na +bool go = false +if strategy.position_size == 0 + if time == timestamp("UTC", 2025, 5, 21, 17, 15) + pts := 8 + lvl := 10.51 + go := true + else if time == timestamp("UTC", 2025, 7, 23, 15, 45) + pts := 5 + lvl := 11.41 + go := true + else if time == timestamp("UTC", 2025, 9, 9, 13, 30) + pts := 6 + lvl := 11.58 + go := true + else if time == timestamp("UTC", 2025, 10, 22, 14, 0) + pts := 7 + lvl := 12.45 + go := true + else if time == timestamp("UTC", 2025, 11, 20, 14, 30) + pts := 4 + lvl := 12.90 + go := true + else if time == timestamp("UTC", 2026, 1, 29, 19, 30) + pts := 8 + lvl := 13.98 + go := true + +// The limit entry and its trailing exit are issued on the same (signal) bar. +if go + strategy.entry("L", strategy.long, limit=lvl) + strategy.exit("LX", "L", trail_points=pts, trail_offset=1) + +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 25 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/tv_trades.csv b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/tv_trades.csv new file mode 100644 index 00000000..1d24d9e2 --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-long-pending-limit-arm/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-05-22 01:30,LX,10.58,100,1051,7,0.67,0,8,0.76,-1,-0.10,7,0.01,0 +1,Entry long,2025-05-22 01:30,L,10.51,100,1051,7,0.67,0,8,0.76,-1,-0.10,7,0.01,0 +2,Exit long,2025-07-24 00:00,LX,11.45,100,1141,4,0.35,0,5,0.44,-1,-0.09,11,0.01,0 +2,Entry long,2025-07-24 00:00,L,11.41,100,1141,4,0.35,0,5,0.44,-1,-0.09,11,0.01,0 +3,Exit long,2025-09-09 21:45,LX,11.63,100,1158,5,0.43,0,6,0.52,-1,-0.09,16,0.02,0 +3,Entry long,2025-09-09 21:45,L,11.58,100,1158,5,0.43,0,6,0.52,-1,-0.09,16,0.02,0 +4,Exit long,2025-10-22 22:15,LX,12.51,100,1245,6,0.48,0,7,0.56,-1,-0.08,22,0.02,0 +4,Entry long,2025-10-22 22:15,L,12.45,100,1245,6,0.48,0,7,0.56,-1,-0.08,22,0.02,0 +5,Exit long,2025-11-20 22:45,LX,12.93,100,1290,3,0.23,0,4,0.31,-1,-0.08,25,0.03,0 +5,Entry long,2025-11-20 22:45,L,12.9,100,1290,3,0.23,0,4,0.31,-1,-0.08,25,0.03,0 +6,Exit long,2026-01-30 04:00,LX,14.05,100,1398,7,0.50,0,8,0.57,-1,-0.07,32,0.03,0 +6,Entry long,2026-01-30 04:00,L,13.98,100,1398,7,0.50,0,8,0.57,-1,-0.07,32,0.03,0 \ No newline at end of file diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/meta.json b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/meta.json new file mode 100644 index 00000000..e1c3a67a --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-MEASURE H4 NYSE:F short pending-entry trail arm", + "slug": "hm-orders-h4-f-short-pending-arm", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:45:49.157Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10" + }, + "pine_sha256": "361ea74dca1894c88b0ca9781c6a4d68dcbe90a825de03e2d010068f1c186d01", + "tv_trades_sha256": "41d1837adb0ebcc096e544de4abbd65eb2b0abf56553257ae2e004380a221521", + "tv_trades_rows": 16 +} diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/metrics.json b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/metrics.json new file mode 100644 index 00000000..53f33c1e --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-orders-h4-f-short-pending-arm", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10", + "deepBacktesting": true, + "trades": 8, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "361ea74dca1894c88b0ca9781c6a4d68dcbe90a825de03e2d010068f1c186d01", + "tvTradesCsvHash": "41d1837adb0ebcc096e544de4abbd65eb2b0abf56553257ae2e004380a221521", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74388", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1775779200000 + }, + "returnedRange": { + "from": 1743514200000, + "to": 1775763900000 + }, + "rangeProof": "covered", + "rawFrameSha256": "30acc75f2dc55806d7088d34af0db2016749618a788bc350d7629674d6d1ffbc", + "decodedReportSha256": "4f422e810dca047b42e48a8119a918b7cff10f6ba3b35021f091927dff606fcd", + "closedTrades": 8, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:45:49.156Z" +} diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/strategy.pine b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/strategy.pine new file mode 100644 index 00000000..ff96ad06 --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/strategy.pine @@ -0,0 +1,68 @@ +// H-MEASURE H4 NYSE:F short pending-entry trail arm (H-MEASURE research, group orders). +// Row 4 (E5 pending-entry arm): does a trail whose strategy.exit was issued while its +// entry was PENDING arm on the tick-QUANTIZED path, as lane E5 measured for a filled +// entry? On every event the arm bar's raw extreme stops inside the activation's +// tick cell; a quantized arm exits on that bar at activation -/+ 1 tick, a raw arm +// later. +// NYSE:F 15m (sub-cent prints). Each entry is a MARKET strategy.entry issued on +// its signal bar together with strategy.exit(trail_points, trail_offset); the +// entry fills at the next bar's open. Events (UTC signal bar -> fill bar): +// 2025-04-24 15:30 -> 2025-04-24 15:45 trail_points 1 trail_offset 1 +// 2025-04-30 15:00 -> 2025-04-30 15:15 trail_points 4 trail_offset 1 +// 2025-05-05 15:30 -> 2025-05-05 15:45 trail_points 1 trail_offset 1 +// 2025-05-28 16:00 -> 2025-05-28 16:15 trail_points 2 trail_offset 1 +// 2025-06-02 19:30 -> 2025-06-02 19:45 trail_points 7 trail_offset 1 +// 2025-12-08 14:45 -> 2025-12-08 15:00 trail_points 5 trail_offset 1 +// 2025-12-15 19:45 -> 2025-12-15 20:00 trail_points 2 trail_offset 1 +// 2026-02-09 17:15 -> 2026-02-09 17:30 trail_points 1 trail_offset 1 +//@version=6 +strategy("H-MEASURE H4 NYSE:F short pending-entry trail arm", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +var float pts = na +var float off = na +bool go = false +if strategy.position_size == 0 + if time == timestamp("UTC", 2025, 4, 24, 15, 30) + pts := 1 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 4, 30, 15, 0) + pts := 4 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 5, 5, 15, 30) + pts := 1 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 5, 28, 16, 0) + pts := 2 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 6, 2, 19, 30) + pts := 7 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 12, 8, 14, 45) + pts := 5 + off := 1 + go := true + else if time == timestamp("UTC", 2025, 12, 15, 19, 45) + pts := 2 + off := 1 + go := true + else if time == timestamp("UTC", 2026, 2, 9, 17, 15) + pts := 1 + off := 1 + go := true + +// The entry and its trailing exit are issued on the same (signal) bar: the exit +// is placed while the entry is still pending. +if go + strategy.entry("S", strategy.short) + strategy.exit("SX", "S", trail_points=pts, trail_offset=off) + +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 25 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/tv_trades.csv b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/tv_trades.csv new file mode 100644 index 00000000..1d326d76 --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-arm/tv_trades.csv @@ -0,0 +1,17 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-04-24 23:45,SX,10,100,1000,0,0.00,0,1,0.10,0,0.00,0,0.00,0 +1,Entry short,2025-04-24 23:45,S,10,100,1000,0,0.00,0,1,0.10,0,0.00,0,0.00,0 +2,Exit short,2025-04-30 23:45,SX,9.92,100,995,3,0.30,0,4,0.40,-1,-0.10,3,0.00,2 +2,Entry short,2025-04-30 23:15,S,9.95,100,995,3,0.30,0,4,0.40,-1,-0.10,3,0.00,2 +3,Exit short,2025-05-05 23:45,SX,10.15,100,1015,0,0.00,0,1,0.10,0,0.00,3,0.00,0 +3,Entry short,2025-05-05 23:45,S,10.15,100,1015,0,0.00,0,1,0.10,0,0.00,3,0.00,0 +4,Exit short,2025-05-29 00:15,SX,10.17,100,1018,1,0.10,0,2,0.20,-1,-0.10,4,0.00,0 +4,Entry short,2025-05-29 00:15,S,10.18,100,1018,1,0.10,0,2,0.20,-1,-0.10,4,0.00,0 +5,Exit short,2025-06-03 21:30,SX,9.9,100,996,6,0.60,0,7,0.70,-2,-0.20,10,0.01,1 +5,Entry short,2025-06-03 03:45,S,9.96,100,996,6,0.60,0,7,0.70,-2,-0.20,10,0.01,1 +6,Exit short,2025-12-09 00:00,SX,13.05,100,1309,4,0.31,0,5,0.38,-5,-0.38,14,0.01,4 +6,Entry short,2025-12-08 23:00,S,13.09,100,1309,4,0.31,0,5,0.38,-5,-0.38,14,0.01,4 +7,Exit short,2025-12-16 04:15,SX,13.67,100,1368,1,0.07,0,2,0.15,-3,-0.22,15,0.01,1 +7,Entry short,2025-12-16 04:00,S,13.68,100,1368,1,0.07,0,2,0.15,-3,-0.22,15,0.01,1 +8,Exit short,2026-02-10 01:30,SX,13.67,100,1367,0,0.00,0,1,0.07,0,0.00,15,0.01,0 +8,Entry short,2026-02-10 01:30,S,13.67,100,1367,0,0.00,0,1,0.07,0,0.00,15,0.01,0 \ No newline at end of file diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/meta.json b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/meta.json new file mode 100644 index 00000000..bb7e166f --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-MEASURE H4 NYSE:F short pending limit-entry trail arm", + "slug": "hm-orders-h4-f-short-pending-limit-arm", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:18:16.551Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10" + }, + "pine_sha256": "a64da4618816d737850592ee4d8601a4ffede3320b7deb1edc8162846bb30c49", + "tv_trades_sha256": "f88f872fe583f0b85a33909f5bc113fbd3e0c529798f5c21f2e359b95501cf2c", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/metrics.json b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/metrics.json new file mode 100644 index 00000000..b816a928 --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-orders-h4-f-short-pending-limit-arm", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "a64da4618816d737850592ee4d8601a4ffede3320b7deb1edc8162846bb30c49", + "tvTradesCsvHash": "f88f872fe583f0b85a33909f5bc113fbd3e0c529798f5c21f2e359b95501cf2c", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74422", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1775779200000 + }, + "returnedRange": { + "from": 1743514200000, + "to": 1775763900000 + }, + "rangeProof": "covered", + "rawFrameSha256": "d40592170cb6f6a70d898de57f8e5ea2dd9f3ae2dadde690f2f6fa969ac094f8", + "decodedReportSha256": "f0ca8cec3e2a0610614e792dcc7b0a0a995bb8f8912c32070da4b769a5eec9c5", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:18:16.550Z" +} diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/strategy.pine b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/strategy.pine new file mode 100644 index 00000000..19acf3b8 --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/strategy.pine @@ -0,0 +1,55 @@ +// H-MEASURE H4 NYSE:F short pending limit-entry trail arm (H-MEASURE research, group orders). +// Row 4 (E5 pending-entry arm), LIMIT-entry variant: strategy.entry(limit=X) and +// strategy.exit(trail_points=n, trail_offset=1) are issued together on the signal bar; +// the limit fills later MID-BAR (the path crosses X; open and earlier extremes stay a +// tick away, so no gap and no quantization ambiguity), and the trail may arm on the +// rest of the fill bar. The arm bar's raw extreme stops inside the activation's tick +// cell: a quantized arm exits there at activation -/+ 1 tick, a raw arm later. (UTC): +// 2025-04-23 14:45 limit 9.89 (fills 2025-04-23 15:00) trail_points 7 +// 2025-04-30 18:00 limit 9.94 (fills 2025-04-30 18:15) trail_points 4 +// 2025-05-05 15:15 limit 10.19 (fills 2025-05-05 15:45) trail_points 3 +// 2025-05-28 15:45 limit 10.17 (fills 2025-05-28 16:00) trail_points 1 +// 2025-06-02 19:30 limit 9.97 (fills 2025-06-02 19:45) trail_points 8 +// 2025-12-08 15:00 limit 13.13 (fills 2025-12-08 15:15) trail_points 9 +//@version=6 +strategy("H-MEASURE H4 NYSE:F short pending limit-entry trail arm", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +var float pts = na +var float lvl = na +bool go = false +if strategy.position_size == 0 + if time == timestamp("UTC", 2025, 4, 23, 14, 45) + pts := 7 + lvl := 9.89 + go := true + else if time == timestamp("UTC", 2025, 4, 30, 18, 0) + pts := 4 + lvl := 9.94 + go := true + else if time == timestamp("UTC", 2025, 5, 5, 15, 15) + pts := 3 + lvl := 10.19 + go := true + else if time == timestamp("UTC", 2025, 5, 28, 15, 45) + pts := 1 + lvl := 10.17 + go := true + else if time == timestamp("UTC", 2025, 6, 2, 19, 30) + pts := 8 + lvl := 9.97 + go := true + else if time == timestamp("UTC", 2025, 12, 8, 15, 0) + pts := 9 + lvl := 13.13 + go := true + +// The limit entry and its trailing exit are issued on the same (signal) bar. +if go + strategy.entry("S", strategy.short, limit=lvl) + strategy.exit("SX", "S", trail_points=pts, trail_offset=1) + +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 25 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/tv_trades.csv b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/tv_trades.csv new file mode 100644 index 00000000..2561ad80 --- /dev/null +++ b/tests/fixtures/pending_entry_trail/hm-orders-h4-f-short-pending-limit-arm/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-04-23 23:00,SX,9.83,100,989,6,0.61,0,7,0.71,-1,-0.10,6,0.01,0 +1,Entry short,2025-04-23 23:00,S,9.89,100,989,6,0.61,0,7,0.71,-1,-0.10,6,0.01,0 +2,Exit short,2025-05-01 02:30,SX,9.91,100,994,3,0.30,0,4,0.40,-1,-0.10,9,0.01,1 +2,Entry short,2025-05-01 02:15,S,9.94,100,994,3,0.30,0,4,0.40,-1,-0.10,9,0.01,1 +3,Exit short,2025-05-06 00:30,SX,10.17,100,1019,2,0.20,0,3,0.29,-1,-0.10,11,0.01,3 +3,Entry short,2025-05-05 23:45,S,10.19,100,1019,2,0.20,0,3,0.29,-1,-0.10,11,0.01,3 +4,Exit short,2025-05-29 00:15,SX,10.17,100,1017,0,0.00,0,1,0.10,-2,-0.20,11,0.01,1 +4,Entry short,2025-05-29 00:00,S,10.17,100,1017,0,0.00,0,1,0.10,-2,-0.20,11,0.01,1 +5,Exit short,2025-06-03 21:30,SX,9.9,100,997,7,0.70,0,8,0.80,-1,-0.10,18,0.02,1 +5,Entry short,2025-06-03 03:45,S,9.97,100,997,7,0.70,0,8,0.80,-1,-0.10,18,0.02,1 +6,Exit short,2025-12-09 00:00,SX,13.05,100,1313,8,0.61,0,9,0.69,-1,-0.08,26,0.03,3 +6,Entry short,2025-12-08 23:15,S,13.13,100,1313,8,0.61,0,9,0.69,-1,-0.08,26,0.03,3 \ No newline at end of file diff --git a/tests/fixtures/pyramiding_count/README.md b/tests/fixtures/pyramiding_count/README.md new file mode 100644 index 00000000..952a4453 --- /dev/null +++ b/tests/fixtures/pyramiding_count/README.md @@ -0,0 +1,53 @@ +# Pyramiding count tapes (H-MEASURE row M13 / G2-15) + +TradingView's own trades for Pine `pyramiding = 2` (fixed 100 shares, orders +filled at the next bar's open), fifteen scenarios on fifteen NYSE:F sessions, +each flat at its start and closed by `strategy.close_all`. +`tests/test_pyramiding_count_differential.cpp` runs every scenario three ways +on the same bars: the Pine adapter (its per-cycle `accepted_in_cycle` count, +`project()` leaving `max_open_lots` unset), a bare `NativeStrategyHost` with +`max_open_lots = 2`, and this tape. + +Each directory is one `lab tv` export (`ws-report-v1`, `rangeProof: covered`, +NYSE:F 15), byte-identical: `strategy.pine`, `tv_trades.csv` (UTC+8), +`meta.json`, `metrics.json`. + +| probe | window | trades | tvTradesSha256 | +|---|---|---|---| +| `hm-orders-m13-f-pyramiding` (P1-P4, P4b, P6, P8-P10) | 2025-05-01 .. 2025-07-31 | 24 | `de780cacf8ee965d787ef715d3af6ddf439370652dea820e71dbad55c4b0909d` | +| `hm-orders-m13-f-pyramiding-rule` (D1, D2, D3, D3b) | 2025-06-02 .. 2025-07-31 | 9 | `44c68d01446717e491eb9389ac5e6db8ef9f501a437bf7640cd6e1b3ebe0cfa4` | +| `hm-orders-m13-f-pyramiding-cap-orders` (D4, D4s) | 2025-07-01 .. 2025-09-30 | 4 | `67cb6805a7b9cb20c8542e1e4c0805dbd539d1f3182dafe4a020a0c01c77a1b5` | + +| scenario | shape | TradingView | adapter | `max_open_lots` | +|---|---|---|---|---| +| P1 | `close(qty)` closes lot 1 whole, re-add | re-add fills | = | = | +| P2 | `close(qty)` inside a lot, re-add | refused | = | = | +| P3 | close one id of two, third id | fills | = | = | +| P4 | resting far limit L2, then a market add L3 | L3 fills | refuses L3 | = | +| P4b | the resting limit fills after the add | L, L3 AND L2 (3 lots) | L, L2 | L, L3 | +| P6 | three market entries on one flat bar | 2 fill | 3 fill | = | +| P8 | `exit(qty)` from B stops out lot A (FIFO), third id | fills | refuses | = | +| P9 | `exit(qty)` from A stops out lot A, third id | fills | = | = | +| P10 | `exit` from B, default quantity, lots A+B | closes 100 (A) | closes 200 | = (Reduce 100) | +| D1 | one lot; same bar: far limit, then market add | add fills | refuses | = | +| D2 | flat; same bar: far limit, two markets | both fill | = | = | +| D3 | at the cap; same bar: `close("A")` then `entry("C")` | C fills | refuses | = | +| D3b | at the cap; same bar: `entry("C")` then `close("A")` | refused | = | = | +| D4 | at the cap: a limit entry placed, reached later | never fills | = | = (refused at the fill) | +| D4s | at the cap: a stop entry placed, reached later | never fills | = | = (refused at the fill) | + +What the fifteen imply about TradingView's rule: an entry is checked once, +when it is first processed at the next open (orders in placement order), +against the trades then open in its direction; a resting limit or stop entry +that passed that check is never checked again when it fills (P4b). The +kernel's `max_open_lots` (surviving + new lots at every match) agrees on 14 of +15, all but P4b; the adapter's command-time count on 8 of 15. P10 is not a +pyramiding question (the quantity a default `strategy.exit(from_entry)` closes +under FIFO) and is pinned as a separate recorded divergence. + +`bars.inc`: per scenario the 12 bars from its first signal bar, from the +registry feed `lab bars NYSE:F 15`, evidence sha256 +`80f404ae85ef0b6a0d8056a90997e92fa1236f1ae68b75c1b2d3a6f182558e32`. Every +market fill lands on an on-grid open; each limit or stop level is first +reached on its intended bar with a tick of margin and no gap. +`expectations.inc`: each scenario's commands and pinned outcomes. diff --git a/tests/fixtures/pyramiding_count/bars.inc b/tests/fixtures/pyramiding_count/bars.inc new file mode 100644 index 00000000..25e710ec --- /dev/null +++ b/tests/fixtures/pyramiding_count/bars.inc @@ -0,0 +1,215 @@ +// Generated from the NYSE:F 15m registry feed +// (evidence sha256 80f404ae85ef0b6a0d8056a90997e92fa1236f1ae68b75c1b2d3a6f182558e32): +// per scenario the 12 bars from its first signal bar s0. {ts_ms, o, h, l, c} +constexpr FeedBar kPyramidBars[15][12] = { + { // P1 (hm-orders-m13-f-pyramiding): s0 2025-06-05 16:30Z + {1749141000000LL, 10.225, 10.225, 10.165, 10.165}, + {1749141900000LL, 10.16, 10.16, 10.11, 10.12}, + {1749142800000LL, 10.12, 10.135, 10.11, 10.12}, + {1749143700000LL, 10.12, 10.12, 10.1, 10.12}, + {1749144600000LL, 10.12, 10.13, 10.11, 10.125}, + {1749145500000LL, 10.125, 10.125, 10.1, 10.11}, + {1749146400000LL, 10.11, 10.115, 10.085, 10.095}, + {1749147300000LL, 10.095, 10.11, 10.06, 10.11}, + {1749148200000LL, 10.11, 10.13, 10.105, 10.12}, + {1749149100000LL, 10.13, 10.13, 10.11, 10.115}, + {1749150000000LL, 10.11, 10.13, 10.09, 10.09}, + {1749150900000LL, 10.095, 10.1, 10.04, 10.07}, + }, + { // P2 (hm-orders-m13-f-pyramiding): s0 2025-06-18 17:30Z + {1750267800000LL, 10.435, 10.44, 10.42, 10.425}, + {1750268700000LL, 10.42, 10.45, 10.41, 10.445}, + {1750269600000LL, 10.45, 10.49, 10.415, 10.425}, + {1750270500000LL, 10.425, 10.44, 10.42, 10.44}, + {1750271400000LL, 10.43, 10.475, 10.43, 10.465}, + {1750272300000LL, 10.46, 10.475, 10.43, 10.44}, + {1750273200000LL, 10.43, 10.465, 10.425, 10.45}, + {1750274100000LL, 10.45, 10.46, 10.43, 10.43}, + {1750275000000LL, 10.43, 10.46, 10.425, 10.46}, + {1750275900000LL, 10.46, 10.46, 10.41, 10.425}, + {1750426200000LL, 10.49, 10.53, 10.47, 10.525}, + {1750427100000LL, 10.53, 10.53, 10.465, 10.505}, + }, + { // P3 (hm-orders-m13-f-pyramiding): s0 2025-06-20 16:45Z + {1750437900000LL, 10.525, 10.535, 10.52, 10.53}, + {1750438800000LL, 10.53, 10.54, 10.505, 10.51}, + {1750439700000LL, 10.51, 10.52, 10.49, 10.49}, + {1750440600000LL, 10.49, 10.505, 10.485, 10.505}, + {1750441500000LL, 10.51, 10.51, 10.49, 10.49}, + {1750442400000LL, 10.49, 10.495, 10.475, 10.485}, + {1750443300000LL, 10.49, 10.505, 10.485, 10.495}, + {1750444200000LL, 10.5, 10.53, 10.5, 10.53}, + {1750445100000LL, 10.53, 10.53, 10.51, 10.515}, + {1750446000000LL, 10.51, 10.53, 10.505, 10.511}, + {1750446900000LL, 10.51, 10.52, 10.51, 10.515}, + {1750447800000LL, 10.52, 10.53, 10.51, 10.525}, + }, + { // P4 (hm-orders-m13-f-pyramiding): s0 2025-06-02 15:00Z + {1748876400000LL, 9.985, 10.02, 9.97, 9.97}, + {1748877300000LL, 9.97, 9.97, 9.9, 9.91}, + {1748878200000LL, 9.91, 9.94, 9.9, 9.935}, + {1748879100000LL, 9.935, 9.935, 9.915, 9.92}, + {1748880000000LL, 9.92, 9.93, 9.9, 9.91}, + {1748880900000LL, 9.915, 9.945, 9.91, 9.945}, + {1748881800000LL, 9.94, 9.94, 9.92, 9.93}, + {1748882700000LL, 9.93, 9.96, 9.925, 9.95}, + {1748883600000LL, 9.945, 9.95, 9.93, 9.93}, + {1748884500000LL, 9.93, 9.945, 9.9, 9.905}, + {1748885400000LL, 9.905, 9.925, 9.905, 9.905}, + {1748886300000LL, 9.905, 9.92, 9.9, 9.915}, + }, + { // P4b (hm-orders-m13-f-pyramiding): s0 2025-06-11 14:30Z + {1749652200000LL, 10.755, 10.765, 10.74, 10.755}, + {1749653100000LL, 10.75, 10.75, 10.695, 10.705}, + {1749654000000LL, 10.705, 10.74, 10.705, 10.725}, + {1749654900000LL, 10.73, 10.745, 10.72, 10.725}, + {1749655800000LL, 10.73, 10.74, 10.72, 10.72}, + {1749656700000LL, 10.725, 10.73, 10.705, 10.715}, + {1749657600000LL, 10.715, 10.715, 10.67, 10.67}, + {1749658500000LL, 10.67, 10.68, 10.64, 10.655}, + {1749659400000LL, 10.66, 10.665, 10.635, 10.665}, + {1749660300000LL, 10.665, 10.705, 10.655, 10.695}, + {1749661200000LL, 10.69, 10.71, 10.685, 10.705}, + {1749662100000LL, 10.7, 10.73, 10.7, 10.725}, + }, + { // P6 (hm-orders-m13-f-pyramiding): s0 2025-06-03 14:45Z + {1748961900000LL, 10.105, 10.16, 10.105, 10.125}, + {1748962800000LL, 10.13, 10.155, 10.125, 10.145}, + {1748963700000LL, 10.14, 10.155, 10.13, 10.14}, + {1748964600000LL, 10.14, 10.16, 10.135, 10.145}, + {1748965500000LL, 10.14, 10.165, 10.13, 10.15}, + {1748966400000LL, 10.145, 10.2, 10.145, 10.195}, + {1748967300000LL, 10.195, 10.21, 10.18, 10.205}, + {1748968200000LL, 10.2, 10.215, 10.185, 10.185}, + {1748969100000LL, 10.185, 10.205, 10.18, 10.2}, + {1748970000000LL, 10.195, 10.2, 10.18, 10.19}, + {1748970900000LL, 10.185, 10.2, 10.17, 10.175}, + {1748971800000LL, 10.175, 10.195, 10.175, 10.19}, + }, + { // P8 (hm-orders-m13-f-pyramiding): s0 2025-06-23 15:00Z + {1750690800000LL, 10.73, 10.75, 10.725, 10.74}, + {1750691700000LL, 10.74, 10.74, 10.715, 10.715}, + {1750692600000LL, 10.71, 10.72, 10.69, 10.695}, + {1750693500000LL, 10.695, 10.695, 10.67, 10.67}, + {1750694400000LL, 10.67, 10.67, 10.64, 10.64}, + {1750695300000LL, 10.65, 10.65, 10.605, 10.605}, + {1750696200000LL, 10.605, 10.61, 10.565, 10.61}, + {1750697100000LL, 10.61, 10.62, 10.565, 10.575}, + {1750698000000LL, 10.575, 10.62, 10.57, 10.615}, + {1750698900000LL, 10.62, 10.68, 10.615, 10.675}, + {1750699800000LL, 10.68, 10.72, 10.68, 10.715}, + {1750700700000LL, 10.72, 10.73, 10.715, 10.725}, + }, + { // P9 (hm-orders-m13-f-pyramiding): s0 2025-06-30 16:45Z + {1751301900000LL, 10.75, 10.77, 10.745, 10.765}, + {1751302800000LL, 10.77, 10.77, 10.76, 10.765}, + {1751303700000LL, 10.77, 10.81, 10.76, 10.785}, + {1751304600000LL, 10.785, 10.79, 10.775, 10.78}, + {1751305500000LL, 10.78, 10.78, 10.735, 10.75}, + {1751306400000LL, 10.76, 10.77, 10.745, 10.76}, + {1751307300000LL, 10.76, 10.77, 10.76, 10.76}, + {1751308200000LL, 10.77, 10.775, 10.76, 10.775}, + {1751309100000LL, 10.78, 10.785, 10.77, 10.78}, + {1751310000000LL, 10.78, 10.8, 10.78, 10.79}, + {1751310900000LL, 10.795, 10.82, 10.78, 10.815}, + {1751311800000LL, 10.82, 10.84, 10.8, 10.825}, + }, + { // P10 (hm-orders-m13-f-pyramiding): s0 2025-07-21 17:45Z + {1753119900000LL, 11.36, 11.36, 11.335, 11.335}, + {1753120800000LL, 11.34, 11.37, 11.335, 11.365}, + {1753121700000LL, 11.37, 11.41, 11.365, 11.405}, + {1753122600000LL, 11.405, 11.41, 11.4, 11.405}, + {1753123500000LL, 11.405, 11.41, 11.37, 11.37}, + {1753124400000LL, 11.375, 11.375, 11.325, 11.335}, + {1753125300000LL, 11.335, 11.36, 11.33, 11.335}, + {1753126200000LL, 11.34, 11.345, 11.31, 11.31}, + {1753127100000LL, 11.315, 11.33, 11.3, 11.33}, + {1753191000000LL, 11.24, 11.28, 11.08, 11.145}, + {1753191900000LL, 11.145, 11.195, 11.13, 11.16}, + {1753192800000LL, 11.15, 11.185, 11.12, 11.155}, + }, + { // D1 (hm-orders-m13-f-pyramiding-rule): s0 2025-07-01 16:45Z + {1751388300000LL, 11.29, 11.3, 11.21, 11.21}, + {1751389200000LL, 11.21, 11.25, 11.2, 11.245}, + {1751390100000LL, 11.25, 11.29, 11.245, 11.275}, + {1751391000000LL, 11.29, 11.315, 11.28, 11.305}, + {1751391900000LL, 11.305, 11.31, 11.29, 11.29}, + {1751392800000LL, 11.29, 11.29, 11.27, 11.28}, + {1751393700000LL, 11.275, 11.31, 11.27, 11.305}, + {1751394600000LL, 11.31, 11.32, 11.3, 11.305}, + {1751395500000LL, 11.31, 11.36, 11.305, 11.35}, + {1751396400000LL, 11.345, 11.36, 11.33, 11.345}, + {1751397300000LL, 11.345, 11.35, 11.32, 11.35}, + {1751398200000LL, 11.35, 11.37, 11.31, 11.32}, + }, + { // D2 (hm-orders-m13-f-pyramiding-rule): s0 2025-07-09 14:15Z + {1752070500000LL, 11.83, 11.835, 11.79, 11.795}, + {1752071400000LL, 11.8, 11.8, 11.77, 11.78}, + {1752072300000LL, 11.775, 11.79, 11.76, 11.765}, + {1752073200000LL, 11.765, 11.84, 11.76, 11.825}, + {1752074100000LL, 11.825, 11.85, 11.82, 11.845}, + {1752075000000LL, 11.85, 11.85, 11.825, 11.835}, + {1752075900000LL, 11.835, 11.85, 11.82, 11.835}, + {1752076800000LL, 11.835, 11.86, 11.835, 11.855}, + {1752077700000LL, 11.855, 11.86, 11.825, 11.835}, + {1752078600000LL, 11.84, 11.85, 11.83, 11.845}, + {1752079500000LL, 11.845, 11.87, 11.845, 11.855}, + {1752080400000LL, 11.855, 11.89, 11.85, 11.89}, + }, + { // D3 (hm-orders-m13-f-pyramiding-rule): s0 2025-07-11 16:30Z + {1752251400000LL, 11.75, 11.76, 11.745, 11.755}, + {1752252300000LL, 11.76, 11.78, 11.76, 11.78}, + {1752253200000LL, 11.78, 11.8, 11.78, 11.795}, + {1752254100000LL, 11.795, 11.805, 11.795, 11.8}, + {1752255000000LL, 11.8, 11.81, 11.795, 11.805}, + {1752255900000LL, 11.805, 11.81, 11.795, 11.81}, + {1752256800000LL, 11.81, 11.83, 11.805, 11.83}, + {1752257700000LL, 11.83, 11.83, 11.82, 11.83}, + {1752258600000LL, 11.83, 11.86, 11.825, 11.855}, + {1752259500000LL, 11.855, 11.86, 11.84, 11.855}, + {1752260400000LL, 11.855, 11.855, 11.81, 11.815}, + {1752261300000LL, 11.815, 11.82, 11.805, 11.805}, + }, + { // D3b (hm-orders-m13-f-pyramiding-rule): s0 2025-07-14 14:00Z + {1752501600000LL, 11.73, 11.74, 11.685, 11.72}, + {1752502500000LL, 11.73, 11.755, 11.685, 11.69}, + {1752503400000LL, 11.69, 11.76, 11.69, 11.745}, + {1752504300000LL, 11.745, 11.755, 11.68, 11.71}, + {1752505200000LL, 11.7, 11.72, 11.68, 11.71}, + {1752506100000LL, 11.7, 11.73, 11.67, 11.725}, + {1752507000000LL, 11.73, 11.75, 11.725, 11.73}, + {1752507900000LL, 11.73, 11.73, 11.71, 11.715}, + {1752508800000LL, 11.715, 11.745, 11.715, 11.725}, + {1752509700000LL, 11.725, 11.73, 11.72, 11.725}, + {1752510600000LL, 11.725, 11.74, 11.715, 11.73}, + {1752511500000LL, 11.735, 11.75, 11.735, 11.745}, + }, + { // D4 (hm-orders-m13-f-pyramiding-cap-orders): s0 2025-07-30 17:30Z + {1753896600000LL, 10.985, 10.99, 10.97, 10.985}, + {1753897500000LL, 10.99, 10.99, 10.98, 10.985}, + {1753898400000LL, 10.99, 11, 10.95, 10.975}, + {1753899300000LL, 10.975, 10.975, 10.95, 10.955}, + {1753900200000LL, 10.955, 10.985, 10.925, 10.955}, + {1753901100000LL, 10.955, 10.96, 10.88, 10.885}, + {1753902000000LL, 10.885, 10.895, 10.84, 10.885}, + {1753902900000LL, 10.885, 10.89, 10.845, 10.855}, + {1753903800000LL, 10.85, 10.87, 10.82, 10.825}, + {1753904700000LL, 10.825, 10.91, 10.825, 10.905}, + {1753968600000LL, 10.77, 10.92, 10.69, 10.845}, + {1753969500000LL, 10.845, 10.99, 10.825, 10.945}, + }, + { // D4s (hm-orders-m13-f-pyramiding-cap-orders): s0 2025-07-31 14:15Z + {1753971300000LL, 10.93, 10.98, 10.93, 10.94}, + {1753972200000LL, 10.95, 11.085, 10.935, 11.05}, + {1753973100000LL, 11.06, 11.07, 11.035, 11.035}, + {1753974000000LL, 11.04, 11.1, 10.935, 10.975}, + {1753974900000LL, 10.98, 11.055, 10.955, 11.04}, + {1753975800000LL, 11.035, 11.11, 11.03, 11.1}, + {1753976700000LL, 11.105, 11.12, 11.085, 11.11}, + {1753977600000LL, 11.11, 11.11, 11.055, 11.095}, + {1753978500000LL, 11.1, 11.13, 11.09, 11.115}, + {1753979400000LL, 11.115, 11.14, 11.105, 11.13}, + {1753980300000LL, 11.14, 11.15, 11.11, 11.135}, + {1753981200000LL, 11.14, 11.14, 11.095, 11.105}, + }, +}; diff --git a/tests/fixtures/pyramiding_count/expectations.inc b/tests/fixtures/pyramiding_count/expectations.inc new file mode 100644 index 00000000..80433625 --- /dev/null +++ b/tests/fixtures/pyramiding_count/expectations.inc @@ -0,0 +1,80 @@ +// Every scenario of the three pyramiding tapes (strategy.pine in each tape +// directory), in kPyramidBars order: its commands per signal-bar offset, which of +// the two outcomes TradingView agrees with, and the pinned adapter / kernel outcome. +const Scenario kScenarios[] = { + {"P1", "hm-orders-m13-f-pyramiding", + {{0, Op::Entry, "L"}, {1, Op::Entry, "L"}, {3, Op::Close, "L", 100.0}, {5, Op::Entry, "L"}, {7, Op::CloseAll, "close_all"}}, + true, true, + "L 100 @1:10.16->4:10.12; L 100 @2:10.12->8:10.11; L 100 @6:10.11->8:10.11; ", + "L 100 @1:10.16->4:10.12; L 100 @2:10.12->8:10.11; L 100 @6:10.11->8:10.11; "}, + {"P2", "hm-orders-m13-f-pyramiding", + {{0, Op::Entry, "L", 200.0}, {1, Op::Entry, "L", 200.0}, {3, Op::Close, "L", 100.0}, {5, Op::Entry, "L", 200.0}, {7, Op::CloseAll, "close_all"}}, + true, true, + "L 100 @1:10.42->4:10.43; L 100 @1:10.42->8:10.43; L 200 @2:10.45->8:10.43; ", + "L 100 @1:10.42->4:10.43; L 100 @1:10.42->8:10.43; L 200 @2:10.45->8:10.43; "}, + {"P3", "hm-orders-m13-f-pyramiding", + {{0, Op::Entry, "A"}, {1, Op::Entry, "B"}, {3, Op::Close, "A"}, {5, Op::Entry, "C"}, {7, Op::CloseAll, "close_all"}}, + true, true, + "A 100 @1:10.53->4:10.51; B 100 @2:10.51->8:10.53; C 100 @6:10.49->8:10.53; ", + "A 100 @1:10.53->4:10.51; B 100 @2:10.51->8:10.53; C 100 @6:10.49->8:10.53; "}, + {"P4", "hm-orders-m13-f-pyramiding", + {{0, Op::Entry, "L"}, {2, Op::LimitEntry, "L2", kNaN, 8.93}, {3, Op::Entry, "L3"}, {5, Op::Cancel, "L2"}, {6, Op::CloseAll, "close_all"}}, + false, true, + "L 100 @1:9.97->7:9.93; ", + "L 100 @1:9.97->7:9.93; L3 100 @4:9.92->7:9.93; "}, + {"P4b", "hm-orders-m13-f-pyramiding", + {{0, Op::Entry, "L"}, {2, Op::LimitEntry, "L2", kNaN, 10.68}, {3, Op::Entry, "L3"}, {7, Op::CloseAll, "close_all"}}, + false, false, + "L 100 @1:10.75->8:10.66; L2 100 @6:10.68->8:10.66; ", + "L 100 @1:10.75->8:10.66; L3 100 @4:10.73->8:10.66; "}, + {"P6", "hm-orders-m13-f-pyramiding", + {{0, Op::Entry, "A"}, {0, Op::Entry, "B"}, {0, Op::Entry, "C"}, {3, Op::CloseAll, "close_all"}}, + false, true, + "A 100 @1:10.13->4:10.14; B 100 @1:10.13->4:10.14; C 100 @1:10.13->4:10.14; ", + "A 100 @1:10.13->4:10.14; B 100 @1:10.13->4:10.14; "}, + {"P8", "hm-orders-m13-f-pyramiding", + {{0, Op::Entry, "A"}, {1, Op::Entry, "B"}, {2, Op::StopExit, "XB", 100.0, 10.65, "B"}, {6, Op::Entry, "C"}, {8, Op::CloseAll, "close_all"}}, + false, true, + "A 100 @1:10.74->4:10.65; B 100 @2:10.71->9:10.62; ", + "A 100 @1:10.74->4:10.65; B 100 @2:10.71->9:10.62; C 100 @7:10.61->9:10.62; "}, + {"P9", "hm-orders-m13-f-pyramiding", + {{0, Op::Entry, "A"}, {1, Op::Entry, "B"}, {2, Op::StopExit, "XA", 100.0, 10.75, "A"}, {6, Op::Entry, "C"}, {8, Op::CloseAll, "close_all"}}, + true, true, + "A 100 @1:10.77->4:10.75; B 100 @2:10.77->9:10.78; C 100 @7:10.77->9:10.78; ", + "A 100 @1:10.77->4:10.75; B 100 @2:10.77->9:10.78; C 100 @7:10.77->9:10.78; "}, + {"P10", "hm-orders-m13-f-pyramiding", + {{0, Op::Entry, "A"}, {1, Op::Entry, "B"}, {2, Op::StopExit, "XB", kNaN, 11.38, "B"}, {6, Op::CloseAll, "close_all"}}, + false, true, + "A 100 @1:11.34->4:11.38; B 100 @2:11.37->4:11.38; ", + "A 100 @1:11.34->4:11.38; B 100 @2:11.37->7:11.34; "}, + {"D1", "hm-orders-m13-f-pyramiding-rule", + {{0, Op::Entry, "A"}, {2, Op::LimitEntry, "L2", kNaN, 10.27}, {2, Op::Entry, "M"}, {5, Op::Cancel, "L2"}, {6, Op::CloseAll, "close_all"}}, + false, true, + "A 100 @1:11.21->7:11.31; ", + "A 100 @1:11.21->7:11.31; M 100 @3:11.29->7:11.31; "}, + {"D2", "hm-orders-m13-f-pyramiding-rule", + {{0, Op::LimitEntry, "L2", kNaN, 10.79}, {0, Op::Entry, "M1"}, {0, Op::Entry, "M2"}, {3, Op::Cancel, "L2"}, {4, Op::CloseAll, "close_all"}}, + true, true, + "M1 100 @1:11.80->5:11.85; M2 100 @1:11.80->5:11.85; ", + "M1 100 @1:11.80->5:11.85; M2 100 @1:11.80->5:11.85; "}, + {"D3", "hm-orders-m13-f-pyramiding-rule", + {{0, Op::Entry, "A"}, {1, Op::Entry, "B"}, {3, Op::Close, "A"}, {3, Op::Entry, "C"}, {6, Op::CloseAll, "close_all"}}, + false, true, + "A 100 @1:11.76->4:11.80; B 100 @2:11.78->7:11.83; ", + "A 100 @1:11.76->4:11.80; B 100 @2:11.78->7:11.83; C 100 @4:11.80->7:11.83; "}, + {"D3b", "hm-orders-m13-f-pyramiding-rule", + {{0, Op::Entry, "A"}, {1, Op::Entry, "B"}, {3, Op::Entry, "C"}, {3, Op::Close, "A"}, {6, Op::CloseAll, "close_all"}}, + true, true, + "A 100 @1:11.73->4:11.70; B 100 @2:11.69->7:11.73; ", + "A 100 @1:11.73->4:11.70; B 100 @2:11.69->7:11.73; "}, + {"D4", "hm-orders-m13-f-pyramiding-cap-orders", + {{0, Op::Entry, "A"}, {1, Op::Entry, "B"}, {3, Op::LimitEntry, "L2", kNaN, 10.89}, {7, Op::CloseAll, "close_all"}}, + true, true, + "A 100 @1:10.99->8:10.85; B 100 @2:10.99->8:10.85; ", + "A 100 @1:10.99->8:10.85; B 100 @2:10.99->8:10.85; "}, + {"D4s", "hm-orders-m13-f-pyramiding-cap-orders", + {{0, Op::Entry, "A"}, {1, Op::Entry, "B"}, {3, Op::StopEntry, "S2", kNaN, 11.10}, {7, Op::CloseAll, "close_all"}}, + true, true, + "A 100 @1:10.95->8:11.10; B 100 @2:11.06->8:11.10; ", + "A 100 @1:10.95->8:11.10; B 100 @2:11.06->8:11.10; "}, +}; diff --git a/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/meta.json b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/meta.json new file mode 100644 index 00000000..26580a20 --- /dev/null +++ b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-MEASURE M13 NYSE:F pyramiding cap orders", + "slug": "hm-orders-m13-f-pyramiding-cap-orders", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:02:37.563Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-09-30" + }, + "pine_sha256": "e97ac2c332282fdb9562be3836610a1594614ba362e669473515b379e3f4e054", + "tv_trades_sha256": "67cb6805a7b9cb20c8542e1e4c0805dbd539d1f3182dafe4a020a0c01c77a1b5", + "tv_trades_rows": 8 +} diff --git a/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/metrics.json b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/metrics.json new file mode 100644 index 00000000..fc26bdbd --- /dev/null +++ b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-orders-m13-f-pyramiding-cap-orders", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-09-30", + "deepBacktesting": true, + "trades": 4, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "e97ac2c332282fdb9562be3836610a1594614ba362e669473515b379e3f4e054", + "tvTradesCsvHash": "67cb6805a7b9cb20c8542e1e4c0805dbd539d1f3182dafe4a020a0c01c77a1b5", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74411", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1759190400000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1759175100000 + }, + "rangeProof": "covered", + "rawFrameSha256": "75cc9cb3b3922643a36fac725fc406515748f79a587f9dd7ef54c3ad16a43aaa", + "decodedReportSha256": "bfdb511324f9ee95609e8a7b5a0d47423e0fd2282533f850289b35c2579b936f", + "closedTrades": 4, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:02:37.561Z" +} diff --git a/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/strategy.pine b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/strategy.pine new file mode 100644 index 00000000..341462e8 --- /dev/null +++ b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/strategy.pine @@ -0,0 +1,32 @@ +// H-MEASURE row M13 / G2-15 (group orders), third tape: is a limit or stop entry +// placed AT the pyramiding cap refused, or does it fill when its level is reached? +// pyramiding = 2, fixed 100 shares (UTC signal-bar times): +// D4 2025-07-30 17:30Z two lots open (the cap); a limit entry placed there, first reached two bars later +// D4s 2025-07-31 14:15Z two lots open (the cap); a stop entry placed there, first reached two bars later +//@version=6 +strategy("H-MEASURE M13 NYSE:F pyramiding cap orders", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=2, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +// D4: two lots open (the cap); a limit entry placed there, first reached two bars later +if at(2025, 7, 30, 17, 30) + strategy.entry("A", strategy.long) +if at(2025, 7, 30, 17, 45) + strategy.entry("B", strategy.long) +if at(2025, 7, 30, 18, 15) + strategy.entry("L2", strategy.long, limit=10.89) +if at(2025, 7, 30, 19, 15) + strategy.close_all() + +// D4s: two lots open (the cap); a stop entry placed there, first reached two bars later +if at(2025, 7, 31, 14, 15) + strategy.entry("A", strategy.long) +if at(2025, 7, 31, 14, 30) + strategy.entry("B", strategy.long) +if at(2025, 7, 31, 15, 0) + strategy.entry("S2", strategy.long, stop=11.10) +if at(2025, 7, 31, 16, 0) + strategy.close_all() diff --git a/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/tv_trades.csv b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/tv_trades.csv new file mode 100644 index 00000000..3c2aa74a --- /dev/null +++ b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-cap-orders/tv_trades.csv @@ -0,0 +1,9 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-31 03:30,Close position order,10.85,100,1099,-14,-1.27,0,1,0.09,-15,-1.36,-14,-0.01,7 +1,Entry long,2025-07-31 01:45,A,10.99,100,1099,-14,-1.27,0,1,0.09,-15,-1.36,-14,-0.01,7 +2,Exit long,2025-07-31 03:30,Close position order,10.85,100,1099,-14,-1.27,0,1,0.09,-15,-1.36,-28,-0.03,6 +2,Entry long,2025-07-31 02:00,B,10.99,100,1099,-14,-1.27,0,1,0.09,-15,-1.36,-28,-0.03,6 +3,Exit long,2025-08-01 00:15,Close position order,11.1,100,1095,15,1.37,0,17,1.55,-1,-0.09,-13,-0.01,7 +3,Entry long,2025-07-31 22:30,A,10.95,100,1095,15,1.37,0,17,1.55,-1,-0.09,-13,-0.01,7 +4,Exit long,2025-08-01 00:15,Close position order,11.1,100,1106,4,0.36,0,6,0.54,-12,-1.08,-9,-0.01,6 +4,Entry long,2025-07-31 22:45,B,11.06,100,1106,4,0.36,0,6,0.54,-12,-1.08,-9,-0.01,6 \ No newline at end of file diff --git a/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/meta.json b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/meta.json new file mode 100644 index 00000000..41d24a31 --- /dev/null +++ b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-MEASURE M13 NYSE:F pyramiding rule", + "slug": "hm-orders-m13-f-pyramiding-rule", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:01:53.491Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-06-02", + "to": "2025-07-31" + }, + "pine_sha256": "f26882569e83f59904481ed88b53731225963b94b987d05cfda0f9ce2e8c3345", + "tv_trades_sha256": "44c68d01446717e491eb9389ac5e6db8ef9f501a437bf7640cd6e1b3ebe0cfa4", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/metrics.json b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/metrics.json new file mode 100644 index 00000000..db56f8c2 --- /dev/null +++ b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-orders-m13-f-pyramiding-rule", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-06-02", + "to": "2025-07-31", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "f26882569e83f59904481ed88b53731225963b94b987d05cfda0f9ce2e8c3345", + "tvTradesCsvHash": "44c68d01446717e491eb9389ac5e6db8ef9f501a437bf7640cd6e1b3ebe0cfa4", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74409", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1748822400000, + "to": 1753920000000 + }, + "returnedRange": { + "from": 1748871000000, + "to": 1753904700000 + }, + "rangeProof": "covered", + "rawFrameSha256": "dcfa54a9d3cd83c2e703227b760cb25c52e6a82d64606c961f340d46768afecf", + "decodedReportSha256": "6d9bb14eb0783e40798ab13b4a5c5fd260798414657305836ee579e9924eb290", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:01:53.491Z" +} diff --git a/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/strategy.pine b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/strategy.pine new file mode 100644 index 00000000..43be9e83 --- /dev/null +++ b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/strategy.pine @@ -0,0 +1,57 @@ +// H-MEASURE row M13 / G2-15 (group orders), second tape: WHICH rule does TradingView's +// pyramiding apply? pyramiding = 2, fixed 100 shares, fills at the next bar's open. +// Four scenarios, each on its own NYSE:F session (UTC signal-bar times): +// D1 2025-07-01 16:45Z one lot open; on one bar a far limit entry, then a market add +// D2 2025-07-09 14:15Z flat; on one bar a far limit entry, then two market entries +// D3 2025-07-11 16:30Z two lots open (the cap); on one bar strategy.close("A"), then entry("C") +// D3b 2025-07-14 14:00Z two lots open (the cap); on one bar entry("C"), then strategy.close("A") +//@version=6 +strategy("H-MEASURE M13 NYSE:F pyramiding rule", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=2, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +// D1: one lot open; on one bar a far limit entry, then a market add +if at(2025, 7, 1, 16, 45) + strategy.entry("A", strategy.long) +if at(2025, 7, 1, 17, 15) + strategy.entry("L2", strategy.long, limit=10.27) + strategy.entry("M", strategy.long) +if at(2025, 7, 1, 18, 0) + strategy.cancel("L2") +if at(2025, 7, 1, 18, 15) + strategy.close_all() + +// D2: flat; on one bar a far limit entry, then two market entries +if at(2025, 7, 9, 14, 15) + strategy.entry("L2", strategy.long, limit=10.79) + strategy.entry("M1", strategy.long) + strategy.entry("M2", strategy.long) +if at(2025, 7, 9, 15, 0) + strategy.cancel("L2") +if at(2025, 7, 9, 15, 15) + strategy.close_all() + +// D3: two lots open (the cap); on one bar strategy.close("A"), then entry("C") +if at(2025, 7, 11, 16, 30) + strategy.entry("A", strategy.long) +if at(2025, 7, 11, 16, 45) + strategy.entry("B", strategy.long) +if at(2025, 7, 11, 17, 15) + strategy.close("A") + strategy.entry("C", strategy.long) +if at(2025, 7, 11, 18, 0) + strategy.close_all() + +// D3b: two lots open (the cap); on one bar entry("C"), then strategy.close("A") +if at(2025, 7, 14, 14, 0) + strategy.entry("A", strategy.long) +if at(2025, 7, 14, 14, 15) + strategy.entry("B", strategy.long) +if at(2025, 7, 14, 14, 45) + strategy.entry("C", strategy.long) + strategy.close("A") +if at(2025, 7, 14, 15, 30) + strategy.close_all() diff --git a/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/tv_trades.csv b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/tv_trades.csv new file mode 100644 index 00000000..57cb25bb --- /dev/null +++ b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding-rule/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 02:30,Close position order,11.31,100,1121,10,0.89,0,11,0.98,-1,-0.09,10,0.01,6 +1,Entry long,2025-07-02 01:00,A,11.21,100,1121,10,0.89,0,11,0.98,-1,-0.09,10,0.01,6 +2,Exit long,2025-07-02 02:30,Close position order,11.31,100,1129,2,0.18,0,3,0.27,-2,-0.18,12,0.01,4 +2,Entry long,2025-07-02 01:30,M,11.29,100,1129,2,0.18,0,3,0.27,-2,-0.18,12,0.01,4 +3,Exit long,2025-07-09 23:30,Close position order,11.85,100,1180,5,0.42,0,5,0.42,-4,-0.34,17,0.02,4 +3,Entry long,2025-07-09 22:30,M1,11.8,100,1180,5,0.42,0,5,0.42,-4,-0.34,17,0.02,4 +4,Exit long,2025-07-09 23:30,Close position order,11.85,100,1180,5,0.42,0,5,0.42,-4,-0.34,22,0.02,4 +4,Entry long,2025-07-09 22:30,M2,11.8,100,1180,5,0.42,0,5,0.42,-4,-0.34,22,0.02,4 +5,Exit long,2025-07-12 01:30,Close entry(s) order A,11.8,100,1176,4,0.34,0,5,0.43,0,0.00,26,0.03,3 +5,Entry long,2025-07-12 00:45,A,11.76,100,1176,4,0.34,0,5,0.43,0,0.00,26,0.03,3 +6,Exit long,2025-07-12 02:15,Close position order,11.83,100,1178,5,0.42,0,5,0.42,0,0.00,31,0.03,5 +6,Entry long,2025-07-12 01:00,B,11.78,100,1178,5,0.42,0,5,0.42,0,0.00,31,0.03,5 +7,Exit long,2025-07-12 02:15,Close position order,11.83,100,1180,3,0.25,0,3,0.25,0,0.00,34,0.03,3 +7,Entry long,2025-07-12 01:30,C,11.8,100,1180,3,0.25,0,3,0.25,0,0.00,34,0.03,3 +8,Exit long,2025-07-14 23:00,Close entry(s) order A,11.7,100,1173,-3,-0.26,0,3,0.26,-5,-0.43,31,0.03,3 +8,Entry long,2025-07-14 22:15,A,11.73,100,1173,-3,-0.26,0,3,0.26,-5,-0.43,31,0.03,3 +9,Exit long,2025-07-14 23:45,Close position order,11.73,100,1169,4,0.34,0,7,0.60,-2,-0.17,35,0.03,5 +9,Entry long,2025-07-14 22:30,B,11.69,100,1169,4,0.34,0,7,0.60,-2,-0.17,35,0.03,5 \ No newline at end of file diff --git a/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/meta.json b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/meta.json new file mode 100644 index 00000000..3dc2d495 --- /dev/null +++ b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-MEASURE M13 NYSE:F pyramiding count", + "slug": "hm-orders-m13-f-pyramiding", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:57:42.928Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-05-01", + "to": "2025-07-31" + }, + "pine_sha256": "99eb81b7cf2a4b2623c2492f315b05a0b6c0d54582c398e34c6ce080ae68e5cf", + "tv_trades_sha256": "de780cacf8ee965d787ef715d3af6ddf439370652dea820e71dbad55c4b0909d", + "tv_trades_rows": 48 +} diff --git a/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/metrics.json b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/metrics.json new file mode 100644 index 00000000..edabd03c --- /dev/null +++ b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-orders-m13-f-pyramiding", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-05-01", + "to": "2025-07-31", + "deepBacktesting": true, + "trades": 24, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "99eb81b7cf2a4b2623c2492f315b05a0b6c0d54582c398e34c6ce080ae68e5cf", + "tvTradesCsvHash": "de780cacf8ee965d787ef715d3af6ddf439370652dea820e71dbad55c4b0909d", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74407", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1746057600000, + "to": 1753920000000 + }, + "returnedRange": { + "from": 1746106200000, + "to": 1753904700000 + }, + "rangeProof": "covered", + "rawFrameSha256": "b066ac5d352dbcf3967634571804265e850c3047a209f96e6e8da0104e82c19c", + "decodedReportSha256": "80d75e2a90449cef155567eb418429de2dea17249cc0ee2507e3e05fdfd6e212", + "closedTrades": 24, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:57:42.927Z" +} diff --git a/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/strategy.pine b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/strategy.pine new file mode 100644 index 00000000..729c670d --- /dev/null +++ b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/strategy.pine @@ -0,0 +1,120 @@ +// H-MEASURE row M13 / G2-15 (group orders): Pine pyramiding as TradingView counts it. +// pyramiding = 2, fixed 100 shares, orders filled at the next bar's open. Nine +// scenarios, each on its own NYSE:F session, each flat at its start and closed by +// strategy.close_all at its end (UTC signal-bar times): +// P1 2025-06-05 16:30Z a strategy.close(qty) that closes the first lot whole, then a re-add (same id) +// P2 2025-06-18 17:30Z a strategy.close(qty) inside a lot (2+2, close 1), then a re-add +// P3 2025-06-20 16:45Z strategy.close of one id of two, then a third id +// P4 2025-06-02 15:00Z a resting limit entry, then a market add while it rests (never fills) +// P4b 2025-06-11 14:30Z a resting limit entry that fills AFTER a market add +// P6 2025-06-03 14:45Z three market entries on one bar while flat +// P8 2025-06-23 15:00Z strategy.exit(qty) FROM B stops out the oldest lot (A, FIFO), then a third id +// P9 2025-06-30 16:45Z strategy.exit(qty) FROM A stops out lot A, then a third id +// P10 2025-07-21 17:45Z strategy.exit FROM B with the default quantity, two lots open (how much closes?) +//@version=6 +strategy("H-MEASURE M13 NYSE:F pyramiding count", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=2, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +// P1: a strategy.close(qty) that closes the first lot whole, then a re-add (same id) +if at(2025, 6, 5, 16, 30) + strategy.entry("L", strategy.long) +if at(2025, 6, 5, 16, 45) + strategy.entry("L", strategy.long) +if at(2025, 6, 5, 17, 15) + strategy.close("L", qty=100) +if at(2025, 6, 5, 17, 45) + strategy.entry("L", strategy.long) +if at(2025, 6, 5, 18, 15) + strategy.close_all() + +// P2: a strategy.close(qty) inside a lot (2+2, close 1), then a re-add +if at(2025, 6, 18, 17, 30) + strategy.entry("L", strategy.long, qty=200) +if at(2025, 6, 18, 17, 45) + strategy.entry("L", strategy.long, qty=200) +if at(2025, 6, 18, 18, 15) + strategy.close("L", qty=100) +if at(2025, 6, 18, 18, 45) + strategy.entry("L", strategy.long, qty=200) +if at(2025, 6, 18, 19, 15) + strategy.close_all() + +// P3: strategy.close of one id of two, then a third id +if at(2025, 6, 20, 16, 45) + strategy.entry("A", strategy.long) +if at(2025, 6, 20, 17, 0) + strategy.entry("B", strategy.long) +if at(2025, 6, 20, 17, 30) + strategy.close("A") +if at(2025, 6, 20, 18, 0) + strategy.entry("C", strategy.long) +if at(2025, 6, 20, 18, 30) + strategy.close_all() + +// P4: a resting limit entry, then a market add while it rests (never fills) +if at(2025, 6, 2, 15, 0) + strategy.entry("L", strategy.long) +if at(2025, 6, 2, 15, 30) + strategy.entry("L2", strategy.long, limit=8.93) +if at(2025, 6, 2, 15, 45) + strategy.entry("L3", strategy.long) +if at(2025, 6, 2, 16, 15) + strategy.cancel("L2") +if at(2025, 6, 2, 16, 30) + strategy.close_all() + +// P4b: a resting limit entry that fills AFTER a market add +if at(2025, 6, 11, 14, 30) + strategy.entry("L", strategy.long) +if at(2025, 6, 11, 15, 0) + strategy.entry("L2", strategy.long, limit=10.68) +if at(2025, 6, 11, 15, 15) + strategy.entry("L3", strategy.long) +if at(2025, 6, 11, 16, 15) + strategy.close_all() + +// P6: three market entries on one bar while flat +if at(2025, 6, 3, 14, 45) + strategy.entry("A", strategy.long) + strategy.entry("B", strategy.long) + strategy.entry("C", strategy.long) +if at(2025, 6, 3, 15, 30) + strategy.close_all() + +// P8: strategy.exit(qty) FROM B stops out the oldest lot (A, FIFO), then a third id +if at(2025, 6, 23, 15, 0) + strategy.entry("A", strategy.long) +if at(2025, 6, 23, 15, 15) + strategy.entry("B", strategy.long) +if at(2025, 6, 23, 15, 30) + strategy.exit("XB", "B", stop=10.65, qty=100) +if at(2025, 6, 23, 16, 30) + strategy.entry("C", strategy.long) +if at(2025, 6, 23, 17, 0) + strategy.close_all() + +// P9: strategy.exit(qty) FROM A stops out lot A, then a third id +if at(2025, 6, 30, 16, 45) + strategy.entry("A", strategy.long) +if at(2025, 6, 30, 17, 0) + strategy.entry("B", strategy.long) +if at(2025, 6, 30, 17, 15) + strategy.exit("XA", "A", stop=10.75, qty=100) +if at(2025, 6, 30, 18, 15) + strategy.entry("C", strategy.long) +if at(2025, 6, 30, 18, 45) + strategy.close_all() + +// P10: strategy.exit FROM B with the default quantity, two lots open (how much closes?) +if at(2025, 7, 21, 17, 45) + strategy.entry("A", strategy.long) +if at(2025, 7, 21, 18, 0) + strategy.entry("B", strategy.long) +if at(2025, 7, 21, 18, 15) + strategy.exit("XB", "B", stop=11.38) +if at(2025, 7, 21, 19, 15) + strategy.close_all() diff --git a/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/tv_trades.csv b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/tv_trades.csv new file mode 100644 index 00000000..d9552abc --- /dev/null +++ b/tests/fixtures/pyramiding_count/hm-orders-m13-f-pyramiding/tv_trades.csv @@ -0,0 +1,49 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-03 00:45,Close position order,9.93,100,997,-4,-0.40,0,0,0.00,-7,-0.70,-4,0.00,6 +1,Entry long,2025-06-02 23:15,L,9.97,100,997,-4,-0.40,0,0,0.00,-7,-0.70,-4,0.00,6 +2,Exit long,2025-06-03 00:45,Close position order,9.93,100,992,1,0.10,0,3,0.30,-2,-0.20,-3,0.00,3 +2,Entry long,2025-06-03 00:00,L3,9.92,100,992,1,0.10,0,3,0.30,-2,-0.20,-3,0.00,3 +3,Exit long,2025-06-03 23:45,Close position order,10.14,100,1013,1,0.10,0,3,0.30,0,0.00,-2,0.00,3 +3,Entry long,2025-06-03 23:00,A,10.13,100,1013,1,0.10,0,3,0.30,0,0.00,-2,0.00,3 +4,Exit long,2025-06-03 23:45,Close position order,10.14,100,1013,1,0.10,0,3,0.30,0,0.00,-1,0.00,3 +4,Entry long,2025-06-03 23:00,B,10.13,100,1013,1,0.10,0,3,0.30,0,0.00,-1,0.00,3 +5,Exit long,2025-06-06 01:30,Close entry(s) order L,10.12,100,1016,-4,-0.39,0,0,0.00,-6,-0.59,-5,-0.01,3 +5,Entry long,2025-06-06 00:45,L,10.16,100,1016,-4,-0.39,0,0,0.00,-6,-0.59,-5,-0.01,3 +6,Exit long,2025-06-06 02:30,Close position order,10.11,100,1012,-1,-0.10,0,2,0.20,-6,-0.59,-6,-0.01,6 +6,Entry long,2025-06-06 01:00,L,10.12,100,1012,-1,-0.10,0,2,0.20,-6,-0.59,-6,-0.01,6 +7,Exit long,2025-06-06 02:30,Close position order,10.11,100,1011,0,0.00,0,1,0.10,-5,-0.49,-6,-0.01,2 +7,Entry long,2025-06-06 02:00,L,10.11,100,1011,0,0.00,0,1,0.10,-5,-0.49,-6,-0.01,2 +8,Exit long,2025-06-12 00:30,Close position order,10.66,100,1075,-9,-0.84,0,0,0.00,-11,-1.02,-15,-0.01,7 +8,Entry long,2025-06-11 22:45,L,10.75,100,1075,-9,-0.84,0,0,0.00,-11,-1.02,-15,-0.01,7 +9,Exit long,2025-06-12 00:30,Close position order,10.66,100,1073,-7,-0.65,0,1,0.09,-9,-0.84,-22,-0.02,4 +9,Entry long,2025-06-11 23:30,L3,10.73,100,1073,-7,-0.65,0,1,0.09,-9,-0.84,-22,-0.02,4 +10,Exit long,2025-06-12 00:30,Close position order,10.66,100,1068,-2,-0.19,0,0,0.00,-4,-0.37,-24,-0.02,2 +10,Entry long,2025-06-12 00:00,L2,10.68,100,1068,-2,-0.19,0,0,0.00,-4,-0.37,-24,-0.02,2 +11,Exit long,2025-06-19 02:30,Close entry(s) order L,10.43,100,1042,1,0.10,0,7,0.67,-1,-0.10,-23,-0.02,3 +11,Entry long,2025-06-19 01:45,L,10.42,100,1042,1,0.10,0,7,0.67,-1,-0.10,-23,-0.02,3 +12,Exit long,2025-06-19 03:30,Close position order,10.43,100,1042,1,0.10,0,7,0.67,-1,-0.10,-22,-0.02,7 +12,Entry long,2025-06-19 01:45,L,10.42,100,1042,1,0.10,0,7,0.67,-1,-0.10,-22,-0.02,7 +13,Exit long,2025-06-19 03:30,Close position order,10.43,200,2090,-4,-0.19,0,8,0.38,-6,-0.29,-26,-0.03,6 +13,Entry long,2025-06-19 02:00,L,10.45,200,2090,-4,-0.19,0,8,0.38,-6,-0.29,-26,-0.03,6 +14,Exit long,2025-06-21 01:45,Close entry(s) order A,10.51,100,1053,-2,-0.19,0,1,0.09,-4,-0.38,-28,-0.03,3 +14,Entry long,2025-06-21 01:00,A,10.53,100,1053,-2,-0.19,0,1,0.09,-4,-0.38,-28,-0.03,3 +15,Exit long,2025-06-21 02:45,Close position order,10.53,100,1051,2,0.19,0,2,0.19,-3,-0.29,-26,-0.03,6 +15,Entry long,2025-06-21 01:15,B,10.51,100,1051,2,0.19,0,2,0.19,-3,-0.29,-26,-0.03,6 +16,Exit long,2025-06-21 02:45,Close position order,10.53,100,1049,4,0.38,0,4,0.38,0,0.00,-22,-0.02,2 +16,Entry long,2025-06-21 02:15,C,10.49,100,1049,4,0.38,0,4,0.38,0,0.00,-22,-0.02,2 +17,Exit long,2025-06-24 00:00,XB,10.65,100,1074,-9,-0.84,0,0,0.00,-9,-0.84,-31,-0.03,3 +17,Entry long,2025-06-23 23:15,A,10.74,100,1074,-9,-0.84,0,0,0.00,-9,-0.84,-31,-0.03,3 +18,Exit long,2025-06-24 01:15,Close position order,10.62,100,1071,-9,-0.84,0,1,0.09,-14,-1.31,-40,-0.04,7 +18,Entry long,2025-06-23 23:30,B,10.71,100,1071,-9,-0.84,0,1,0.09,-14,-1.31,-40,-0.04,7 +19,Exit long,2025-06-24 01:15,Close position order,10.62,100,1061,1,0.09,0,1,0.09,-4,-0.38,-39,-0.04,2 +19,Entry long,2025-06-24 00:45,C,10.61,100,1061,1,0.09,0,1,0.09,-4,-0.38,-39,-0.04,2 +20,Exit long,2025-07-01 01:45,XA,10.75,100,1077,-2,-0.19,0,4,0.37,-2,-0.19,-41,-0.04,3 +20,Entry long,2025-07-01 01:00,A,10.77,100,1077,-2,-0.19,0,4,0.37,-2,-0.19,-41,-0.04,3 +21,Exit long,2025-07-01 03:00,Close position order,10.78,100,1077,1,0.09,0,4,0.37,-3,-0.28,-40,-0.04,7 +21,Entry long,2025-07-01 01:15,B,10.77,100,1077,1,0.09,0,4,0.37,-3,-0.28,-40,-0.04,7 +22,Exit long,2025-07-01 03:00,Close position order,10.78,100,1077,1,0.09,0,2,0.19,-1,-0.09,-39,-0.04,2 +22,Entry long,2025-07-01 02:30,C,10.77,100,1077,1,0.09,0,2,0.19,-1,-0.09,-39,-0.04,2 +23,Exit long,2025-07-22 02:45,XB,11.38,100,1134,4,0.35,0,7,0.62,0,0.00,-35,-0.03,3 +23,Entry long,2025-07-22 02:00,A,11.34,100,1134,4,0.35,0,7,0.62,0,0.00,-35,-0.03,3 +24,Exit long,2025-07-22 03:30,Close position order,11.34,100,1137,-3,-0.26,0,4,0.35,-4,-0.35,-38,-0.04,5 +24,Entry long,2025-07-22 02:15,B,11.37,100,1137,-3,-0.26,0,4,0.35,-4,-0.35,-38,-0.04,5 \ No newline at end of file diff --git a/tests/fixtures/zero_trail_sibling_stop/README.md b/tests/fixtures/zero_trail_sibling_stop/README.md new file mode 100644 index 00000000..a4805f32 --- /dev/null +++ b/tests/fixtures/zero_trail_sibling_stop/README.md @@ -0,0 +1,25 @@ +# Zero-offset trail, next open ON the carried best (H-MEASURE row M16 / G2-18) + +TradingView's own trades for `strategy.exit(trail_points=n, trail_offset=0)` +issued at the close of the bar whose open filled the entry, where that close +has ALREADY reached the activation (on its tick), and the next bar opens +exactly on the carried best (the close, or the activation when the close +reaches it only on its tick) and moves favourably first to a new extreme. The +Pine adapter rests two legs there: the kernel `Trail` (TrailTicks 0.5, +`best_seed` = the carried best) and a sibling `Stop` at the carried best. +`tests/test_zero_trail_sibling_stop.cpp` replays each trade through the +adapter, through a bare kernel given the adapter's Trail alone, and through a +bare kernel given both legs. + +Each directory is one `lab tv` export (`ws-report-v1`, `rangeProof: covered`, +NYSE:F 15, 2025-04-01 .. 2025-09-30), byte-identical. + +| probe | trades | tvTradesSha256 | TradingView | adapter | kernel Trail alone | +|---|---|---|---|---|---| +| `hm-orders-m16-f-long-zero-trail-open-on-best` | 8 | `4a5bbbd341a7565d7d99a593b20d389b000bdcf3a55960f4c5e19f0a8a7eb5c7` | every exit on the next bar at the carried best's tick (the touch at the open) | 8/8 | 0/8 | +| `hm-orders-m16-f-short-zero-trail-open-on-best` | 8 | `2e1f165d18468d7904ef034cae12f06a7491467236811d45257750a408fafbe4` | the short twin | 8/8 | 0/8 | + +`bars.inc`: per event the 28 feed bars from the signal bar (registry feed +`lab bars NYSE:F 15`, evidence sha256 +`80f404ae85ef0b6a0d8056a90997e92fa1236f1ae68b75c1b2d3a6f182558e32`), with the +per-event `trail_points`. diff --git a/tests/fixtures/zero_trail_sibling_stop/bars.inc b/tests/fixtures/zero_trail_sibling_stop/bars.inc new file mode 100644 index 00000000..d556f283 --- /dev/null +++ b/tests/fixtures/zero_trail_sibling_stop/bars.inc @@ -0,0 +1,491 @@ +// Generated from the NYSE:F 15m registry feed +// (evidence sha256 80f404ae85ef0b6a0d8056a90997e92fa1236f1ae68b75c1b2d3a6f182558e32). +// Per event: the signal bar (market entry), the fill bar (strategy.exit with +// trail_offset 0 issued at its close, already reached there), the bar that opens ON +// the carried best, through the bar the 25-bar timeout close fills on. {ts, o, h, l, c} +constexpr double kZeroTrailLongPoints[] = {3.0, 3.0, 7.0, 3.0, 4.0, 4.0, 7.0, 4.0}; +constexpr FeedBar kZeroTrailLong[8][28] = { + { // signal 2025-05-06 15:45Z, fill 2025-05-06 16:00Z, trail_points 3, carried best 10.500 + {1746546300000LL, 10.535, 10.535, 10.46, 10.465}, + {1746547200000LL, 10.47, 10.505, 10.445, 10.5}, + {1746548100000LL, 10.5, 10.51, 10.445, 10.46}, + {1746549000000LL, 10.47, 10.49, 10.45, 10.45}, + {1746549900000LL, 10.455, 10.455, 10.415, 10.425}, + {1746550800000LL, 10.43, 10.47, 10.41, 10.465}, + {1746551700000LL, 10.46, 10.465, 10.42, 10.435}, + {1746552600000LL, 10.435, 10.47, 10.43, 10.44}, + {1746553500000LL, 10.44, 10.455, 10.43, 10.445}, + {1746554400000LL, 10.445, 10.465, 10.415, 10.425}, + {1746555300000LL, 10.425, 10.465, 10.42, 10.465}, + {1746556200000LL, 10.46, 10.48, 10.455, 10.47}, + {1746557100000LL, 10.47, 10.48, 10.45, 10.46}, + {1746558000000LL, 10.46, 10.47, 10.44, 10.45}, + {1746558900000LL, 10.45, 10.45, 10.425, 10.445}, + {1746559800000LL, 10.44, 10.45, 10.42, 10.445}, + {1746560700000LL, 10.45, 10.46, 10.41, 10.44}, + {1746624600000LL, 10.47, 10.5, 10.37, 10.405}, + {1746625500000LL, 10.405, 10.435, 10.385, 10.39}, + {1746626400000LL, 10.395, 10.42, 10.325, 10.325}, + {1746627300000LL, 10.33, 10.34, 10.3, 10.33}, + {1746628200000LL, 10.33, 10.33, 10.27, 10.275}, + {1746629100000LL, 10.275, 10.3, 10.265, 10.295}, + {1746630000000LL, 10.3, 10.33, 10.28, 10.305}, + {1746630900000LL, 10.305, 10.305, 10.275, 10.285}, + {1746631800000LL, 10.285, 10.305, 10.285, 10.3}, + {1746632700000LL, 10.295, 10.31, 10.285, 10.305}, + {1746633600000LL, 10.305, 10.33, 10.295, 10.315}, + }, + { // signal 2025-05-21 19:15Z, fill 2025-05-21 19:30Z, trail_points 3, carried best 10.510 + {1747854900000LL, 10.495, 10.5, 10.45, 10.475}, + {1747855800000LL, 10.48, 10.51, 10.475, 10.505}, + {1747856700000LL, 10.51, 10.52, 10.47, 10.475}, + {1747920600000LL, 10.48, 10.48, 10.355, 10.39}, + {1747921500000LL, 10.39, 10.39, 10.345, 10.365}, + {1747922400000LL, 10.365, 10.44, 10.365, 10.37}, + {1747923300000LL, 10.37, 10.39, 10.355, 10.365}, + {1747924200000LL, 10.37, 10.39, 10.365, 10.38}, + {1747925100000LL, 10.375, 10.415, 10.375, 10.415}, + {1747926000000LL, 10.415, 10.45, 10.41, 10.435}, + {1747926900000LL, 10.435, 10.445, 10.415, 10.415}, + {1747927800000LL, 10.415, 10.45, 10.415, 10.445}, + {1747928700000LL, 10.445, 10.45, 10.425, 10.44}, + {1747929600000LL, 10.44, 10.46, 10.43, 10.455}, + {1747930500000LL, 10.46, 10.47, 10.435, 10.435}, + {1747931400000LL, 10.435, 10.44, 10.41, 10.415}, + {1747932300000LL, 10.41, 10.425, 10.41, 10.425}, + {1747933200000LL, 10.425, 10.44, 10.42, 10.435}, + {1747934100000LL, 10.435, 10.465, 10.435, 10.46}, + {1747935000000LL, 10.455, 10.48, 10.44, 10.48}, + {1747935900000LL, 10.47, 10.485, 10.465, 10.465}, + {1747936800000LL, 10.465, 10.485, 10.46, 10.485}, + {1747937700000LL, 10.485, 10.5, 10.475, 10.475}, + {1747938600000LL, 10.475, 10.485, 10.45, 10.465}, + {1747939500000LL, 10.465, 10.48, 10.465, 10.475}, + {1747940400000LL, 10.48, 10.49, 10.48, 10.49}, + {1747941300000LL, 10.49, 10.495, 10.48, 10.495}, + {1747942200000LL, 10.5, 10.5, 10.45, 10.45}, + }, + { // signal 2025-05-30 13:45Z, fill 2025-05-30 14:00Z, trail_points 7, carried best 10.330 + {1748612700000LL, 10.205, 10.28, 10.205, 10.255}, + {1748613600000LL, 10.26, 10.34, 10.26, 10.33}, + {1748614500000LL, 10.33, 10.345, 10.285, 10.31}, + {1748615400000LL, 10.31, 10.38, 10.31, 10.36}, + {1748616300000LL, 10.355, 10.36, 10.33, 10.34}, + {1748617200000LL, 10.35, 10.38, 10.345, 10.37}, + {1748618100000LL, 10.37, 10.385, 10.345, 10.385}, + {1748619000000LL, 10.38, 10.405, 10.355, 10.36}, + {1748619900000LL, 10.36, 10.39, 10.355, 10.36}, + {1748620800000LL, 10.36, 10.39, 10.345, 10.385}, + {1748621700000LL, 10.39, 10.39, 10.345, 10.35}, + {1748622600000LL, 10.355, 10.37, 10.305, 10.35}, + {1748623500000LL, 10.345, 10.35, 10.32, 10.325}, + {1748624400000LL, 10.32, 10.35, 10.31, 10.325}, + {1748625300000LL, 10.325, 10.35, 10.325, 10.34}, + {1748626200000LL, 10.35, 10.36, 10.34, 10.34}, + {1748627100000LL, 10.335, 10.37, 10.335, 10.36}, + {1748628000000LL, 10.37, 10.41, 10.365, 10.395}, + {1748628900000LL, 10.39, 10.41, 10.38, 10.395}, + {1748629800000LL, 10.395, 10.4, 10.39, 10.395}, + {1748630700000LL, 10.395, 10.425, 10.39, 10.42}, + {1748631600000LL, 10.42, 10.45, 10.41, 10.44}, + {1748632500000LL, 10.435, 10.45, 10.42, 10.435}, + {1748633400000LL, 10.44, 10.46, 10.43, 10.43}, + {1748634300000LL, 10.43, 10.45, 10.36, 10.375}, + {1748871000000LL, 10.25, 10.3, 10.065, 10.115}, + {1748871900000LL, 10.12, 10.125, 10.04, 10.05}, + {1748872800000LL, 10.05, 10.05, 9.95, 9.96}, + }, + { // signal 2025-06-05 14:00Z, fill 2025-06-05 14:15Z, trail_points 3, carried best 10.155 + {1749132000000LL, 10.07, 10.12, 10.07, 10.115}, + {1749132900000LL, 10.12, 10.16, 10.1, 10.155}, + {1749133800000LL, 10.155, 10.185, 10.11, 10.17}, + {1749134700000LL, 10.17, 10.22, 10.16, 10.195}, + {1749135600000LL, 10.195, 10.22, 10.195, 10.205}, + {1749136500000LL, 10.21, 10.215, 10.185, 10.195}, + {1749137400000LL, 10.2, 10.2, 10.18, 10.2}, + {1749138300000LL, 10.19, 10.25, 10.185, 10.25}, + {1749139200000LL, 10.245, 10.25, 10.225, 10.235}, + {1749140100000LL, 10.235, 10.25, 10.205, 10.23}, + {1749141000000LL, 10.225, 10.225, 10.165, 10.165}, + {1749141900000LL, 10.16, 10.16, 10.11, 10.12}, + {1749142800000LL, 10.12, 10.135, 10.11, 10.12}, + {1749143700000LL, 10.12, 10.12, 10.1, 10.12}, + {1749144600000LL, 10.12, 10.13, 10.11, 10.125}, + {1749145500000LL, 10.125, 10.125, 10.1, 10.11}, + {1749146400000LL, 10.11, 10.115, 10.085, 10.095}, + {1749147300000LL, 10.095, 10.11, 10.06, 10.11}, + {1749148200000LL, 10.11, 10.13, 10.105, 10.12}, + {1749149100000LL, 10.13, 10.13, 10.11, 10.115}, + {1749150000000LL, 10.11, 10.13, 10.09, 10.09}, + {1749150900000LL, 10.095, 10.1, 10.04, 10.07}, + {1749151800000LL, 10.08, 10.13, 10.075, 10.1}, + {1749152700000LL, 10.095, 10.11, 10.07, 10.095}, + {1749216600000LL, 10.16, 10.21, 10.15, 10.185}, + {1749217500000LL, 10.185, 10.2, 10.16, 10.19}, + {1749218400000LL, 10.185, 10.21, 10.175, 10.205}, + {1749219300000LL, 10.205, 10.23, 10.2, 10.21}, + }, + { // signal 2025-06-09 13:30Z, fill 2025-06-09 13:45Z, trail_points 4, carried best 10.430 + {1749475800000LL, 10.3, 10.4, 10.29, 10.385}, + {1749476700000LL, 10.39, 10.43, 10.36, 10.425}, + {1749477600000LL, 10.43, 10.445, 10.405, 10.405}, + {1749478500000LL, 10.4, 10.41, 10.38, 10.4}, + {1749479400000LL, 10.405, 10.415, 10.385, 10.39}, + {1749480300000LL, 10.39, 10.39, 10.36, 10.37}, + {1749481200000LL, 10.37, 10.375, 10.35, 10.355}, + {1749482100000LL, 10.355, 10.37, 10.355, 10.365}, + {1749483000000LL, 10.365, 10.37, 10.35, 10.365}, + {1749483900000LL, 10.365, 10.37, 10.34, 10.35}, + {1749484800000LL, 10.345, 10.36, 10.335, 10.35}, + {1749485700000LL, 10.35, 10.35, 10.34, 10.34}, + {1749486600000LL, 10.345, 10.35, 10.34, 10.345}, + {1749487500000LL, 10.345, 10.37, 10.345, 10.355}, + {1749488400000LL, 10.355, 10.37, 10.355, 10.365}, + {1749489300000LL, 10.36, 10.375, 10.36, 10.37}, + {1749490200000LL, 10.375, 10.38, 10.36, 10.36}, + {1749491100000LL, 10.365, 10.38, 10.36, 10.37}, + {1749492000000LL, 10.37, 10.38, 10.365, 10.375}, + {1749492900000LL, 10.37, 10.38, 10.37, 10.375}, + {1749493800000LL, 10.375, 10.375, 10.36, 10.365}, + {1749494700000LL, 10.365, 10.365, 10.35, 10.36}, + {1749495600000LL, 10.355, 10.37, 10.345, 10.365}, + {1749496500000LL, 10.37, 10.37, 10.345, 10.355}, + {1749497400000LL, 10.355, 10.355, 10.34, 10.35}, + {1749498300000LL, 10.345, 10.36, 10.32, 10.335}, + {1749562200000LL, 10.375, 10.43, 10.37, 10.395}, + {1749563100000LL, 10.395, 10.46, 10.365, 10.455}, + }, + { // signal 2025-06-23 14:00Z, fill 2025-06-23 14:15Z, trail_points 4, carried best 10.690 + {1750687200000LL, 10.625, 10.66, 10.625, 10.64}, + {1750688100000LL, 10.65, 10.69, 10.65, 10.69}, + {1750689000000LL, 10.69, 10.71, 10.665, 10.675}, + {1750689900000LL, 10.68, 10.735, 10.68, 10.73}, + {1750690800000LL, 10.73, 10.75, 10.725, 10.74}, + {1750691700000LL, 10.74, 10.74, 10.715, 10.715}, + {1750692600000LL, 10.71, 10.72, 10.69, 10.695}, + {1750693500000LL, 10.695, 10.695, 10.67, 10.67}, + {1750694400000LL, 10.67, 10.67, 10.64, 10.64}, + {1750695300000LL, 10.65, 10.65, 10.605, 10.605}, + {1750696200000LL, 10.605, 10.61, 10.565, 10.61}, + {1750697100000LL, 10.61, 10.62, 10.565, 10.575}, + {1750698000000LL, 10.575, 10.62, 10.57, 10.615}, + {1750698900000LL, 10.62, 10.68, 10.615, 10.675}, + {1750699800000LL, 10.68, 10.72, 10.68, 10.715}, + {1750700700000LL, 10.72, 10.73, 10.715, 10.725}, + {1750701600000LL, 10.725, 10.725, 10.695, 10.715}, + {1750702500000LL, 10.715, 10.72, 10.71, 10.715}, + {1750703400000LL, 10.715, 10.72, 10.705, 10.705}, + {1750704300000LL, 10.71, 10.715, 10.69, 10.695}, + {1750705200000LL, 10.7, 10.72, 10.7, 10.715}, + {1750706100000LL, 10.715, 10.715, 10.7, 10.715}, + {1750707000000LL, 10.71, 10.711, 10.695, 10.705}, + {1750707900000LL, 10.705, 10.75, 10.695, 10.735}, + {1750771800000LL, 10.79, 10.845, 10.775, 10.845}, + {1750772700000LL, 10.845, 10.86, 10.78, 10.785}, + {1750773600000LL, 10.79, 10.85, 10.785, 10.835}, + {1750774500000LL, 10.835, 10.865, 10.82, 10.85}, + }, + { // signal 2025-07-22 15:00Z, fill 2025-07-22 15:15Z, trail_points 7, carried best 11.220 + {1753196400000LL, 11.125, 11.15, 11.11, 11.15}, + {1753197300000LL, 11.15, 11.215, 11.15, 11.215}, + {1753198200000LL, 11.22, 11.25, 11.18, 11.23}, + {1753199100000LL, 11.23, 11.26, 11.23, 11.26}, + {1753200000000LL, 11.25, 11.285, 11.25, 11.255}, + {1753200900000LL, 11.255, 11.26, 11.215, 11.23}, + {1753201800000LL, 11.23, 11.23, 11.155, 11.165}, + {1753202700000LL, 11.165, 11.19, 11.15, 11.165}, + {1753203600000LL, 11.165, 11.19, 11.16, 11.185}, + {1753204500000LL, 11.185, 11.225, 11.185, 11.225}, + {1753205400000LL, 11.225, 11.23, 11.2, 11.21}, + {1753206300000LL, 11.21, 11.22, 11.205, 11.215}, + {1753207200000LL, 11.22, 11.22, 11.18, 11.185}, + {1753208100000LL, 11.185, 11.21, 11.18, 11.195}, + {1753209000000LL, 11.195, 11.215, 11.19, 11.205}, + {1753209900000LL, 11.205, 11.245, 11.2, 11.22}, + {1753210800000LL, 11.22, 11.23, 11.215, 11.225}, + {1753211700000LL, 11.225, 11.225, 11.205, 11.205}, + {1753212600000LL, 11.205, 11.22, 11.2, 11.2}, + {1753213500000LL, 11.205, 11.21, 11.18, 11.185}, + {1753277400000LL, 11.27, 11.45, 11.27, 11.36}, + {1753278300000LL, 11.355, 11.41, 11.32, 11.33}, + {1753279200000LL, 11.33, 11.46, 11.33, 11.42}, + {1753280100000LL, 11.42, 11.43, 11.4, 11.425}, + {1753281000000LL, 11.425, 11.45, 11.42, 11.435}, + {1753281900000LL, 11.435, 11.445, 11.41, 11.415}, + {1753282800000LL, 11.415, 11.43, 11.39, 11.395}, + {1753283700000LL, 11.395, 11.395, 11.355, 11.36}, + }, + { // signal 2025-08-19 14:00Z, fill 2025-08-19 14:15Z, trail_points 4, carried best 11.710 + {1755612000000LL, 11.65, 11.68, 11.64, 11.67}, + {1755612900000LL, 11.67, 11.71, 11.67, 11.705}, + {1755613800000LL, 11.71, 11.72, 11.665, 11.675}, + {1755614700000LL, 11.675, 11.71, 11.635, 11.635}, + {1755615600000LL, 11.635, 11.64, 11.62, 11.62}, + {1755616500000LL, 11.625, 11.64, 11.6, 11.635}, + {1755617400000LL, 11.63, 11.645, 11.615, 11.615}, + {1755618300000LL, 11.615, 11.615, 11.56, 11.565}, + {1755619200000LL, 11.57, 11.58, 11.56, 11.57}, + {1755620100000LL, 11.565, 11.58, 11.555, 11.555}, + {1755621000000LL, 11.55, 11.57, 11.54, 11.555}, + {1755621900000LL, 11.555, 11.56, 11.535, 11.555}, + {1755622800000LL, 11.56, 11.56, 11.54, 11.555}, + {1755623700000LL, 11.555, 11.57, 11.55, 11.565}, + {1755624600000LL, 11.565, 11.58, 11.565, 11.575}, + {1755625500000LL, 11.575, 11.58, 11.555, 11.555}, + {1755626400000LL, 11.555, 11.555, 11.515, 11.525}, + {1755627300000LL, 11.525, 11.53, 11.51, 11.515}, + {1755628200000LL, 11.51, 11.52, 11.51, 11.52}, + {1755629100000LL, 11.52, 11.555, 11.52, 11.555}, + {1755630000000LL, 11.555, 11.56, 11.545, 11.555}, + {1755630900000LL, 11.555, 11.575, 11.545, 11.575}, + {1755631800000LL, 11.58, 11.595, 11.575, 11.585}, + {1755632700000LL, 11.585, 11.59, 11.57, 11.59}, + {1755696600000LL, 11.52, 11.58, 11.505, 11.565}, + {1755697500000LL, 11.565, 11.595, 11.515, 11.555}, + {1755698400000LL, 11.555, 11.585, 11.52, 11.52}, + {1755699300000LL, 11.525, 11.53, 11.485, 11.485}, + }, +}; +constexpr double kZeroTrailShortPoints[] = {4.0, 3.0, 1.0, 3.0, 8.0, 5.0, 2.0, 1.0}; +constexpr FeedBar kZeroTrailShort[8][28] = { + { // signal 2025-04-21 13:30Z, fill 2025-04-21 13:45Z, trail_points 4, carried best 9.500 + {1745242200000LL, 9.59, 9.625, 9.52, 9.535}, + {1745243100000LL, 9.54, 9.55, 9.5, 9.5}, + {1745244000000LL, 9.5, 9.55, 9.49, 9.54}, + {1745244900000LL, 9.54, 9.545, 9.49, 9.495}, + {1745245800000LL, 9.49, 9.49, 9.46, 9.465}, + {1745246700000LL, 9.465, 9.48, 9.415, 9.415}, + {1745247600000LL, 9.415, 9.42, 9.385, 9.39}, + {1745248500000LL, 9.395, 9.42, 9.395, 9.42}, + {1745249400000LL, 9.415, 9.415, 9.365, 9.385}, + {1745250300000LL, 9.39, 9.395, 9.36, 9.38}, + {1745251200000LL, 9.38, 9.41, 9.38, 9.41}, + {1745252100000LL, 9.415, 9.425, 9.39, 9.39}, + {1745253000000LL, 9.395, 9.43, 9.395, 9.425}, + {1745253900000LL, 9.425, 9.43, 9.38, 9.385}, + {1745254800000LL, 9.385, 9.4, 9.36, 9.36}, + {1745255700000LL, 9.365, 9.395, 9.35, 9.37}, + {1745256600000LL, 9.365, 9.405, 9.365, 9.39}, + {1745257500000LL, 9.395, 9.395, 9.365, 9.375}, + {1745258400000LL, 9.375, 9.39, 9.35, 9.365}, + {1745259300000LL, 9.36, 9.39, 9.35, 9.375}, + {1745260200000LL, 9.38, 9.405, 9.37, 9.37}, + {1745261100000LL, 9.375, 9.38, 9.36, 9.37}, + {1745262000000LL, 9.375, 9.4, 9.375, 9.395}, + {1745262900000LL, 9.395, 9.405, 9.375, 9.39}, + {1745263800000LL, 9.39, 9.45, 9.39, 9.445}, + {1745264700000LL, 9.445, 9.49, 9.44, 9.47}, + {1745328600000LL, 9.55, 9.635, 9.53, 9.605}, + {1745329500000LL, 9.605, 9.63, 9.59, 9.59}, + }, + { // signal 2025-04-30 16:00Z, fill 2025-04-30 16:15Z, trail_points 3, carried best 9.900 + {1746028800000LL, 9.94, 9.95, 9.925, 9.925}, + {1746029700000LL, 9.93, 9.94, 9.9, 9.905}, + {1746030600000LL, 9.9, 9.92, 9.89, 9.905}, + {1746031500000LL, 9.91, 9.93, 9.9, 9.92}, + {1746032400000LL, 9.915, 9.93, 9.865, 9.89}, + {1746033300000LL, 9.885, 9.93, 9.885, 9.93}, + {1746034200000LL, 9.925, 9.95, 9.925, 9.945}, + {1746035100000LL, 9.94, 9.95, 9.92, 9.935}, + {1746036000000LL, 9.935, 9.955, 9.9, 9.935}, + {1746036900000LL, 9.93, 9.95, 9.92, 9.93}, + {1746037800000LL, 9.93, 9.945, 9.905, 9.915}, + {1746038700000LL, 9.915, 9.94, 9.91, 9.915}, + {1746039600000LL, 9.92, 9.93, 9.91, 9.93}, + {1746040500000LL, 9.93, 9.94, 9.91, 9.91}, + {1746041400000LL, 9.915, 9.945, 9.91, 9.945}, + {1746042300000LL, 9.945, 10.04, 9.945, 9.99}, + {1746106200000LL, 10.15, 10.23, 10.025, 10.07}, + {1746107100000LL, 10.07, 10.12, 10.05, 10.1}, + {1746108000000LL, 10.095, 10.245, 10.095, 10.225}, + {1746108900000LL, 10.225, 10.245, 10.22, 10.24}, + {1746109800000LL, 10.235, 10.255, 10.195, 10.245}, + {1746110700000LL, 10.245, 10.3, 10.245, 10.285}, + {1746111600000LL, 10.28, 10.315, 10.26, 10.31}, + {1746112500000LL, 10.31, 10.32, 10.27, 10.28}, + {1746113400000LL, 10.28, 10.29, 10.25, 10.25}, + {1746114300000LL, 10.25, 10.25, 10.21, 10.22}, + {1746115200000LL, 10.215, 10.235, 10.195, 10.225}, + {1746116100000LL, 10.22, 10.225, 10.19, 10.195}, + }, + { // signal 2025-05-16 18:30Z, fill 2025-05-16 18:45Z, trail_points 1, carried best 10.785 + {1747420200000LL, 10.785, 10.8, 10.78, 10.795}, + {1747421100000LL, 10.8, 10.81, 10.78, 10.785}, + {1747422000000LL, 10.785, 10.8, 10.775, 10.775}, + {1747422900000LL, 10.775, 10.785, 10.765, 10.765}, + {1747423800000LL, 10.765, 10.79, 10.76, 10.78}, + {1747424700000LL, 10.785, 10.8, 10.77, 10.8}, + {1747661400000LL, 10.69, 10.705, 10.64, 10.64}, + {1747662300000LL, 10.645, 10.67, 10.63, 10.665}, + {1747663200000LL, 10.67, 10.69, 10.66, 10.685}, + {1747664100000LL, 10.685, 10.71, 10.68, 10.705}, + {1747665000000LL, 10.71, 10.75, 10.71, 10.735}, + {1747665900000LL, 10.735, 10.74, 10.725, 10.735}, + {1747666800000LL, 10.735, 10.75, 10.735, 10.745}, + {1747667700000LL, 10.745, 10.75, 10.73, 10.735}, + {1747668600000LL, 10.735, 10.76, 10.73, 10.755}, + {1747669500000LL, 10.755, 10.76, 10.74, 10.755}, + {1747670400000LL, 10.755, 10.77, 10.75, 10.755}, + {1747671300000LL, 10.755, 10.76, 10.74, 10.745}, + {1747672200000LL, 10.745, 10.76, 10.74, 10.755}, + {1747673100000LL, 10.755, 10.78, 10.755, 10.775}, + {1747674000000LL, 10.775, 10.775, 10.76, 10.765}, + {1747674900000LL, 10.76, 10.775, 10.755, 10.775}, + {1747675800000LL, 10.775, 10.78, 10.77, 10.775}, + {1747676700000LL, 10.775, 10.775, 10.755, 10.765}, + {1747677600000LL, 10.765, 10.765, 10.735, 10.735}, + {1747678500000LL, 10.735, 10.75, 10.72, 10.745}, + {1747679400000LL, 10.745, 10.77, 10.745, 10.77}, + {1747680300000LL, 10.765, 10.765, 10.74, 10.745}, + }, + { // signal 2025-06-17 14:30Z, fill 2025-06-17 14:45Z, trail_points 3, carried best 10.520 + {1750170600000LL, 10.565, 10.58, 10.55, 10.55}, + {1750171500000LL, 10.55, 10.56, 10.52, 10.52}, + {1750172400000LL, 10.52, 10.54, 10.51, 10.525}, + {1750173300000LL, 10.525, 10.525, 10.5, 10.5}, + {1750174200000LL, 10.5, 10.5, 10.47, 10.49}, + {1750175100000LL, 10.49, 10.505, 10.485, 10.495}, + {1750176000000LL, 10.495, 10.53, 10.49, 10.525}, + {1750176900000LL, 10.525, 10.525, 10.51, 10.515}, + {1750177800000LL, 10.51, 10.515, 10.5, 10.505}, + {1750178700000LL, 10.51, 10.52, 10.485, 10.49}, + {1750179600000LL, 10.485, 10.515, 10.485, 10.505}, + {1750180500000LL, 10.505, 10.505, 10.47, 10.475}, + {1750181400000LL, 10.47, 10.5, 10.465, 10.49}, + {1750182300000LL, 10.49, 10.49, 10.455, 10.47}, + {1750183200000LL, 10.465, 10.465, 10.44, 10.44}, + {1750184100000LL, 10.445, 10.445, 10.41, 10.42}, + {1750185000000LL, 10.43, 10.43, 10.405, 10.415}, + {1750185900000LL, 10.415, 10.425, 10.405, 10.415}, + {1750186800000LL, 10.415, 10.415, 10.39, 10.39}, + {1750187700000LL, 10.395, 10.43, 10.385, 10.42}, + {1750188600000LL, 10.425, 10.43, 10.41, 10.415}, + {1750189500000LL, 10.415, 10.44, 10.41, 10.425}, + {1750253400000LL, 10.43, 10.48, 10.4, 10.44}, + {1750254300000LL, 10.44, 10.5, 10.435, 10.495}, + {1750255200000LL, 10.495, 10.525, 10.485, 10.485}, + {1750256100000LL, 10.48, 10.49, 10.47, 10.48}, + {1750257000000LL, 10.48, 10.495, 10.47, 10.48}, + {1750257900000LL, 10.475, 10.49, 10.46, 10.47}, + }, + { // signal 2025-07-01 16:30Z, fill 2025-07-01 16:45Z, trail_points 8, carried best 11.210 + {1751387400000LL, 11.375, 11.38, 11.295, 11.295}, + {1751388300000LL, 11.29, 11.3, 11.21, 11.21}, + {1751389200000LL, 11.21, 11.25, 11.2, 11.245}, + {1751390100000LL, 11.25, 11.29, 11.245, 11.275}, + {1751391000000LL, 11.29, 11.315, 11.28, 11.305}, + {1751391900000LL, 11.305, 11.31, 11.29, 11.29}, + {1751392800000LL, 11.29, 11.29, 11.27, 11.28}, + {1751393700000LL, 11.275, 11.31, 11.27, 11.305}, + {1751394600000LL, 11.31, 11.32, 11.3, 11.305}, + {1751395500000LL, 11.31, 11.36, 11.305, 11.35}, + {1751396400000LL, 11.345, 11.36, 11.33, 11.345}, + {1751397300000LL, 11.345, 11.35, 11.32, 11.35}, + {1751398200000LL, 11.35, 11.37, 11.31, 11.32}, + {1751399100000LL, 11.32, 11.35, 11.31, 11.345}, + {1751463000000LL, 11.36, 11.51, 11.36, 11.445}, + {1751463900000LL, 11.45, 11.455, 11.41, 11.445}, + {1751464800000LL, 11.45, 11.48, 11.41, 11.45}, + {1751465700000LL, 11.45, 11.485, 11.425, 11.465}, + {1751466600000LL, 11.465, 11.52, 11.46, 11.475}, + {1751467500000LL, 11.48, 11.515, 11.44, 11.505}, + {1751468400000LL, 11.5, 11.58, 11.495, 11.575}, + {1751469300000LL, 11.575, 11.665, 11.575, 11.655}, + {1751470200000LL, 11.655, 11.695, 11.645, 11.655}, + {1751471100000LL, 11.655, 11.665, 11.63, 11.665}, + {1751472000000LL, 11.665, 11.67, 11.65, 11.66}, + {1751472900000LL, 11.665, 11.68, 11.65, 11.655}, + {1751473800000LL, 11.655, 11.67, 11.645, 11.665}, + {1751474700000LL, 11.67, 11.68, 11.655, 11.655}, + }, + { // signal 2025-07-15 13:30Z, fill 2025-07-15 13:45Z, trail_points 5, carried best 11.755 + {1752586200000LL, 11.9, 11.925, 11.81, 11.81}, + {1752587100000LL, 11.81, 11.83, 11.75, 11.755}, + {1752588000000LL, 11.755, 11.785, 11.745, 11.755}, + {1752588900000LL, 11.755, 11.79, 11.72, 11.72}, + {1752589800000LL, 11.72, 11.75, 11.71, 11.745}, + {1752590700000LL, 11.745, 11.755, 11.71, 11.715}, + {1752591600000LL, 11.715, 11.73, 11.695, 11.705}, + {1752592500000LL, 11.705, 11.745, 11.695, 11.735}, + {1752593400000LL, 11.73, 11.75, 11.715, 11.72}, + {1752594300000LL, 11.71, 11.735, 11.71, 11.715}, + {1752595200000LL, 11.715, 11.73, 11.68, 11.685}, + {1752596100000LL, 11.685, 11.7, 11.64, 11.645}, + {1752597000000LL, 11.645, 11.66, 11.625, 11.655}, + {1752597900000LL, 11.65, 11.665, 11.605, 11.61}, + {1752598800000LL, 11.615, 11.615, 11.59, 11.59}, + {1752599700000LL, 11.595, 11.615, 11.575, 11.615}, + {1752600600000LL, 11.62, 11.62, 11.58, 11.58}, + {1752601500000LL, 11.58, 11.585, 11.55, 11.585}, + {1752602400000LL, 11.58, 11.595, 11.56, 11.58}, + {1752603300000LL, 11.58, 11.6, 11.57, 11.6}, + {1752604200000LL, 11.595, 11.6, 11.57, 11.58}, + {1752605100000LL, 11.575, 11.58, 11.56, 11.565}, + {1752606000000LL, 11.565, 11.58, 11.56, 11.565}, + {1752606900000LL, 11.56, 11.565, 11.54, 11.545}, + {1752607800000LL, 11.54, 11.58, 11.525, 11.575}, + {1752608700000LL, 11.575, 11.585, 11.555, 11.575}, + {1752672600000LL, 11.355, 11.45, 11.335, 11.365}, + {1752673500000LL, 11.36, 11.36, 11.305, 11.325}, + }, + { // signal 2025-07-17 14:45Z, fill 2025-07-17 15:00Z, trail_points 2, carried best 11.145 + {1752763500000LL, 11.155, 11.17, 11.125, 11.165}, + {1752764400000LL, 11.17, 11.175, 11.14, 11.145}, + {1752765300000LL, 11.145, 11.17, 11.125, 11.155}, + {1752766200000LL, 11.155, 11.175, 11.145, 11.175}, + {1752767100000LL, 11.175, 11.175, 11.145, 11.165}, + {1752768000000LL, 11.165, 11.18, 11.155, 11.165}, + {1752768900000LL, 11.165, 11.17, 11.15, 11.165}, + {1752769800000LL, 11.165, 11.18, 11.165, 11.175}, + {1752770700000LL, 11.175, 11.175, 11.15, 11.16}, + {1752771600000LL, 11.155, 11.175, 11.155, 11.16}, + {1752772500000LL, 11.165, 11.17, 11.15, 11.155}, + {1752773400000LL, 11.155, 11.16, 11.11, 11.135}, + {1752774300000LL, 11.135, 11.135, 11.12, 11.13}, + {1752775200000LL, 11.13, 11.165, 11.12, 11.165}, + {1752776100000LL, 11.165, 11.2, 11.16, 11.195}, + {1752777000000LL, 11.195, 11.195, 11.18, 11.18}, + {1752777900000LL, 11.185, 11.185, 11.17, 11.175}, + {1752778800000LL, 11.175, 11.175, 11.16, 11.165}, + {1752779700000LL, 11.165, 11.17, 11.15, 11.155}, + {1752780600000LL, 11.155, 11.17, 11.15, 11.155}, + {1752781500000LL, 11.155, 11.18, 11.14, 11.165}, + {1752845400000LL, 11.18, 11.23, 11.16, 11.205}, + {1752846300000LL, 11.2, 11.23, 11.19, 11.2}, + {1752847200000LL, 11.2, 11.22, 11.15, 11.155}, + {1752848100000LL, 11.155, 11.17, 11.1, 11.105}, + {1752849000000LL, 11.105, 11.14, 11.105, 11.105}, + {1752849900000LL, 11.105, 11.15, 11.105, 11.135}, + {1752850800000LL, 11.135, 11.18, 11.105, 11.11}, + }, + { // signal 2025-07-25 16:00Z, fill 2025-07-25 16:15Z, trail_points 1, carried best 11.280 + {1753459200000LL, 11.285, 11.3, 11.28, 11.285}, + {1753460100000LL, 11.29, 11.29, 11.275, 11.28}, + {1753461000000LL, 11.28, 11.325, 11.27, 11.325}, + {1753461900000LL, 11.325, 11.33, 11.32, 11.325}, + {1753462800000LL, 11.325, 11.35, 11.32, 11.35}, + {1753463700000LL, 11.35, 11.37, 11.325, 11.36}, + {1753464600000LL, 11.355, 11.38, 11.355, 11.38}, + {1753465500000LL, 11.38, 11.39, 11.375, 11.39}, + {1753466400000LL, 11.39, 11.39, 11.38, 11.39}, + {1753467300000LL, 11.39, 11.4, 11.385, 11.4}, + {1753468200000LL, 11.395, 11.4, 11.39, 11.395}, + {1753469100000LL, 11.395, 11.42, 11.38, 11.415}, + {1753470000000LL, 11.415, 11.48, 11.415, 11.475}, + {1753470900000LL, 11.475, 11.48, 11.44, 11.445}, + {1753471800000LL, 11.445, 11.46, 11.44, 11.455}, + {1753472700000LL, 11.455, 11.49, 11.45, 11.465}, + {1753709400000LL, 11.48, 11.48, 11.37, 11.4}, + {1753710300000LL, 11.405, 11.41, 11.33, 11.34}, + {1753711200000LL, 11.34, 11.38, 11.33, 11.375}, + {1753712100000LL, 11.375, 11.375, 11.355, 11.365}, + {1753713000000LL, 11.365, 11.385, 11.36, 11.375}, + {1753713900000LL, 11.375, 11.38, 11.36, 11.365}, + {1753714800000LL, 11.365, 11.385, 11.36, 11.365}, + {1753715700000LL, 11.365, 11.375, 11.34, 11.345}, + {1753716600000LL, 11.345, 11.355, 11.32, 11.325}, + {1753717500000LL, 11.325, 11.35, 11.325, 11.34}, + {1753718400000LL, 11.345, 11.37, 11.34, 11.365}, + {1753719300000LL, 11.365, 11.38, 11.345, 11.345}, + }, +}; diff --git a/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/meta.json b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/meta.json new file mode 100644 index 00000000..8df881dd --- /dev/null +++ b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-MEASURE M16 NYSE:F long zero-offset trail, next open ON the carried best", + "slug": "hm-orders-m16-f-long-zero-trail-open-on-best", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:09:40.942Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2025-09-30" + }, + "pine_sha256": "cc7753b42850a0a1a98d492190d41687f4c881b450f06e0d54aecf29909bccba", + "tv_trades_sha256": "4a5bbbd341a7565d7d99a593b20d389b000bdcf3a55960f4c5e19f0a8a7eb5c7", + "tv_trades_rows": 16 +} diff --git a/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/metrics.json b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/metrics.json new file mode 100644 index 00000000..f74b930b --- /dev/null +++ b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-orders-m16-f-long-zero-trail-open-on-best", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2025-09-30", + "deepBacktesting": true, + "trades": 8, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "cc7753b42850a0a1a98d492190d41687f4c881b450f06e0d54aecf29909bccba", + "tvTradesCsvHash": "4a5bbbd341a7565d7d99a593b20d389b000bdcf3a55960f4c5e19f0a8a7eb5c7", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74413", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1759190400000 + }, + "returnedRange": { + "from": 1743514200000, + "to": 1759175100000 + }, + "rangeProof": "covered", + "rawFrameSha256": "fd7c6301e53a2ecf47734779f872030d390705b9cfb842a5e09d4d72a2c32499", + "decodedReportSha256": "9dd3ab604d5a10ff1558caeb1a8ab42a8d39c221480ec2524d668cf93b5ae255", + "closedTrades": 8, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:09:40.941Z" +} diff --git a/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/strategy.pine b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/strategy.pine new file mode 100644 index 00000000..35086d8c --- /dev/null +++ b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/strategy.pine @@ -0,0 +1,54 @@ +// H-MEASURE M16 NYSE:F long zero-offset trail, next open ON the carried best (H-MEASURE research, group orders, row M16 / G2-18). +// strategy.exit(trail_points=n, trail_offset=0) is issued at the close of the bar +// whose open filled the entry, and that close has ALREADY reached the activation +// (on its tick). The next bar opens exactly ON the carried best (the close, or the +// activation when the close only reaches it on its tick) and moves favourably +// first to a new extreme. A touch at the open exits there at the carried best; a +// zero-distance ride exits only after the new extreme. Events (UTC): +// 2025-05-06 15:45 -> fill 2025-05-06 16:00 trail_points 3 carried best 10.500 +// 2025-05-21 19:15 -> fill 2025-05-21 19:30 trail_points 3 carried best 10.510 +// 2025-05-30 13:45 -> fill 2025-05-30 14:00 trail_points 7 carried best 10.330 +// 2025-06-05 14:00 -> fill 2025-06-05 14:15 trail_points 3 carried best 10.155 +// 2025-06-09 13:30 -> fill 2025-06-09 13:45 trail_points 4 carried best 10.430 +// 2025-06-23 14:00 -> fill 2025-06-23 14:15 trail_points 4 carried best 10.690 +// 2025-07-22 15:00 -> fill 2025-07-22 15:15 trail_points 7 carried best 11.220 +// 2025-08-19 14:00 -> fill 2025-08-19 14:15 trail_points 4 carried best 11.710 +//@version=6 +strategy("H-MEASURE M16 NYSE:F long zero-offset trail, next open ON the carried best", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +var float pts = na +if strategy.position_size == 0 + if time == timestamp("UTC", 2025, 5, 6, 15, 45) + pts := 3 + strategy.entry("L", strategy.long) + else if time == timestamp("UTC", 2025, 5, 21, 19, 15) + pts := 3 + strategy.entry("L", strategy.long) + else if time == timestamp("UTC", 2025, 5, 30, 13, 45) + pts := 7 + strategy.entry("L", strategy.long) + else if time == timestamp("UTC", 2025, 6, 5, 14, 0) + pts := 3 + strategy.entry("L", strategy.long) + else if time == timestamp("UTC", 2025, 6, 9, 13, 30) + pts := 4 + strategy.entry("L", strategy.long) + else if time == timestamp("UTC", 2025, 6, 23, 14, 0) + pts := 4 + strategy.entry("L", strategy.long) + else if time == timestamp("UTC", 2025, 7, 22, 15, 0) + pts := 7 + strategy.entry("L", strategy.long) + else if time == timestamp("UTC", 2025, 8, 19, 14, 0) + pts := 4 + strategy.entry("L", strategy.long) + +// The exit is issued once, at the close of the bar whose open filled the entry. +if strategy.position_size > 0 and strategy.opentrades.entry_bar_index(0) == bar_index + strategy.exit("LX", "L", trail_points=pts, trail_offset=0) + +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 25 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/tv_trades.csv b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/tv_trades.csv new file mode 100644 index 00000000..6769567e --- /dev/null +++ b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-long-zero-trail-open-on-best/tv_trades.csv @@ -0,0 +1,17 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-05-07 00:15,LX,10.5,100,1047,3,0.29,0,4,0.38,-2,-0.19,3,0.00,1 +1,Entry long,2025-05-07 00:00,L,10.47,100,1047,3,0.29,0,4,0.38,-2,-0.19,3,0.00,1 +2,Exit long,2025-05-22 03:45,LX,10.51,100,1048,3,0.29,0,3,0.29,0,0.00,6,0.01,1 +2,Entry long,2025-05-22 03:30,L,10.48,100,1048,3,0.29,0,3,0.29,0,0.00,6,0.01,1 +3,Exit long,2025-05-30 22:15,LX,10.33,100,1026,7,0.68,0,8,0.78,0,0.00,13,0.01,1 +3,Entry long,2025-05-30 22:00,L,10.26,100,1026,7,0.68,0,8,0.78,0,0.00,13,0.01,1 +4,Exit long,2025-06-05 22:30,LX,10.15,100,1012,3,0.30,0,4,0.40,-2,-0.20,16,0.02,1 +4,Entry long,2025-06-05 22:15,L,10.12,100,1012,3,0.30,0,4,0.40,-2,-0.20,16,0.02,1 +5,Exit long,2025-06-09 22:00,LX,10.43,100,1039,4,0.38,0,4,0.38,-3,-0.29,20,0.02,1 +5,Entry long,2025-06-09 21:45,L,10.39,100,1039,4,0.38,0,4,0.38,-3,-0.29,20,0.02,1 +6,Exit long,2025-06-23 22:30,LX,10.69,100,1065,4,0.38,0,4,0.38,0,0.00,24,0.02,1 +6,Entry long,2025-06-23 22:15,L,10.65,100,1065,4,0.38,0,4,0.38,0,0.00,24,0.02,1 +7,Exit long,2025-07-22 23:30,LX,11.22,100,1115,7,0.63,0,7,0.63,0,0.00,31,0.03,1 +7,Entry long,2025-07-22 23:15,L,11.15,100,1115,7,0.63,0,7,0.63,0,0.00,31,0.03,1 +8,Exit long,2025-08-19 22:30,LX,11.71,100,1167,4,0.34,0,4,0.34,0,0.00,35,0.03,1 +8,Entry long,2025-08-19 22:15,L,11.67,100,1167,4,0.34,0,4,0.34,0,0.00,35,0.03,1 \ No newline at end of file diff --git a/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/meta.json b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/meta.json new file mode 100644 index 00000000..599ca720 --- /dev/null +++ b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-MEASURE M16 NYSE:F short zero-offset trail, next open ON the carried best", + "slug": "hm-orders-m16-f-short-zero-trail-open-on-best", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:09:45.263Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2025-09-30" + }, + "pine_sha256": "72e16bac83c429907a0d130393736aeae74103f6d2d10fa1dd026c2608d3e09a", + "tv_trades_sha256": "2e1f165d18468d7904ef034cae12f06a7491467236811d45257750a408fafbe4", + "tv_trades_rows": 16 +} diff --git a/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/metrics.json b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/metrics.json new file mode 100644 index 00000000..202c558e --- /dev/null +++ b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-orders-m16-f-short-zero-trail-open-on-best", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2025-09-30", + "deepBacktesting": true, + "trades": 8, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "72e16bac83c429907a0d130393736aeae74103f6d2d10fa1dd026c2608d3e09a", + "tvTradesCsvHash": "2e1f165d18468d7904ef034cae12f06a7491467236811d45257750a408fafbe4", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74414", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1759190400000 + }, + "returnedRange": { + "from": 1743514200000, + "to": 1759175100000 + }, + "rangeProof": "covered", + "rawFrameSha256": "7ddadf378b6d6c2fb87fa7f13698dbd7b2a9c5ad64b04ca114e2f6f89251acb0", + "decodedReportSha256": "0a3ad217eb8d46011f0e2e637e1b86fd0b0cdc46a739d536c8b52a32e3dbac92", + "closedTrades": 8, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:09:45.263Z" +} diff --git a/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/strategy.pine b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/strategy.pine new file mode 100644 index 00000000..fa27a697 --- /dev/null +++ b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/strategy.pine @@ -0,0 +1,54 @@ +// H-MEASURE M16 NYSE:F short zero-offset trail, next open ON the carried best (H-MEASURE research, group orders, row M16 / G2-18). +// strategy.exit(trail_points=n, trail_offset=0) is issued at the close of the bar +// whose open filled the entry, and that close has ALREADY reached the activation +// (on its tick). The next bar opens exactly ON the carried best (the close, or the +// activation when the close only reaches it on its tick) and moves favourably +// first to a new extreme. A touch at the open exits there at the carried best; a +// zero-distance ride exits only after the new extreme. Events (UTC): +// 2025-04-21 13:30 -> fill 2025-04-21 13:45 trail_points 4 carried best 9.500 +// 2025-04-30 16:00 -> fill 2025-04-30 16:15 trail_points 3 carried best 9.900 +// 2025-05-16 18:30 -> fill 2025-05-16 18:45 trail_points 1 carried best 10.785 +// 2025-06-17 14:30 -> fill 2025-06-17 14:45 trail_points 3 carried best 10.520 +// 2025-07-01 16:30 -> fill 2025-07-01 16:45 trail_points 8 carried best 11.210 +// 2025-07-15 13:30 -> fill 2025-07-15 13:45 trail_points 5 carried best 11.755 +// 2025-07-17 14:45 -> fill 2025-07-17 15:00 trail_points 2 carried best 11.145 +// 2025-07-25 16:00 -> fill 2025-07-25 16:15 trail_points 1 carried best 11.280 +//@version=6 +strategy("H-MEASURE M16 NYSE:F short zero-offset trail, next open ON the carried best", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +var float pts = na +if strategy.position_size == 0 + if time == timestamp("UTC", 2025, 4, 21, 13, 30) + pts := 4 + strategy.entry("S", strategy.short) + else if time == timestamp("UTC", 2025, 4, 30, 16, 0) + pts := 3 + strategy.entry("S", strategy.short) + else if time == timestamp("UTC", 2025, 5, 16, 18, 30) + pts := 1 + strategy.entry("S", strategy.short) + else if time == timestamp("UTC", 2025, 6, 17, 14, 30) + pts := 3 + strategy.entry("S", strategy.short) + else if time == timestamp("UTC", 2025, 7, 1, 16, 30) + pts := 8 + strategy.entry("S", strategy.short) + else if time == timestamp("UTC", 2025, 7, 15, 13, 30) + pts := 5 + strategy.entry("S", strategy.short) + else if time == timestamp("UTC", 2025, 7, 17, 14, 45) + pts := 2 + strategy.entry("S", strategy.short) + else if time == timestamp("UTC", 2025, 7, 25, 16, 0) + pts := 1 + strategy.entry("S", strategy.short) + +// The exit is issued once, at the close of the bar whose open filled the entry. +if strategy.position_size < 0 and strategy.opentrades.entry_bar_index(0) == bar_index + strategy.exit("SX", "S", trail_points=pts, trail_offset=0) + +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 25 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/tv_trades.csv b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/tv_trades.csv new file mode 100644 index 00000000..eee09786 --- /dev/null +++ b/tests/fixtures/zero_trail_sibling_stop/hm-orders-m16-f-short-zero-trail-open-on-best/tv_trades.csv @@ -0,0 +1,17 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-04-21 22:00,SX,9.5,100,954,4,0.42,0,4,0.42,-1,-0.10,4,0.00,1 +1,Entry short,2025-04-21 21:45,S,9.54,100,954,4,0.42,0,4,0.42,-1,-0.10,4,0.00,1 +2,Exit short,2025-05-01 00:30,SX,9.9,100,993,3,0.30,0,3,0.30,-1,-0.10,7,0.01,1 +2,Entry short,2025-05-01 00:15,S,9.93,100,993,3,0.30,0,3,0.30,-1,-0.10,7,0.01,1 +3,Exit short,2025-05-17 03:00,SX,10.79,100,1080,1,0.09,0,2,0.19,-1,-0.09,8,0.01,1 +3,Entry short,2025-05-17 02:45,S,10.8,100,1080,1,0.09,0,2,0.19,-1,-0.09,8,0.01,1 +4,Exit short,2025-06-17 23:00,SX,10.52,100,1055,3,0.28,0,3,0.28,-1,-0.09,11,0.01,1 +4,Entry short,2025-06-17 22:45,S,10.55,100,1055,3,0.28,0,3,0.28,-1,-0.09,11,0.01,1 +5,Exit short,2025-07-02 01:00,SX,11.21,100,1129,8,0.71,0,8,0.71,-1,-0.09,19,0.02,1 +5,Entry short,2025-07-02 00:45,S,11.29,100,1129,8,0.71,0,8,0.71,-1,-0.09,19,0.02,1 +6,Exit short,2025-07-15 22:00,SX,11.76,100,1181,5,0.42,0,6,0.51,-2,-0.17,24,0.02,1 +6,Entry short,2025-07-15 21:45,S,11.81,100,1181,5,0.42,0,6,0.51,-2,-0.17,24,0.02,1 +7,Exit short,2025-07-17 23:15,SX,11.15,100,1117,2,0.18,0,3,0.27,-1,-0.09,26,0.03,1 +7,Entry short,2025-07-17 23:00,S,11.17,100,1117,2,0.18,0,3,0.27,-1,-0.09,26,0.03,1 +8,Exit short,2025-07-26 00:30,SX,11.28,100,1129,1,0.09,0,1,0.09,0,0.00,27,0.03,1 +8,Entry short,2025-07-26 00:15,S,11.29,100,1129,1,0.09,0,1,0.09,0,0.00,27,0.03,1 \ No newline at end of file diff --git a/tests/test_pending_entry_trail_tapes.cpp b/tests/test_pending_entry_trail_tapes.cpp new file mode 100644 index 00000000..eee5f4be --- /dev/null +++ b/tests/test_pending_entry_trail_tapes.cpp @@ -0,0 +1,412 @@ +/* + * test_pending_entry_trail_tapes.cpp — H-MEASURE (rows E5 pending-entry arm + * and E14 anchored-leg seed). + * + * Every tape here issues strategy.exit(trail_points=n, trail_offset=k) on the + * SIGNAL bar, together with the MARKET strategy.entry it names, so the exit + * is placed while its entry is still pending; the entry fills at the next + * bar's open. The Pine adapter lowers such an exit to the kernel's anchored + * child (relative_leg_shapes: a Trail whose arm threshold is FromOwnerFill, + * materialized at the parent's fill through resolve_anchored_level) and adopts + * that child at the fill point. None of the 23 E5 / E9 / E14 tapes measured + * this shape: every one of them issued its exit once the entry had filled. + * The tapes are `lab tv` exports on NYSE:F 15m (sub-cent prints), + * tests/fixtures/pending_entry_trail (README.md there lists them). + * + * 1. The arm (lane E5's pending-entry claim). On every event the bar that arms + * the trail has a raw extreme strictly inside the activation's tick cell + * (two more tapes issue a LIMIT entry instead, which fills mid-bar one or + * two bars later; seven of their twelve arms land on the rest of that bar): + * its tick IS the activation, the raw price is not. TradingView exits on + * that bar at activation -/+ 1 tick, 28 of 28 (market entries 8 + 8, 11 of + * them on the fill bar itself; limit entries 6 + 6), so the pending-entry trail arms on the + * tick-quantized path exactly as lane E5 measured for a filled entry. The + * adapter's anchored child arms there too (source_trail_arm_level in + * resolve_anchored_level) and books TradingView's bar and price, 16 of 16. + * + * 2. The running best (lane E14's open measurement). Same shape, but the prints + * after the arm land between the two stops a best can give: TradingView's + * exit is the stop of a best that starts AT the activation (13 of 13), the + * reading lane E14 measured for a filled entry. The adapter's anchored child + * names no Trail::best_seed, so its best starts at the raw arm print, half a + * tick or less short of the activation, and its stop stays out of reach: + * the engine exits 1 to 24 bars later, 13 of 13. These are RECORDED + * divergences (the E14 protocol): each row asserts the engine DIFFERS from + * TradingView at exactly the bar and price listed, so the row fails loudly + * the day an anchored seed lands and the mask has to go, deliberately. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; + +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +#ifndef PINEFORGE_PENDING_TRAIL_FIXTURE_DIR +#error "PINEFORGE_PENDING_TRAIL_FIXTURE_DIR must name tests/fixtures/pending_entry_trail" +#endif + +namespace { + +constexpr double kNaN = std::numeric_limits::quiet_NaN(); +constexpr double kTick = 0.01; +constexpr int kTimeout = 25; // bars after the fill bar, then strategy.close + +struct FeedBar { + std::int64_t ts; + double open, high, low, close; +}; + +#include "fixtures/pending_entry_trail/bars.inc" + +// The engine's exit on a recorded divergence: bars after the fill bar, price. +struct EngineExit { + int bars_after_fill; + double price; +}; + +enum class Question { Arm, RunningBest }; + +constexpr double kOffsetOne[] = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}; + +struct Probe { + const char* slug; + bool is_long; + const double* trail_points; + const double* trail_offsets; + const double* limit_levels; // nullptr: a MARKET entry + const FeedBar* bars; + int entries; + int bars_per_entry; // the signal bar .. past the timeout close's fill + Question question; + // Entries whose engine exit is a RECORDED divergence, bit per entry index, + // with the engine's exit for each (nullptr: none). + unsigned recorded_divergences; + const EngineExit* engine_exits; +}; + +// Row 3's measured divergences (the adapter's anchored child, no best_seed). +constexpr EngineExit kH3LongEngine[] = { + {7, 10.31}, {7, 10.61}, {23, 11.44}, {6, 11.74}, + {8, 11.74}, {14, 12.27}, {25, 13.26}, {7, 11.72}, +}; +constexpr EngineExit kH3ShortEngine[] = { + {3, 9.94}, {17, 10.17}, {3, 13.67}, {2, 13.66}, {13, 13.68}, +}; + +const Probe kProbes[] = { + {"hm-orders-h4-f-long-pending-arm", true, kH4LongPoints, kH4LongOffsets, nullptr, + &kH4Long[0][0], 8, 28, Question::Arm, 0u, nullptr}, + {"hm-orders-h4-f-short-pending-arm", false, kH4ShortPoints, kH4ShortOffsets, nullptr, + &kH4Short[0][0], 8, 28, Question::Arm, 0u, nullptr}, + {"hm-orders-h4-f-long-pending-limit-arm", true, kH4LimitLongPoints, kOffsetOne, + kH4LimitLongLevels, &kH4LimitLong[0][0], 6, 30, Question::Arm, 0u, nullptr}, + {"hm-orders-h4-f-short-pending-limit-arm", false, kH4LimitShortPoints, kOffsetOne, + kH4LimitShortLevels, &kH4LimitShort[0][0], 6, 30, Question::Arm, 0u, nullptr}, + {"hm-orders-h3-f-long-pending-seed", true, kH3LongPoints, kH3LongOffsets, nullptr, + &kH3Long[0][0], 8, 28, Question::RunningBest, 0xFFu, kH3LongEngine}, + {"hm-orders-h3-f-short-pending-seed", false, kH3ShortPoints, kH3ShortOffsets, nullptr, + &kH3Short[0][0], 5, 28, Question::RunningBest, 0x1Fu, kH3ShortEngine}, +}; + +// ── the tape ────────────────────────────────────────────────────────── + +struct TapeTrade { + std::int64_t entry_ms = 0; + double entry_price = kNaN; + std::int64_t exit_ms = 0; + double exit_price = kNaN; + std::string exit_signal; +}; + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +// "YYYY-MM-DD HH:MM" in the tape's UTC+8 -> UTC milliseconds. +std::int64_t tape_ms(const std::string& text) { + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + if (std::sscanf(text.c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) return -1; + const std::int64_t days = days_from_civil(y, static_cast(mo), + static_cast(d)); + return ((days * 24 + h - 8) * 60 + mi) * 60'000; +} + +std::vector read_tape(const std::string& slug) { + std::ifstream in(std::string(PINEFORGE_PENDING_TRAIL_FIXTURE_DIR) + "/" + slug + + "/tv_trades.csv"); + std::vector trades; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 5) continue; + const std::size_t n = static_cast(std::stoi(cell[0])); + if (trades.size() < n) trades.resize(n); + TapeTrade& t = trades[n - 1]; + const bool entry = cell[1].rfind("Entry", 0) == 0; + (entry ? t.entry_ms : t.exit_ms) = tape_ms(cell[2]); + (entry ? t.entry_price : t.exit_price) = std::stod(cell[4]); + if (!entry) t.exit_signal = cell[3]; + } + return trades; +} + +// ── the strategy, as the probe's Pine source runs it ───────────────── + +// Bar 0 is the signal bar: the entry (market, or a limit that fills later) and +// its trailing exit are issued there together. `kTimeout` bars after the fill bar strategy.close takes +// what is left. +class TapeHost : public pineforge::source::PineStrategyHost { +public: + TapeHost(bool is_long, double trail_points, double trail_offset, double limit) + : is_long_(is_long), trail_points_(trail_points), trail_offset_(trail_offset), + limit_(limit) { + pineforge::source::PineStrategyConfig config; + config.initial_capital = 100'000.0; + config.default_qty_type = static_cast(QtyType::FIXED); + config.default_qty_value = 100.0; + config.commission_value = 0.0; + config.commission_type = static_cast(CommissionType::PERCENT); + config.slippage = 0; + config.pyramiding = 1; + config.process_orders_on_close = false; + configure_pine_strategy(config); + syminfo_mintick_ = kTick; + } + + void on_source_bar(const Bar&) override { + const char* id = is_long_ ? "L" : "S"; + if (bar_index_ == 0) { + strategy_entry(id, is_long_, limit_); + strategy_exit(is_long_ ? "LX" : "SX", id, kNaN, kNaN, trail_points_, trail_offset_); + } + const double units = physical_position().signed_units; + if (units != 0.0 && entry_bar_ < 0) entry_bar_ = bar_index_; + if (units != 0.0 && entry_bar_ >= 0 && bar_index_ - entry_bar_ >= kTimeout) + strategy_close(id, "timeout"); + } + + source::PineExecutionAdapter::AnchoredRelativeStats anchored() const { + return adapter_.anchored_relative_stats(); + } + double entry_price(int i) const { return closed_trade_entry_price(i); } + double exit_price(int i) const { return closed_trade_exit_price(i); } + std::int64_t entry_time(int i) const { return closed_trade_entry_time(i); } + std::int64_t exit_time(int i) const { return closed_trade_exit_time(i); } + +private: + bool is_long_; + double trail_points_; + double trail_offset_; + double limit_; + int entry_bar_ = -1; +}; + +std::vector entry_bars(const Probe& probe, int entry) { + std::vector bars; + const FeedBar* row = probe.bars + entry * probe.bars_per_entry; + for (int i = 0; i < probe.bars_per_entry; ++i) { + Bar b{}; + b.timestamp = row[i].ts; + b.open = row[i].open; b.high = row[i].high; + b.low = row[i].low; b.close = row[i].close; + b.volume = 1000.0; + bars.push_back(b); + } + return bars; +} + +bool same_price(double a, double b) { return std::fabs(a - b) <= 1e-9; } + +// TradingView's tick of a print: floor(p / mintick + 0.5). +double tick_of(double price) { return std::floor(price / kTick + 0.5) * kTick; } + +int bar_of(const std::vector& bars, std::int64_t ms) { + for (std::size_t i = 0; i < bars.size(); ++i) + if (bars[i].timestamp == ms) return static_cast(i); + return -1; +} + +std::string utc(std::int64_t ms) { + const std::int64_t minutes = ms / 60'000; + char text[32]; + std::snprintf(text, sizeof(text), "day %lld %02lld:%02lld", + static_cast(minutes / 1440), + static_cast(minutes % 1440 / 60), + static_cast(minutes % 60)); + return text; +} + +// What the kernel recorded for the exit label: the accepted request (an +// anchored Trail, and whether it names a best_seed) and its TrailArm. +struct ExitRecord { + bool anchored_trail = false; + bool best_seed = false; + std::optional arm_bar; + std::optional arm_reached; +}; + +ExitRecord exit_record(const TapeHost& host, const char* label) { + ExitRecord record; + for (const auto& event : host.native_events(0)) { + if (!event.command) continue; + if (const auto* accepted = std::get_if(&*event.command)) { + if (accepted->request().label != label) continue; + if (const auto* t = std::get_if(&accepted->request().trigger)) { + record.anchored_trail = record.anchored_trail + || std::holds_alternative(accepted->request().anchor); + record.best_seed = record.best_seed || t->best_seed.has_value(); + } + } else if (const auto* act = std::get_if(&*event.command)) { + if (act->definition->request.label != label + || act->kind != native_order::ActivationKind::TrailArm) { + continue; + } + record.arm_bar = act->cursor.point.interval_index; + record.arm_reached = act->reached_price; + } + } + return record; +} + +// ── every tape trade, replayed ──────────────────────────────────────── + +void test_tapes_replay(Question question) { + for (const Probe& probe : kProbes) { + if (probe.question != question) continue; + std::printf("-- %s: %d TradingView trades (%s) --\n", probe.slug, probe.entries, + question == Question::Arm ? "the arm" : "the running best"); + const auto tape = read_tape(probe.slug); + CHECK(static_cast(tape.size()) == probe.entries); + if (static_cast(tape.size()) != probe.entries) continue; + const double sign = probe.is_long ? 1.0 : -1.0; + const char* label = probe.is_long ? "LX" : "SX"; + int matched = 0; + for (int e = 0; e < probe.entries; ++e) { + const auto bars = entry_bars(probe, e); + const double limit = probe.limit_levels ? probe.limit_levels[e] : kNaN; + TapeHost host(probe.is_long, probe.trail_points[e], probe.trail_offsets[e], limit); + host.fixture_retain_all_events(); // read after the run (V19-B) + host.run(bars.data(), static_cast(bars.size())); + CHECK(host.last_error().empty()); + CHECK(host.trade_count() == 1); + if (host.trade_count() != 1) continue; + const TapeTrade& tv = tape[static_cast(e)]; + // The entry: a market fill at the open of the bar after the signal, + // or the limit's own level on the later bar whose path crosses it + // (never at that bar's open: no gap). + CHECK(host.entry_time(0) == tv.entry_ms); + CHECK(same_price(host.entry_price(0), tv.entry_price)); + const int fill_bar = bar_of(bars, tv.entry_ms); + if (probe.limit_levels) { + CHECK(fill_bar >= 1 && same_price(tv.entry_price, limit)); + if (fill_bar >= 1) + CHECK(!same_price(bars[static_cast(fill_bar)].open, limit)); + } else { + CHECK(fill_bar == 1 && same_price(tv.entry_price, bars[1].open)); + } + // The shape under test: the exit ran on the kernel's anchored child, + // which the fill point adopted, and that child names no best_seed. + const auto stats = host.anchored(); + CHECK(stats.anchored == 1 && stats.adopted == 1 && stats.withdrawn == 0); + const ExitRecord record = exit_record(host, label); + CHECK(record.anchored_trail); + CHECK(!record.best_seed); + // TradingView's exit is the trail's (never the timeout), on the bar + // that armed it or later, at the stop of a best that starts AT the + // activation: activation -/+ k ticks. + const int k = static_cast(probe.trail_offsets[e]); + const double activation = tv.entry_price + sign * probe.trail_points[e] * kTick; + const int tv_bar = bar_of(bars, tv.exit_ms); + CHECK(tv.exit_signal == label); + CHECK(tv_bar >= 1); + CHECK(same_price(tv.exit_price, activation - sign * k * kTick)); + // The arm: the kernel's TrailArm lands on a bar whose raw extreme + // stops inside the activation's tick cell (its tick is the + // activation, the raw price is not): a quantized arm. + CHECK(record.arm_bar.has_value()); + if (record.arm_bar && *record.arm_bar >= 0 + && *record.arm_bar < static_cast(bars.size())) { + const Bar& arm = bars[static_cast(*record.arm_bar)]; + const double extreme = probe.is_long ? arm.high : arm.low; + CHECK(same_price(tick_of(extreme), activation)); + CHECK(probe.is_long ? extreme < activation : extreme > activation); + if (question == Question::Arm) CHECK(*record.arm_bar == tv_bar); + else CHECK(*record.arm_bar <= tv_bar); + } + const bool matches = host.exit_time(0) == tv.exit_ms + && same_price(host.exit_price(0), tv.exit_price); + if (probe.recorded_divergences & (1u << e)) { + // The anchored child's best starts at the raw arm print, so + // its stop sits below (a short's above) TradingView's and the + // engine exits later, at exactly this bar and price. + CHECK(!matches); + const EngineExit want = probe.engine_exits[e]; + CHECK(host.exit_time(0) == bars[static_cast(1 + want.bars_after_fill)].timestamp); + CHECK(same_price(host.exit_price(0), want.price)); + CHECK(host.exit_time(0) > tv.exit_ms); + std::printf(" trade %d: RECORDED divergence — engine exit %s @%.10g " + "(+%d bars after the fill), TradingView %s @%.10g (+%d)\n", + e + 1, utc(host.exit_time(0)).c_str(), host.exit_price(0), + bar_of(bars, host.exit_time(0)) - 1, utc(tv.exit_ms).c_str(), + tv.exit_price, tv_bar - 1); + continue; + } + CHECK(host.exit_time(0) == tv.exit_ms); + CHECK(same_price(host.exit_price(0), tv.exit_price)); + if (matches) { + ++matched; + } else { + std::printf(" trade %d: engine exit %s @%.10g, TradingView %s @%.10g\n", + e + 1, utc(host.exit_time(0)).c_str(), host.exit_price(0), + utc(tv.exit_ms).c_str(), tv.exit_price); + } + } + if (probe.recorded_divergences == 0) + std::printf(" %d of %d trades match TradingView bar and price\n", matched, + probe.entries); + } +} + +} // namespace + +int main() { + test_tapes_replay(Question::Arm); + test_tapes_replay(Question::RunningBest); + std::printf("\n%s pending-entry trail tapes: %d checks, %d failures\n", + tests_failed == 0 ? "PASS" : "FAIL", tests_passed + tests_failed, tests_failed); + return tests_failed == 0 ? 0 : 1; +} diff --git a/tests/test_pyramiding_count_differential.cpp b/tests/test_pyramiding_count_differential.cpp new file mode 100644 index 00000000..2f2320f8 --- /dev/null +++ b/tests/test_pyramiding_count_differential.cpp @@ -0,0 +1,377 @@ +/* + * test_pyramiding_count_differential.cpp — H-MEASURE row M13 / G2-15. + * + * Pine pyramiding is adapter command policy: PineExecutionAdapter::entry() + * counts `accepted_in_cycle` (the cycle's opened origins that no close path or + * owned bracket has drained, plus every live same-side opening order) when the + * strategy.entry command is written, and project() leaves the kernel's + * NativeRunSpec::max_open_lots unset (ADR-0001 ruling row `max_open_lots`, + * design row MG3). This row measures that policy three ways on the same bars + * and the same commands: + * + * ADAPTER a PineStrategyHost with pyramiding = 2; + * KERNEL a bare NativeStrategyHost with max_open_lots = 2 (a surviving + + * new physical-lot cap checked at the match); + * TAPE TradingView's own trades (`lab tv`, NYSE:F 15m, ws-report-v1, + * rangeProof covered; tests/fixtures/pyramiding_count). + * + * Nine scenarios, each on its own session, flat at its start, closed by + * strategy.close_all. Every outcome is one canonical string (rows sorted by + * entry bar, exit bar, id), pinned for the adapter and the kernel, and each + * scenario asserts which of the two TradingView agrees with: + * + * P1 close(qty) closes lot 1 whole, re-add adapter = kernel = TV + * P2 close(qty) inside a lot, re-add adapter = kernel = TV + * P3 close one id of two, third id adapter = kernel = TV + * P4 resting limit entry, then a market add adapter != TV = kernel + * P4b the resting limit fills after the market add adapter != TV != kernel + * P6 three market entries on one flat bar adapter != TV = kernel + * P8 exit(qty) from B drains lot A (FIFO), third adapter != TV = kernel + * P9 exit(qty) from A drains lot A, third id adapter = kernel = TV + * P10 exit from B, default quantity, two lots open adapter != TV = kernel + * + * P10 is not a pyramiding question: it is the quantity a default + * strategy.exit(from_entry) closes under FIFO with two lots open, which the + * tape's scenario exposed (TradingView closes B's 100, the adapter the book). + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; + +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +#ifndef PINEFORGE_PYRAMIDING_FIXTURE_DIR +#error "PINEFORGE_PYRAMIDING_FIXTURE_DIR must name tests/fixtures/pyramiding_count" +#endif + +namespace { + +constexpr double kNaN = std::numeric_limits::quiet_NaN(); +constexpr int kBars = 12; +constexpr std::int64_t kBarMs = 900'000; + +struct FeedBar { + std::int64_t ts; + double open, high, low, close; +}; + +#include "fixtures/pyramiding_count/bars.inc" + +// ── one canonical outcome ────────────────────────────────────────────── + +struct Row { + std::string id; + double qty; + int entry_bar; + double entry_price; + int exit_bar; + double exit_price; +}; + +std::string canonical(std::vector rows) { + std::sort(rows.begin(), rows.end(), [](const Row& a, const Row& b) { + if (a.entry_bar != b.entry_bar) return a.entry_bar < b.entry_bar; + if (a.exit_bar != b.exit_bar) return a.exit_bar < b.exit_bar; + if (a.id != b.id) return a.id < b.id; + return a.qty < b.qty; + }); + std::string out; + for (const auto& r : rows) { + char buf[160]; + std::snprintf(buf, sizeof buf, "%s %g @%d:%.2f->%d:%.2f; ", r.id.c_str(), r.qty, + r.entry_bar, r.entry_price, r.exit_bar, r.exit_price); + out += buf; + } + return out; +} + +std::vector engine_rows(const BacktestEngine& e, std::int64_t s0) { + std::vector rows; + for (int i = 0; i < e.report_trade_count(); ++i) { + const Trade& t = e.get_report_trade(i); + rows.push_back({t.entry_id, t.qty, static_cast((t.entry_time - s0) / kBarMs), + t.entry_price, static_cast((t.exit_time - s0) / kBarMs), + t.exit_price}); + } + return rows; +} + +// ── the commands, per signal-bar offset ──────────────────────────────── + +enum class Op { Entry, LimitEntry, StopEntry, Close, CloseAll, Cancel, StopExit }; + +struct Cmd { + int bar; + Op op; + const char* id; + double qty = kNaN; // Entry/Close/StopExit quantity (NaN: default) + double level = kNaN; // LimitEntry limit / StopExit stop + const char* from = ""; // StopExit from_entry +}; + +struct Scenario { + const char* key; + const char* tape; + std::vector cmds; + bool adapter_matches_tv; + bool kernel_matches_tv; + const char* adapter; // pinned outcome + const char* kernel; // pinned outcome +}; + +// ── ADAPTER ──────────────────────────────────────────────────────────── + +class PineProbe final : public source::PineStrategyHost { +public: + explicit PineProbe(const std::vector& cmds) : cmds_(cmds) { + source::PineStrategyConfig c; + c.initial_capital = 100'000.0; + c.default_qty_type = static_cast(QtyType::FIXED); + c.default_qty_value = 100.0; + c.commission_value = 0.0; + c.commission_type = static_cast(CommissionType::PERCENT); + c.slippage = 0; + c.pyramiding = 2; + c.process_orders_on_close = false; + configure_pine_strategy(c); + syminfo_mintick_ = 0.01; + } + void on_source_bar(const Bar&) override { + for (const auto& c : cmds_) { + if (c.bar != bar_index_) continue; + switch (c.op) { + case Op::Entry: strategy_entry(c.id, true, kNaN, kNaN, c.qty); break; + case Op::LimitEntry: strategy_entry(c.id, true, c.level); break; + case Op::StopEntry: strategy_entry(c.id, true, kNaN, c.level); break; + case Op::Close: strategy_close(c.id, "", c.qty); break; + case Op::CloseAll: strategy_close_all(); break; + case Op::Cancel: strategy_cancel(c.id); break; + case Op::StopExit: + strategy_exit(c.id, c.from, kNaN, c.level, kNaN, kNaN, kNaN, 100.0, "", c.qty); + break; + } + } + } +private: + std::vector cmds_; +}; + +// ── KERNEL ───────────────────────────────────────────────────────────── + +class KernelProbe final : public NativeStrategyHost { +public: + explicit KernelProbe(const std::vector& cmds) : cmds_(cmds) {} + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + const int b = bar_++; + for (const auto& c : cmds_) { + if (c.bar != b) continue; + const double qty = std::isnan(c.qty) ? 100.0 : c.qty; + no::Request r; + r.label = c.id; + switch (c.op) { + case Op::Entry: r.intent = no::Transact{qty}; break; + case Op::LimitEntry: r.intent = no::Transact{100.0}; r.trigger = no::Limit{c.level}; break; + case Op::StopEntry: r.intent = no::Transact{100.0}; r.trigger = no::Stop{c.level}; break; + case Op::Close: r.intent = no::Reduce{no::ExplicitUnits{qty}}; break; + case Op::CloseAll: r.intent = no::Flatten{}; break; + case Op::StopExit: + // the exit's own entry's quantity, FIFO from the oldest lot + r.intent = no::Reduce{no::ExplicitUnits{qty}}; + r.trigger = no::Stop{c.level}; + break; + case Op::Cancel: { + const auto h = handles_.find(c.id); + if (h != handles_.end()) (void)cancel(h->second); + continue; + } + } + const auto res = submit(r); + if (res.handle) handles_[c.id] = *res.handle; + } + } + int max_open_lots_rejects() const { + int n = 0; + for (const auto& row : native_events(0)) { + if (!row.command) continue; + if (const auto* e = std::get_if(&*row.command)) + if (e->reason == no::MatchRejectReason::MaxOpenLots) ++n; + } + return n; + } +private: + std::vector cmds_; + std::map handles_; + int bar_ = 0; +}; + +NativeRunSpec kernel_spec(const char* key) { + NativeRunSpec s; + s.identity = {key, 1}; + s.event_retention = NativeEventRetention::Full; + s.input_tf = "15"; + s.script_tf = "15"; + s.tickerid = "NYSE:F"; + s.timezone = "UTC"; + s.session = "24x7"; + s.initial_capital = 100'000.0; + s.point_value = 1.0; + s.account_fx = 1.0; + s.price_tick = 0.01; + s.fee_kind = NativeFeeKind::CashPerExecution; + s.fee_value = 0.0; + s.max_open_lots = 2; + return s; +} + +// ── TAPE ─────────────────────────────────────────────────────────────── + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +// "YYYY-MM-DD HH:MM" in the tape's UTC+8 -> UTC milliseconds. +std::int64_t tape_ms(const std::string& text) { + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + if (std::sscanf(text.c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) return -1; + const std::int64_t days = days_from_civil(y, static_cast(mo), + static_cast(d)); + return ((days * 24 + h - 8) * 60 + mi) * 60'000; +} + +struct TapeRow { + std::string id; + double qty = 0.0; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = kNaN, exit_price = kNaN; +}; + +std::vector read_tape(const std::string& slug) { + std::ifstream in(std::string(PINEFORGE_PYRAMIDING_FIXTURE_DIR) + "/" + slug + "/tv_trades.csv"); + std::map trades; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 6) continue; + TapeRow& t = trades[std::stoi(cell[0])]; + const bool entry = cell[1].rfind("Entry", 0) == 0; + if (entry) { + t.entry_ms = tape_ms(cell[2]); + t.entry_price = std::stod(cell[4]); + t.id = cell[3]; + t.qty = std::stod(cell[5]); + } else { + t.exit_ms = tape_ms(cell[2]); + t.exit_price = std::stod(cell[4]); + } + } + std::vector out; + for (auto& kv : trades) out.push_back(kv.second); + return out; +} + +std::string tape_outcome(const std::vector& tape, std::int64_t s0) { + std::vector rows; + for (const auto& t : tape) { + if (t.entry_ms < s0 || t.entry_ms >= s0 + kBars * kBarMs) continue; + rows.push_back({t.id, t.qty, static_cast((t.entry_ms - s0) / kBarMs), t.entry_price, + static_cast((t.exit_ms - s0) / kBarMs), t.exit_price}); + } + return canonical(rows); +} + +#include "fixtures/pyramiding_count/expectations.inc" + +} // namespace + +int main() { + const bool dumping = std::getenv("PF_DUMP") != nullptr; + const std::map tape_trades = { + {"hm-orders-m13-f-pyramiding", 24}, + {"hm-orders-m13-f-pyramiding-rule", 9}, + {"hm-orders-m13-f-pyramiding-cap-orders", 4}, + }; + std::map> tapes; + for (const auto& kv : tape_trades) { + tapes[kv.first] = read_tape(kv.first); + CHECK(tapes[kv.first].size() == kv.second); + } + int index = 0; + for (const auto& s : kScenarios) { + const FeedBar* fb = kPyramidBars[index++]; + std::vector bars; + for (int i = 0; i < kBars; ++i) { + Bar b{}; + b.timestamp = fb[i].ts; b.open = fb[i].open; b.high = fb[i].high; + b.low = fb[i].low; b.close = fb[i].close; b.volume = 1000.0; + bars.push_back(b); + } + const std::int64_t s0 = bars[0].timestamp; + + PineProbe pine(s.cmds); + pine.run(bars.data(), kBars); + CHECK(pine.last_error().empty()); + const std::string adapter = canonical(engine_rows(pine, s0)); + + KernelProbe kernel(s.cmds); + CHECK(kernel.configure_native(kernel_spec(s.key)).status == NativeSetupStatus::Applied); + kernel.run(bars.data(), kBars); + CHECK(kernel.last_error().empty()); + const std::string native = canonical(engine_rows(kernel, s0)); + + const std::string tv = tape_outcome(tapes[s.tape], s0); + std::printf("-- %s\n tape %s\n adapter %s%s\n kernel %s%s (MaxOpenLots refusals %d)\n", + s.key, tv.c_str(), adapter.c_str(), adapter == tv ? " [= tape]" : " [!= tape]", + native.c_str(), native == tv ? " [= tape]" : " [!= tape]", + kernel.max_open_lots_rejects()); + if (dumping) continue; + CHECK(pine.physical_position().signed_units == 0.0); + CHECK(kernel.physical_position().signed_units == 0.0); + CHECK(adapter == s.adapter); + CHECK(native == s.kernel); + CHECK((adapter == tv) == s.adapter_matches_tv); + CHECK((native == tv) == s.kernel_matches_tv); + } + std::printf("\n%s pyramiding count differential: %d checks, %d failures\n", + tests_failed == 0 ? "PASS" : "FAIL", tests_passed + tests_failed, tests_failed); + return tests_failed == 0 ? 0 : 1; +} diff --git a/tests/test_zero_trail_sibling_stop.cpp b/tests/test_zero_trail_sibling_stop.cpp new file mode 100644 index 00000000..3e209cea --- /dev/null +++ b/tests/test_zero_trail_sibling_stop.cpp @@ -0,0 +1,401 @@ +/* + * test_zero_trail_sibling_stop.cpp — H-MEASURE row M16 (G2-18, the sibling stop). + * + * PineExecutionAdapter::exit() lowers a strategy.exit(trail_points=n, + * trail_offset=0) that is ALREADY reached at its placement close to two legs + * in one group: the kernel's Trail (TrailTicks 0.5, best_seed = the carried + * best) and a sibling Stop at the carried best ("A sibling generic stop + * preserves the next-open print decision"). A Stop is reached by a touch; a + * zero-distance ride needs a move strictly past its best. They separate on a + * bar that opens exactly ON the carried best. + * + * 1. The paired differential, seven synthetic tapes: the adapter as it stands, + * a bare NativeStrategyHost given the adapter's own accepted Trail alone, + * and the bare host given both legs. The bare host with both legs IS the + * adapter, bit for bit; the Trail alone rides past the open and fills on a + * later leg of the bar, 0.5 to 1.5 ticks away (Z1: 100.11500000000001 on + * the low leg against 100.09999999999999 at the open). + * + * 2. TradingView (`lab tv`, NYSE:F 15m, ws-report-v1, rangeProof covered; + * tests/fixtures/zero_trail_sibling_stop): eight longs and eight shorts of + * that shape. TradingView exits all sixteen on the bar that opens on the + * carried best, at the carried best's tick: the touch at the open. The + * adapter books TradingView's bar and price 16 of 16; the kernel's Trail + * alone books a different price 16 of 16. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; + +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +#ifndef PINEFORGE_ZERO_TRAIL_FIXTURE_DIR +#error "PINEFORGE_ZERO_TRAIL_FIXTURE_DIR must name tests/fixtures/zero_trail_sibling_stop" +#endif + +namespace { + +constexpr double kNaN = std::numeric_limits::quiet_NaN(); +constexpr std::int64_t kBarMs = 900'000; + +struct FeedBar { + std::int64_t ts; + double open, high, low, close; +}; + +#include "fixtures/zero_trail_sibling_stop/bars.inc" + +bool same_bits(double a, double b) { return std::memcmp(&a, &b, sizeof a) == 0; } +bool same_price(double a, double b) { return std::fabs(a - b) <= 1e-9; } + +// ── the three runs ──────────────────────────────────────────────────── + +// Bar 0 places a market entry; the bar whose open fills it issues the +// zero-offset trailing exit at its close; a 25-bar timeout closes the rest. +class PineRun final : public source::PineStrategyHost { +public: + PineRun(bool is_long, double trail_points, double qty) + : is_long_(is_long), trail_points_(trail_points) { + source::PineStrategyConfig c; + c.initial_capital = 1'000'000.0; + c.default_qty_type = static_cast(QtyType::FIXED); + c.default_qty_value = qty; + c.commission_value = 0.0; + c.slippage = 0; + c.pyramiding = 1; + c.process_orders_on_close = false; + configure_pine_strategy(c); + syminfo_mintick_ = 0.01; + } + void on_source_bar(const Bar&) override { + const char* id = is_long_ ? "L" : "S"; + const double units = physical_position().signed_units; + if (bar_index_ == 0) strategy_entry(id, is_long_); + if (units != 0.0 && entry_bar_ < 0) { + entry_bar_ = bar_index_; + strategy_exit(is_long_ ? "LX" : "SX", id, kNaN, kNaN, trail_points_, 0.0); + } + if (units != 0.0 && entry_bar_ >= 0 && bar_index_ - entry_bar_ >= 25) + strategy_close(id, "timeout"); + } + // The two legs the exit was accepted as. + std::optional trail; + std::optional stop; + void collect() { + const std::string x = is_long_ ? "LX" : "SX"; + for (const auto& ev : native_events(0)) { + if (!ev.command) continue; + const auto* a = std::get_if(&*ev.command); + if (!a || a->request().label != x) continue; + if (const auto* t = std::get_if(&a->request().trigger)) trail = *t; + if (const auto* s = std::get_if(&a->request().trigger)) stop = *s; + } + } +private: + bool is_long_; + double trail_points_; + int entry_bar_ = -1; +}; + +// The bare kernel: the same entry, then at bar 1's calculation the legs the +// adapter was accepted with (as Flatten closes: the trigger is the question). +class KernelRun final : public NativeStrategyHost { +public: + KernelRun(bool is_long, double qty) : is_long_(is_long), qty_(qty) {} + std::vector legs; + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + const int b = bar_++; + if (b == 0) (void)submit({no::Transact{is_long_ ? qty_ : -qty_}, "E", ""}); + if (b == 1) for (const auto& r : legs) (void)submit(r); + } +private: + bool is_long_; + double qty_; + int bar_ = 0; +}; + +NativeRunSpec kernel_spec() { + NativeRunSpec s; + s.identity = {"hm-m16", 1}; + s.event_retention = NativeEventRetention::Full; + s.input_tf = "15"; + s.script_tf = "15"; + s.tickerid = "NYSE:F"; + s.timezone = "UTC"; + s.session = "24x7"; + s.initial_capital = 1'000'000.0; + s.point_value = 1.0; + s.account_fx = 1.0; + s.price_tick = 0.01; + s.fee_kind = NativeFeeKind::CashPerExecution; + s.fee_value = 0.0; + return s; +} + +// The closing fill as the kernel recorded it. +struct Fill { + bool filled = false; + int bar = -1; + double raw = kNaN; + double resolved = kNaN; + bool at_open = false; + bool stop_leg = false; + bool trail_leg = false; +}; + +Fill closing_fill(const NativeStrategyHost& host) { + Fill f; + for (const auto& ev : host.native_events(0)) { + if (!ev.command) continue; + const auto* ap = std::get_if(&*ev.command); + if (!ap || ap->closed_units == 0.0 || f.filled) continue; + f.filled = true; + f.bar = ap->cursor.point.interval_index; + f.raw = ap->raw_price; + f.resolved = ap->resolved_price; + f.at_open = ap->cursor.point.path_phase == NativePathPhase::Open; + f.stop_leg = std::holds_alternative(ap->request().trigger); + f.trail_leg = std::holds_alternative(ap->request().trigger); + } + return f; +} + +struct Triple { + Fill adapter, trail_alone, both_legs; + double adapter_booked = kNaN; + int adapter_exit_bar = -1; + bool adapter_has_sibling = false; +}; + +Triple run_three(const std::vector& bars, bool is_long, double trail_points, double qty) { + Triple out; + PineRun a(is_long, trail_points, qty); + a.fixture_retain_all_events(); + a.run(bars.data(), static_cast(bars.size())); + CHECK(a.last_error().empty()); + a.collect(); + out.adapter = closing_fill(a); + out.adapter_has_sibling = a.trail.has_value() && a.stop.has_value(); + if (a.trade_count() >= 1) { + out.adapter_booked = a.get_trade(0).exit_price; + out.adapter_exit_bar = a.get_trade(0).exit_bar_index; + } + CHECK(a.trail.has_value()); + if (!a.trail) return out; + for (int variant = 0; variant < 2; ++variant) { + KernelRun k(is_long, qty); + no::Request trail{no::Flatten{}, "x", ""}; + trail.trigger = *a.trail; + k.legs.push_back(trail); + if (variant == 1 && a.stop) { + no::Request stop{no::Flatten{}, "x", ""}; + stop.trigger = *a.stop; + k.legs.push_back(stop); + } + CHECK(k.configure_native(kernel_spec()).status == NativeSetupStatus::Applied); + k.run(bars.data(), static_cast(bars.size())); + CHECK(k.last_error().empty()); + (variant == 0 ? out.trail_alone : out.both_legs) = closing_fill(k); + } + return out; +} + +// ── 1. the paired differential ───────────────────────────────────────── + +Bar mk(int i, double o, double h, double l, double c) { + return {o, h, l, c, 1.0, 1743379200000LL + static_cast(i) * kBarMs}; +} + +struct Synthetic { + const char* key; + bool is_long; + std::vector bars; + // pinned: the adapter's booked price and fill, the Trail alone's fill + double adapter_booked; + double trail_alone_raw; + bool trail_alone_same_bar; +}; + +void test_paired_differential() { + std::printf("-- the paired differential (synthetic) --\n"); + const Synthetic cases[] = { + {"Z1 long, close AT the activation, next open ON it", true, + {mk(0, 100.00, 100.00, 100.00, 100.00), mk(1, 100.00, 100.10, 100.00, 100.10), + mk(2, 100.10, 100.12, 100.05, 100.06), mk(3, 100.06, 100.07, 100.00, 100.01)}, + 100.09999999999999, 100.11500000000001, true}, + {"Z2 long, close PAST the activation, next open ON it", true, + {mk(0, 100.00, 100.00, 100.00, 100.00), mk(1, 100.00, 100.13, 100.00, 100.13), + mk(2, 100.13, 100.15, 100.08, 100.09), mk(3, 100.09, 100.10, 100.00, 100.01)}, + 100.13, 100.14500000000001, true}, + {"Z3 short mirror of Z1", false, + {mk(0, 100.00, 100.00, 100.00, 100.00), mk(1, 100.00, 100.00, 99.90, 99.90), + mk(2, 99.90, 99.95, 99.88, 99.94), mk(3, 99.94, 100.00, 99.93, 99.99)}, + 99.900000000000006, 99.884999999999991, true}, + {"Z6 long, close in the activation's cell, next open ON the activation", true, + {mk(0, 100.00, 100.00, 100.00, 100.00), mk(1, 100.00, 100.095, 100.00, 100.095), + mk(2, 100.10, 100.12, 100.05, 100.06), mk(3, 100.06, 100.07, 100.00, 100.01)}, + 100.09999999999999, 100.11500000000001, true}, + {"Z7 long, next open ON the best, low leg first", true, + {mk(0, 100.00, 100.00, 100.00, 100.00), mk(1, 100.00, 100.10, 100.00, 100.10), + mk(2, 100.10, 100.16, 100.08, 100.15), mk(3, 100.15, 100.16, 100.00, 100.01)}, + 100.09999999999999, 100.09500000000001, true}, + }; + for (const auto& c : cases) { + const Triple t = run_three(c.bars, c.is_long, 10.0, 1.0); + std::printf(" %s: adapter %.17g (%s, open=%d) | Trail alone %.17g (open=%d) | both legs %.17g\n", + c.key, t.adapter_booked, t.adapter.stop_leg ? "Stop" : "Trail", + (int)t.adapter.at_open, t.trail_alone.raw, (int)t.trail_alone.at_open, + t.both_legs.raw); + // The adapter rests the sibling, which is the leg that fills: at the + // open, at the carried best. + CHECK(t.adapter_has_sibling); + CHECK(t.adapter.filled && t.adapter.bar == 2 && t.adapter.at_open && t.adapter.stop_leg); + CHECK(same_bits(t.adapter_booked, c.adapter_booked)); + // The kernel given both legs IS the adapter's fill. + CHECK(t.both_legs.filled && t.both_legs.bar == 2 && t.both_legs.at_open + && t.both_legs.stop_leg && same_bits(t.both_legs.raw, t.adapter.raw)); + // The Trail alone does not fire on the touch: it rides and fills + // later on the same bar, at another price. + CHECK(t.trail_alone.filled && t.trail_alone.trail_leg && !t.trail_alone.at_open); + CHECK((t.trail_alone.bar == 2) == c.trail_alone_same_bar); + CHECK(same_bits(t.trail_alone.raw, c.trail_alone_raw)); + CHECK(!same_price(t.trail_alone.raw, t.adapter.raw)); + } +} + +// ── 2. TradingView's tapes ───────────────────────────────────────────── + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +std::int64_t tape_ms(const std::string& text) { // UTC+8 -> UTC ms + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + if (std::sscanf(text.c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) return -1; + const std::int64_t days = days_from_civil(y, static_cast(mo), + static_cast(d)); + return ((days * 24 + h - 8) * 60 + mi) * 60'000; +} + +struct TapeTrade { + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = kNaN, exit_price = kNaN; + std::string exit_signal; +}; + +std::vector read_tape(const std::string& slug) { + std::ifstream in(std::string(PINEFORGE_ZERO_TRAIL_FIXTURE_DIR) + "/" + slug + "/tv_trades.csv"); + std::vector trades; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 5) continue; + const std::size_t n = static_cast(std::stoi(cell[0])); + if (trades.size() < n) trades.resize(n); + TapeTrade& t = trades[n - 1]; + const bool entry = cell[1].rfind("Entry", 0) == 0; + (entry ? t.entry_ms : t.exit_ms) = tape_ms(cell[2]); + (entry ? t.entry_price : t.exit_price) = std::stod(cell[4]); + if (!entry) t.exit_signal = cell[3]; + } + return trades; +} + +void test_tapes() { + struct Set { const char* slug; bool is_long; const double* points; const FeedBar* bars; int n; }; + const Set sets[] = { + {"hm-orders-m16-f-long-zero-trail-open-on-best", true, kZeroTrailLongPoints, + &kZeroTrailLong[0][0], 8}, + {"hm-orders-m16-f-short-zero-trail-open-on-best", false, kZeroTrailShortPoints, + &kZeroTrailShort[0][0], 8}, + }; + for (const auto& set : sets) { + std::printf("-- %s: %d TradingView trades --\n", set.slug, set.n); + const auto tape = read_tape(set.slug); + CHECK(static_cast(tape.size()) == set.n); + if (static_cast(tape.size()) != set.n) continue; + int adapter_matches = 0, trail_alone_matches = 0; + for (int e = 0; e < set.n; ++e) { + std::vector bars; + for (int i = 0; i < 28; ++i) { + const FeedBar& fb = set.bars[e * 28 + i]; + Bar b{}; + b.timestamp = fb.ts; b.open = fb.open; b.high = fb.high; + b.low = fb.low; b.close = fb.close; b.volume = 1000.0; + bars.push_back(b); + } + const TapeTrade& tv = tape[static_cast(e)]; + // The shape: the fill bar's close has reached the activation and + // the next bar opens exactly on the carried best. + CHECK(tv.entry_ms == bars[1].timestamp && same_price(tv.entry_price, bars[1].open)); + // TradingView exits on that next bar, within half a tick of its + // open (the carried best's tick): the touch at the open. + CHECK(tv.exit_ms == bars[2].timestamp); + CHECK(std::fabs(tv.exit_price - bars[2].open) <= 0.005 + 1e-9); + const Triple t = run_three(bars, set.is_long, set.points[e], 100.0); + CHECK(t.adapter_has_sibling); + CHECK(t.adapter.at_open && t.adapter.stop_leg && t.adapter.bar == 2); + const bool adapter_ok = t.adapter_exit_bar == 2 && same_price(t.adapter_booked, tv.exit_price); + const bool trail_ok = t.trail_alone.bar == 2 && same_price(t.trail_alone.raw, tv.exit_price); + CHECK(adapter_ok); + CHECK(!trail_ok); + CHECK(same_bits(t.both_legs.raw, t.adapter.raw) && t.both_legs.at_open); + adapter_matches += adapter_ok ? 1 : 0; + trail_alone_matches += trail_ok ? 1 : 0; + std::printf(" trade %d: TradingView @%.2f; adapter @%.10g (sibling Stop at the open); " + "Trail alone @%.10g (%s)\n", e + 1, tv.exit_price, t.adapter_booked, + t.trail_alone.raw, t.trail_alone.at_open ? "open" : "a later leg"); + } + std::printf(" adapter %d/%d, kernel Trail alone %d/%d match TradingView\n", + adapter_matches, set.n, trail_alone_matches, set.n); + } +} + +} // namespace + +int main() { + test_paired_differential(); + test_tapes(); + std::printf("\n%s zero-offset trail sibling stop: %d checks, %d failures\n", + tests_failed == 0 ? "PASS" : "FAIL", tests_passed + tests_failed, tests_failed); + return tests_failed == 0 ? 0 : 1; +} From 37090e199cc4fd9cb6e1c14cc81552ad50d219c8 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:17:37 +0800 Subject: [PATCH 03/83] H-MEASURE: measure excursions, short-seed swaps, the dust sweep, ismarket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUDIT4 X14 rows E19 (the excursion model, G2-22), G2-21 (short-seed report swaps), G2-32 (the 1e-10 dust sweep) and G2-36 (session.ismarket, the CME weekend and 0000-2400 classes), each measured against the kernel on the same fills and against TradingView's own trades (lab tv, ws-report-v1, rangeProof covered; each fixture README names its tapes): - tests/test_e19_excursion_tape.cpp (E19): a corpus probe's own script re-exported (1580 trades, its excursion cells equal to the corpus tape's) and a calc_on_order_fills scratch tape. Where the host-owned model (RULING A48) and the kernel sampler differ, the kernel's number is TradingView's: entry-bar over-mask, exit-bar over-fold after an at-open exit, the COOF scratch TradingView reports as 0/0. One trade per class pinned; corpus-wide the kernel is TradingView's on 315 of the 326 identified disagreements and the host on none. Recorded divergences. - tests/test_short_seed_report_swap.cpp (G2-21): the swap relabels two rows' entry_incarnation into ab9714be's numbering, read against the kernel's own lot identities in the same run; TradingView exports no incarnation, and the Pine host books the collision tape's 13 rows. - tests/test_pine_dust_sweep_paired.cpp (G2-32): the sweep erases a dust lot inside a live book, unlike ab9714be's whole-book reset; TradingView's decimal quantities leave no remnant row, which the Pine host books and a bare host does not (four 2.8e-17-unit rows). - tests/test_session_ismarket_tape.cpp (G2-36): six tapes (ES1!, EURUSD, XAUUSD, ETH; both 2025 DST switches, Thanksgiving) flag every bar in market; the kernel's in_session matches every spelling, the generated predicate misses every Sunday open under a :23456 mask and every bar of 0000-2400. Routing session.ismarket is a codegen change, not made here. Docs: ADR-0001 (the excursion seam, the dust sweep as a quirk, the session.ismarket selection, the excursion columns being report-only), design RP1, RP5 (its "measured in §3.7 part 2" named no short-seed row) and the excursion-mode alternative, native-settlement.md's sweep paragraph ("restates" the legacy rule, it does not), pine-to-native.md and pine_v6_coverage_detail.md on session.ismarket. No src/ or include/ change. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit fd3c72e2d6cc22a77b86a73d2efc36aeed7c968f) INT26 pick note: docs/native-settlement.md conflicted in text. INT25 (K-ULP4) had appended the sentences on the adapter not declaring the kernel's quantity tolerance to the paragraph this commit rewrites; the merged paragraph is this commit's measured text followed by those INT25 sentences unchanged. The ADR, design and pine-to-native pages conflicted on citation digits only (int16_digit_merge, digits carried from the pre-pick tree). hunkcheck: every hunk of fd3c72e2 carried; design and pine-to-native differ from its diff in anchor digits only. --- docs/adr/0001-kernel-adapter-boundary.md | 28 +- docs/design/native-feature-parity.md | 6 +- docs/native-settlement.md | 14 +- docs/pages/pine-to-native.md | 2 +- docs/pine_v6_coverage_detail.md | 2 +- tests/CMakeLists.txt | 11 + tests/fixtures/e19_excursion_tape/README.md | 25 + tests/fixtures/e19_excursion_tape/bars.inc | 70 +++ .../hm-e19-coof-scratch/meta.json | 16 + .../hm-e19-coof-scratch/metrics.json | 40 ++ .../hm-e19-coof-scratch/strategy.pine | 17 + .../hm-e19-coof-scratch/tv_trades.csv | 9 + .../hm-e19-stop-reversal-grouping/meta.json | 16 + .../metrics.json | 40 ++ .../strategy.pine | 33 ++ tests/fixtures/e19_excursion_tape/tv_rows.inc | 24 + tests/fixtures/g221_short_seed/README.md | 21 + tests/fixtures/g221_short_seed/bars.inc | 389 +++++++++++++++ .../hm-g221-short-seed/meta.json | 16 + .../hm-g221-short-seed/metrics.json | 40 ++ .../hm-g221-short-seed/strategy.pine | 25 + .../hm-g221-short-seed/tv_trades.csv | 27 ++ tests/fixtures/g232_decimal_dust/README.md | 19 + tests/fixtures/g232_decimal_dust/bars.inc | 58 +++ .../hm-g232-decimal-dust/meta.json | 16 + .../hm-g232-decimal-dust/metrics.json | 40 ++ .../hm-g232-decimal-dust/strategy.pine | 24 + .../hm-g232-decimal-dust/tv_trades.csv | 25 + tests/fixtures/session_ismarket/README.md | 27 ++ .../hm-g236-es1-60-dst-mar/meta.json | 16 + .../hm-g236-es1-60-dst-mar/metrics.json | 40 ++ .../hm-g236-es1-60-dst-mar/strategy.pine | 17 + .../hm-g236-es1-60-dst-mar/tv_trades.csv | 421 +++++++++++++++++ .../hm-g236-es1-60-dst-nov/meta.json | 16 + .../hm-g236-es1-60-dst-nov/metrics.json | 40 ++ .../hm-g236-es1-60-dst-nov/strategy.pine | 17 + .../hm-g236-es1-60-dst-nov/tv_trades.csv | 419 +++++++++++++++++ .../hm-g236-es1-60-thanksgiving/meta.json | 16 + .../hm-g236-es1-60-thanksgiving/metrics.json | 40 ++ .../hm-g236-es1-60-thanksgiving/strategy.pine | 17 + .../hm-g236-es1-60-thanksgiving/tv_trades.csv | 385 +++++++++++++++ .../hm-g236-eth-60-24x7/meta.json | 16 + .../hm-g236-eth-60-24x7/metrics.json | 40 ++ .../hm-g236-eth-60-24x7/strategy.pine | 17 + .../hm-g236-eth-60-24x7/tv_trades.csv | 195 ++++++++ .../hm-g236-eurusd-60-dst-mar/meta.json | 16 + .../hm-g236-eurusd-60-dst-mar/metrics.json | 40 ++ .../hm-g236-eurusd-60-dst-mar/strategy.pine | 17 + .../hm-g236-eurusd-60-dst-mar/tv_trades.csv | 441 ++++++++++++++++++ .../hm-g236-xauusd-60-dst-mar/meta.json | 16 + .../hm-g236-xauusd-60-dst-mar/metrics.json | 40 ++ .../hm-g236-xauusd-60-dst-mar/strategy.pine | 17 + .../hm-g236-xauusd-60-dst-mar/tv_trades.csv | 421 +++++++++++++++++ tests/test_e19_excursion_tape.cpp | 374 +++++++++++++++ tests/test_pine_dust_sweep_paired.cpp | 328 +++++++++++++ tests/test_session_ismarket_tape.cpp | 271 +++++++++++ tests/test_short_seed_report_swap.cpp | 379 +++++++++++++++ 57 files changed, 5139 insertions(+), 13 deletions(-) create mode 100644 tests/fixtures/e19_excursion_tape/README.md create mode 100644 tests/fixtures/e19_excursion_tape/bars.inc create mode 100644 tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/meta.json create mode 100644 tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/metrics.json create mode 100644 tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/strategy.pine create mode 100644 tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/tv_trades.csv create mode 100644 tests/fixtures/e19_excursion_tape/hm-e19-stop-reversal-grouping/meta.json create mode 100644 tests/fixtures/e19_excursion_tape/hm-e19-stop-reversal-grouping/metrics.json create mode 100644 tests/fixtures/e19_excursion_tape/hm-e19-stop-reversal-grouping/strategy.pine create mode 100644 tests/fixtures/e19_excursion_tape/tv_rows.inc create mode 100644 tests/fixtures/g221_short_seed/README.md create mode 100644 tests/fixtures/g221_short_seed/bars.inc create mode 100644 tests/fixtures/g221_short_seed/hm-g221-short-seed/meta.json create mode 100644 tests/fixtures/g221_short_seed/hm-g221-short-seed/metrics.json create mode 100644 tests/fixtures/g221_short_seed/hm-g221-short-seed/strategy.pine create mode 100644 tests/fixtures/g221_short_seed/hm-g221-short-seed/tv_trades.csv create mode 100644 tests/fixtures/g232_decimal_dust/README.md create mode 100644 tests/fixtures/g232_decimal_dust/bars.inc create mode 100644 tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/meta.json create mode 100644 tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/metrics.json create mode 100644 tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/strategy.pine create mode 100644 tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/tv_trades.csv create mode 100644 tests/fixtures/session_ismarket/README.md create mode 100644 tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/meta.json create mode 100644 tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/metrics.json create mode 100644 tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/strategy.pine create mode 100644 tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/tv_trades.csv create mode 100644 tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/meta.json create mode 100644 tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/metrics.json create mode 100644 tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/strategy.pine create mode 100644 tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/tv_trades.csv create mode 100644 tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/meta.json create mode 100644 tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/metrics.json create mode 100644 tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/strategy.pine create mode 100644 tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/tv_trades.csv create mode 100644 tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/meta.json create mode 100644 tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/metrics.json create mode 100644 tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/strategy.pine create mode 100644 tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/tv_trades.csv create mode 100644 tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/meta.json create mode 100644 tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/metrics.json create mode 100644 tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/strategy.pine create mode 100644 tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/tv_trades.csv create mode 100644 tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/meta.json create mode 100644 tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/metrics.json create mode 100644 tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/strategy.pine create mode 100644 tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/tv_trades.csv create mode 100644 tests/test_e19_excursion_tape.cpp create mode 100644 tests/test_pine_dust_sweep_paired.cpp create mode 100644 tests/test_session_ismarket_tape.cpp create mode 100644 tests/test_short_seed_report_swap.cpp diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index ae0cc105..22513a96 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -212,7 +212,13 @@ kernel and own these quirks, each at its site: `validate_precommit` (`pine_adapter.cpp:13034`) returns `AdmitWithHostMargin` (`pine_adapter.cpp:13279`) to own the opening margin decision; `owns_lot_excursions() = true` (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:786`), so - MFE/MAE are measured on TV tick-quantized prices; and `on_native_tick` + the Pine host keeps its own per-lot MFE/MAE, `ab9714be`'s model (RULING A48). Measured against + TradingView by R5 lane H-MEASURE (`tests/test_e19_excursion_tape.cpp`, `lab tv` tapes): where + the host's model and the kernel sampler differ, the kernel's number is TradingView's — the host + masks an entry-bar extreme a stop filled at the open never saw, folds the exit bar's extreme + after an at-open exit, and folds the whole entry bar into a `calc_on_order_fills` scratch + TradingView reports as 0 / 0. The retention is `ab9714be` compatibility, a recorded + divergence; `on_native_tick` (`pine_strategy_host.cpp:381`) / `on_native_applied` (`pine_strategy_host.cpp:477`) for `calc_on_order_fills` re-entry. `process_orders_on_close` becomes `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2318-2319`); @@ -231,6 +237,13 @@ kernel and own these quirks, each at its site: adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11671`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:454-460`, twin `tv_money_round` `pine_policy_support.hpp:9-15`). +- **Quantity dust.** After every applied execution the Pine host erases any lot of at most + `kQtyEpsilon` (`1e-10`) from the book it shares with the kernel, with no closing row + (`on_native_applied` `pine_strategy_host.cpp:478`), inside a live book as well as at a flat + reset — broader than `ab9714be`, which reset only a whole book at or under that size. Measured + by R5 lane H-MEASURE (`tests/test_pine_dust_sweep_paired.cpp`): TradingView's decimal + quantities leave no remnant row on a `lab tv` tape, which the Pine host books row for row + while a bare host books four 2.8e-17-unit dust rows beside them. - **Close reservations.** `strategy.close` callsite batching, two-call provenance and the entry-id ledger (`close_logical_units_` `pine_adapter.hpp:2023-2034`); the POOC reservation-growth population predicate (`sha256:f524cf2b9e21e231672451db30ab94184ba3125d8ad6e4f9f614a0f4c538919e` src/compat/pine/reservation_expansion.cpp:9-20). @@ -272,11 +285,16 @@ kernel and own these quirks, each at its site: own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:680`), and the two fills Pine refuses to recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5641`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and - position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the three session flags generated code - reads, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` + position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the three session flags, + `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` `pine_strategy_host.hpp:857`), which `scheduler_update_session_state` (`pine_strategy_host.cpp:1764`) selects from the kernel's session-day facts before each source - callback — the host computes no session-day rule of its own; `barstate_islast_`, still a + callback — the host computes no session-day rule of its own. Generated code reads two of them; + a generated `session.ismarket` still calls the time-of-day predicate + (`pine_session_ismarket` `pine_strategy_host.hpp:606`), which on six `lab tv` tapes misses every + Sunday open of a weekday-masked overnight session and every bar of `0000-2400`, where the + kernel's fact matches TradingView on every bar (R5 lane H-MEASURE, + `tests/test_session_ismarket_tape.cpp`; routing it is a codegen change); `barstate_islast_`, still a `BacktestEngine` member (`engine.hpp:378`) only this host writes; the live-tail / probe-suppress overrides (`set_realtime_tail` `pine_strategy_host.hpp:446`, `set_probe_suppress_tail_logic` `pine_strategy_host.hpp:472`), whose live-probe protocol is @@ -695,7 +713,7 @@ The table records, at the tree the doc wave read, what each block described and | the neutral utility spellings — `map.hpp`, `drawing.hpp`, `series.hpp`, `window_sum.hpp`, `math.hpp`, `matrix.hpp`, `session_time.hpp`, `str_utils.hpp` | neutral public names with exact deprecated `pine_*` aliases for generated code | already ruled: "Neutral spellings" above | dropping the aliases breaks generated code | the ruled-name gate | | `native_matching::ladder_trail_stop` `src/native_matching.hpp`, reaching the core as `native_order::ActivationGrid::ladder_tick` | a trailing stop that stands a whole number of price ticks from a running best that is itself a ladder point IS the ladder point that many ticks away, spelled so no binary64 ULP hides it from the side it is reached from; everything off the ladder keeps `best -/+ offset` raw | a stop `ticks` ticks behind a best is a ladder distance at every venue with a tick ladder, and the rule reads the run's own declared `price_tick` — data, never a platform. It is a defect of arithmetic, not a policy: `11.44 - 5 * 0.01` is `11.389999999999998792` against the ladder point `11.390000000000000568`, so a print that IS 11.39 missed a stop the run had put five ticks under 11.44. Both ends are decimal numbers measured on the host's ladder and only the ladder index is exact between them | nothing, because the run had already decided it — it declared the ladder (`NativeRunSpec::price_tick`) and the host spelled the distance on that ladder (`TrailTicks`, or an equal price distance), so a "raw subtraction or ladder point" switch would put a hash-visible choice on the public surface for an answer already given. Nor is this the per-order-kind quantization the `price_grid` row rejects: nothing is rounded ONTO the ladder, and a sub-tick best, a fractional-tick offset or a run with no declared tick keep the raw level bit for bit | `tests/test_native_trail_stop_ladder.cpp` (kernel-only, batch and stream: lane E14's own 11.44 / 5-tick case against a low of exactly 11.39, its buy mirror, and the three off-ladder shapes pinned bit for bit), with the tape row in `tests/test_trail_activation_tick_reach.cpp` (`e14-f-long-shallow-next` 7/7) and the two paths agreeing in `tests/test_adapter_grid_relower.cpp` section 5. Measured by TradingView's tape (R5 lane E14) and by the price-grid trial from the other side (lane R7: one ULP off on ~14% of (best, offset) pairs), closed by lane E16; design `native-feature-parity.md` §3.6.2 | | `native_order::Trail::best_seed` `include/pineforge/native_order.hpp` | where a trail's running best STARTS: absent, the arm's own print; present, a floor on it — the favourable one of the seed and that print | the level is a number the host hands over exactly as it hands over an `arm_price`: the kernel neither derives it nor knows what named it, and a second venue drives it by passing a different number. A trailing stop that rides from its activation rather than from the next print is an ordinary broker shape | nothing, because the old start was not a choice the run had made another way — it was wherever the arm happened to land (the arm threshold on a crossing, the first live print when the request was born already armed), so there is no second spelling to defer to and nothing for a hook to answer; a request field is admissible, opt-in and hashed only when present | `tests/test_native_trail_best_seed.cpp` (kernel-only, batch and stream, the absent-seed controls pinned as same-host properties of the continuation digest rather than as its pre-field values, because that digest folded the host's own zoneinfo root when the lane landed — E23 made it portable across hosts, but it still moves with the installed tzdata release, so the properties stay); the C tail by `check_trail_best_seed_tail` in `tests/test_native_c_api.c`. The need was measured by R5 lanes E5 and E9 — TradingView's `lab tv` tapes book every trail exit at activation ∓ the offset, never at the kernel's own start — and closed by lane E14; design `native-feature-parity.md` §3.6.2 | -| `build_close_trade_with_costs` and `record_close_trade` `src/engine_orders.cpp` | a closing row's percent P&L is its NET P&L (both commissions deducted) over its entry cost at the same account-currency rate; its excursions include the exit fill itself and sit on the net open-profit basis (the entry commission deducted, the favorable magnitude floored at 0); a row whose P&L is exactly zero counts as even | accounting definitions over the row's own booked numbers -- entry cost, commissions, fill price -- so another venue is driven by its own fees and fills; nothing branches on a platform | a gross/net or tolerance switch over report arithmetic the run's fee terms already decide. One question stays open, recorded at the site: the net basis is applied to a host-owned excursion's magnitudes too (RULING A48's hook) | the corpus parity gate (`scripts/check_corpus_parity.sh`: 312 recorded trade lists carrying the percent P&L and both excursion columns, graded against TradingView's exports) and `tests/test_max_contracts_held.cpp` (a zero-P&L row counted even); the calibration records are the comments at the site (R5 lane F6 reworded them to the mechanism) | +| `build_close_trade_with_costs` and `record_close_trade` `src/engine_orders.cpp` | a closing row's percent P&L is its NET P&L (both commissions deducted) over its entry cost at the same account-currency rate; its excursions include the exit fill itself and sit on the net open-profit basis (the entry commission deducted, the favorable magnitude floored at 0); a row whose P&L is exactly zero counts as even | accounting definitions over the row's own booked numbers -- entry cost, commissions, fill price -- so another venue is driven by its own fees and fills; nothing branches on a platform | a gross/net or tolerance switch over report arithmetic the run's fee terms already decide. One question stays open, recorded at the site: the net basis is applied to a host-owned excursion's magnitudes too (RULING A48's hook) | the corpus parity gate (`scripts/check_corpus_parity.sh`: 312 recorded trade lists carrying the percent P&L and both excursion columns, byte-pinned to the baseline; the excursion columns are read against TradingView's exports as report-only deltas, not graded, `scripts/verify_corpus.py`) and `tests/test_max_contracts_held.cpp` (a zero-P&L row counted even); the calibration records are the comments at the site (R5 lane F6 reworded them to the mechanism) | | the indicator library -- `include/pineforge/ta.hpp`, `src/ta_moving_averages.cpp`, `src/ta_oscillators.cpp`, `src/ta_extremes_volume.cpp`, `src/ta_volatility_trend.cpp`, `src/ta_misc.cpp`, `include/pineforge/window_sum.hpp`, `include/pineforge/math.hpp` | each indicator implements its Pine `ta.*` / `math.*` definition -- warm-up and na rules, the window call-site ring, the compensated window sum, tie and epsilon rules -- and its comments record the TradingView tape or export that calibrated each number (the `ta::ATR` and standard-deviation rows above are two of them) | an indicator is a function of its inputs, the same for every venue; generated strategies call exactly these classes, and where two textbook conventions genuinely compete the choice is a per-instance option (`ta::EmaSeeding`), never a branch on the platform | a convention switch on every indicator, for a choice the script already made by calling that Pine function | the kernel-only TA suites `tests/test_ta_*.cpp` and the corpus parity gate | | the formatting library -- `include/pineforge/str_utils.hpp`, `src/str_utils.cpp` (`str_tostring`, `str_format_values`, `str_format`) | the number text of Pine's `str.tostring` / `str.format` built-ins: a number's shortest round-trip decimal spelling, scaled and rounded half-up on its digits, the default, `percent`, `volume` (K / M / B / T) and pattern modes, MessageFormat placeholders and quoting, `NaN` and `Infinity` -- the published rendering TradingView's exported tapes pin (R5 lane C6), ported from the codegen formatter (lane C6b) by R5 lane B-ENGINE; the `mintick` mode, which generated code delegates to it, is kept as it was | a formatting library is a function of its inputs, the same for every venue, as the indicator library is: it spells a number the way the source language's own `str.*` library defines, and no kernel order, fill, settlement, margin or hash code calls it -- of the thirty-five kernel translation units only `src/str_utils.cpp` names these functions | a rendering switch on functions whose definition the calling script chose by calling them | `tests/test_str_number_format.cpp` (lane C6b's 42-case edge battery, every numeric string of lane C6's TradingView exports, byte-identical output on macOS arm64 and Linux x86-64) and `tests/test_str_utils.cpp` | | the language calendar functions -- `include/pineforge/session_time.hpp`, `src/session_time.cpp` (`timeframe_time`, `local_hour` and its siblings, `session_in_market`, `session_trading_day_open_ms`, ...) | the definitions of Pine's `time()` / `time_close()` / bare-time / `session.is*` built-ins for generated code: session and timezone parsing, the invalid- and 24-hour-session spellings, `time("60")` day keying, `session.is*` on daily charts, each comment naming the tape that pinned it | the inputs are data -- a session string, a timezone, a timeframe -- so another venue is driven by its own session template; nothing branches on a platform, and a native host's calendar is the separate generic `native_calendar` | a rules switch on functions whose definition the generated script chose by calling them | `tests/test_session_time.cpp`, `tests/test_pine_time_day_stamp_grid.cpp` and the corpus parity gate | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 70c1d917..cd176cab 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -155,11 +155,11 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:2613-2626, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1009-1013 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). TV entry-bar masks are A (`sample_open_trade_extremes` pine_adapter.hpp:1226-1233). | +| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:2613-2626, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1009-1013 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). TV entry-bar masks are A (`sample_open_trade_extremes` pine_adapter.hpp:1226-1233). **Measured against TradingView (R5 lane H-MEASURE, `tests/test_e19_excursion_tape.cpp`):** the host-owned model is `ab9714be`'s, not TradingView's — where it and the kernel sampler differ the kernel's number is TradingView's (entry-bar over-mask, exit-bar over-fold after an at-open exit, a `calc_on_order_fills` scratch TradingView reports as 0 / 0); both miss a bar's open print ahead of a same-bar stop fill. On the corpus the two models differ on 341 of 431 390 trades, and of the 326 of those a TradingView export identifies, the kernel's number is TradingView's on 315 and the host's on none. Recorded divergences, retained for `ab9714be` compatibility. | | RP2 | Closed-trade list, trade statistics, report struct | **yes** — `fill_report` engine_report.cpp:39-66, `get_trade` engine.hpp:1791-1828, `compute_trade_stats` engine_metrics.cpp:138 | K | — | F:H2 O:I3 S:X2 | — | | RP3 | Equity curve | **no** — `update_equity_extremes` engine.hpp:1362 / `record_equity_point` engine.hpp:1380 are protected and were called only by the adapter **Closed:** `NativeReportPolicy::KernelRecorded` native_run_spec.hpp:63 has the consumer fold the extremes and append one point per script calculation, labelled with the script interval's open; `KernelRecordedAtHostMarks` records the same series at the host's own marks (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I4 S:X2 | S's "partial" folds RP2 in | | RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:219; `fill_metrics_section` engine_report.cpp:119-139 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | -| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1683) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — and the short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1408) stay A, measured in §3.7 part 2. | +| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1683) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1408) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected `sha256:68946f371856f0a13ae5c347b61ab3112f52edcaa3eafbf411177b565ee88260` engine.hpp:1827 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | | RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9280, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | | RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1358, native_execution_consumer.cpp:10682; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | @@ -1462,7 +1462,7 @@ std::optional risk; // NativeRunSpec | Proposal | Src | Why not now | |---|---|---| -| `NativeExcursionMode { PathExtremes, FillOnly, HostOwned }` + per-lot sample events | S P8 | The kernel sampler + A48 ownership already cover RP1 (F, O); no input found a gap | +| `NativeExcursionMode { PathExtremes, FillOnly, HostOwned }` + per-lot sample events | S P8 | The kernel sampler + A48 ownership already cover RP1 (F, O). TradingView's tapes found gaps in the host-owned half, not in the kernel's (RP1, R5 lane H-MEASURE); a mode enum would not close them | | `NativeFxEpoch` + `on_native_fx_epoch` (stream-safe FX) | S P10 | No R5 lane; F rules it out of scope → U4 | | Generic `FillModel` interface | S §2.2, lane G | R5-3 + R5-1 (§2.ii n) | | TA `ComparePolicy` split | S §2.2 | not neutral (§2.ii b) → U6 | diff --git a/docs/native-settlement.md b/docs/native-settlement.md index 8ce5193a..54542fa2 100644 --- a/docs/native-settlement.md +++ b/docs/native-settlement.md @@ -220,9 +220,17 @@ it -- the closes the exact-sum rule above now closes lot by lot (R5 lane K-ULP3). The Pine source host keeps one quantity rule of its own: after every applied execution, `source::PineStrategyHost::on_native_applied` erases any lot of at -most `kQtyEpsilon` (`1e-10`) without a closing row, the settle rule of the -legacy engine it restates. That is source-layer TradingView policy, not the -kernel's; since K-ULP3 an exact-sum close no longer leaves such a lot for it. +most `kQtyEpsilon` (`1e-10`) from the book it shares with the kernel, with no +closing row or event, and resets the position when nothing is left. It is +broader than the legacy engine's settle rule: `ab9714be` reset only a whole +book at or under `kQtyEpsilon` and kept a dust lot beside a live one, where the +sweep erases it inside a live book too. It is measured source-layer +TradingView policy, not the kernel's: TradingView's decimal quantities leave no +remnant row (`lab tv` tape `hm-g232-decimal-dust`), which the Pine host books +row for row while a bare host books four 2.8e-17-unit dust rows beside them +(`tests/test_pine_dust_sweep_paired.cpp`). Since K-ULP3 an exact-sum close no +longer leaves such a lot, but a decimal sum binary64 misses (0.1 + 0.2 closed +by 0.3) still does. The adapter does not declare the kernel's quantity tolerance (ADR-0001, "Kernel capabilities the Pine adapter does not declare"): its FIFO endpoint test settles a snapped prefix as a selected `Flatten`, charged the lots it diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 685ca02f..2932aa24 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -460,7 +460,7 @@ part both ways (`tests/test_adapter_margin_schedule_differential.cpp`, M11). | raw input before aggregation | `on_native_input` native_host.hpp:863 | `on_input` native_c_api.h:2461 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | | a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:916 | `on_sub_bar` native_c_api.h:2502 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | | `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1454 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. | -| `session.ismarket` | `in_session` market_driver.hpp:161 | `in_session` native_c_api.h:1472 | `tests/test_native_session_day_facts.cpp` | The script bar's label is in session on the run's own calendar (`NativeRunSpec::session` / `timezone`); a D/W/M bar holds whole session days, so it is always true there. The calendar applies a session's day mask to the session day's trading date: Sunday 17:00 CT of a CME-style `1700-1600:23456` is Monday's session, in session. Pine's own `session.ismarket` is still generated as the time-of-day predicate `pine_session_ismarket` session_time.hpp:247, whose mask reads each instant's weekday, so on such a masked overnight session the two differ. In C, read the three session bytes only when `session_facts` native_c_api.h:1471 is 1. | +| `session.ismarket` | `in_session` market_driver.hpp:161 | `in_session` native_c_api.h:1472 | `tests/test_native_session_day_facts.cpp` | The script bar's label is in session on the run's own calendar (`NativeRunSpec::session` / `timezone`); a D/W/M bar holds whole session days, so it is always true there. The calendar applies a session's day mask to the session day's trading date: Sunday 17:00 CT of a CME-style `1700-1600:23456` is Monday's session, in session. Pine's own `session.ismarket` is still generated as the time-of-day predicate `pine_session_ismarket` session_time.hpp:247, whose mask reads each instant's weekday, so on such a masked overnight session the two differ: on six `lab tv` tapes TradingView flags every bar in market, the calendar's fact matches it on every bar, and the predicate misses every Sunday open under a weekday mask (`1700-1600:23456`) and every bar of `0000-2400`, which it cannot parse as a whole day (`tests/test_session_ismarket_tape.cpp`). Unmasked sessions (`1700-1600`, `1700-1700`, `24x7`, `0930-1600`) agree on all three. In C, read the three session bytes only when `session_facts` native_c_api.h:1471 is 1. | | `session.isfirstbar` | `opens_session_day` market_driver.hpp:162 | `opens_session_day` native_c_api.h:1473 | `tests/test_native_session_day_facts.cpp` | In session, and the bar before it is not, or is on another session day. The session day rolls at the session's first window start and is keyed to its trading date, so an overnight session is one day across local midnight (Tokyo `2230-0500`: its days open at 22:30). "The bar before" is the one the run holds — the batch input, the stream warmup — else the calendar's previous eligible input slot across declared breaks; a run's first bar opens its day. | | `session.islastbar` | `closes_session_day` market_driver.hpp:163, `closes_session_day_open_ended` market_driver.hpp:164 | `closes_session_day` native_c_api.h:1474 | `tests/test_native_session_day_facts.cpp` | In session, and the bar after it is not, or is on another session day, "the bar after" read the same way. A batch's final bar closes its day (a batch is complete input); `closes_session_day_open_ended` reads the calendar there instead, for a host recomputing a batch whose last input is still forming (C++ only: a C host's live edge is a stream). A stream's bars read the calendar, so a stream cannot see an early close its session string does not declare: the NYSE half day's 12:45 closes the day in a batch (the next bar held is the next day's) but not in a stream (ADR-0001, ruling "Session-day facts at a bar with nothing held after it"). | | the price at the current point | `current_execution_point` native_host.hpp:1075 | `price` native_c_api.h:1533 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1087 is an observation, never an apply token; `execute_current` native_host.hpp:1092 is the apply. | diff --git a/docs/pine_v6_coverage_detail.md b/docs/pine_v6_coverage_detail.md index 6a3165d3..f26138da 100644 --- a/docs/pine_v6_coverage_detail.md +++ b/docs/pine_v6_coverage_detail.md @@ -164,7 +164,7 @@ | `session.isfirstbar_regular` | var | ✅ Runtime | Aliased to `session.isfirstbar` — engine has single session string, cannot distinguish RTH vs ETH (documented limitation) | Sprint A | | `session.islastbar` | var | ✅ Runtime | `session_islastbar_` on the Pine host, same selection, from `NativeDecisionContext::closes_session_day` (`closes_session_day_open_ended` on a live probe's forming bar); the last bar of a session DAY — a one-bar lookahead whose successor is out of session or belongs to another session day, as TradingView's tapes flag it (`tests/fixtures/session_islastbar`); on a 1D+ chart every bar (the bar is its whole session) | Sprint A | | `session.islastbar_regular` | var | ✅ Runtime | Aliased to `session.islastbar` | Sprint A | -| `session.ismarket` | var | ✅ Runtime | `pine_session_ismarket(session, tz, bar_ms, chart_tf)` in `session_time.hpp` (via `BacktestEngine::pine_session_ismarket`); always true on a 1D+ chart, as TradingView documents | Sprint A | +| `session.ismarket` | var | ✅ Runtime | `pine_session_ismarket(session, tz, bar_ms, chart_tf)` in `session_time.hpp` (via `source::PineStrategyHost::pine_session_ismarket`); always true on a 1D+ chart, as TradingView documents. Measured against TradingView (`tests/test_session_ismarket_tape.cpp`): it misses every Sunday open of a weekday-masked overnight session (`1700-1600:23456`) and every bar of `0000-2400`, where the kernel's `in_session` matches the tapes | Sprint A | | `session.ispostmarket` | var | ✅ Runtime | `pine_session_ispostmarket(...)` — standard ETH window `RTH_close-2000` local; always false on a 1D+ chart | Sprint A | | `session.ispremarket` | var | ✅ Runtime | `pine_session_ispremarket(...)` — standard ETH window `0400-RTH_open` local; always false on a 1D+ chart | Sprint A | diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 361bc814..655d2c66 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1330,6 +1330,14 @@ list(APPEND TEST_SOURCES test_pyramiding_count_differential) list(APPEND TEST_SOURCES test_zero_trail_sibling_stop) # E5's pending-entry arm and E14's anchored-leg seed (AUDIT3 H6 tapes). list(APPEND TEST_SOURCES test_pending_entry_trail_tapes) +# G2-22 / E19 the host-owned excursion model against the kernel sampler. +list(APPEND TEST_SOURCES test_e19_excursion_tape) +# G2-21 the short-seed report swaps against the kernel's lot identities. +list(APPEND TEST_SOURCES test_short_seed_report_swap) +# G2-32 the 1e-10 dust sweep against a bare host on the same fills. +list(APPEND TEST_SOURCES test_pine_dust_sweep_paired) +# G2-36 session.ismarket: the generated predicate, the kernel's in_session. +list(APPEND TEST_SOURCES test_session_ismarket_tape) # R4-D L4b-fix restores every removed ShortSeed, affordability, sizing and # deferred-close oracle as an individually compiled native-route twin. Keep @@ -2066,6 +2074,9 @@ foreach(_pf_hm_row IN ITEMS "test_pyramiding_count_differential|PINEFORGE_PYRAMIDING_FIXTURE_DIR|pyramiding_count" "test_zero_trail_sibling_stop|PINEFORGE_ZERO_TRAIL_FIXTURE_DIR|zero_trail_sibling_stop" "test_pending_entry_trail_tapes|PINEFORGE_PENDING_TRAIL_FIXTURE_DIR|pending_entry_trail" + "test_short_seed_report_swap|PINEFORGE_G221_FIXTURE_DIR|g221_short_seed" + "test_pine_dust_sweep_paired|PINEFORGE_G232_FIXTURE_DIR|g232_decimal_dust" + "test_session_ismarket_tape|PINEFORGE_G236_FIXTURE_DIR|session_ismarket" "test_adapter_margin_schedule_differential|PINEFORGE_HM_M7A_FIXTURE_DIR|margin_entry_bar") string(REPLACE "|" ";" _pf_hm_fields "${_pf_hm_row}") list(GET _pf_hm_fields 0 _pf_hm_target) diff --git a/tests/fixtures/e19_excursion_tape/README.md b/tests/fixtures/e19_excursion_tape/README.md new file mode 100644 index 00000000..eae24b52 --- /dev/null +++ b/tests/fixtures/e19_excursion_tape/README.md @@ -0,0 +1,25 @@ +# Excursion tapes (R5 wave H, lane H-MEASURE, row E19 / G2-22) + +TradingView's own favorable / adverse excursion per trade, against the Pine +adapter's host-owned model (RULING A48) and the kernel sampler. +`tests/test_e19_excursion_tape.cpp` replays the rows below through a +`PineStrategyHost` and, with the same requests, a bare `NativeStrategyHost`. + +| tape (`lab tv`, ws-report-v1, rangeProof covered) | chart | window | trades | tv_trades.csv sha256 | campaign note | +|---|---|---|---|---|---| +| `hm-e19-stop-reversal-grouping` (corpus probe `order-stop-entry-reversal-grouping-01`'s own `strategy.pine`) | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2026-05-01 | 1580 | `1748651d9064e7844e67ed1075a0bc43488600720878cd4a46c61be8fce44a5d` | `tv-tape-hm-e19-stop-reversal-grouping-1748651d` | +| `hm-e19-coof-scratch` | BINANCE:ETHUSDT.P 15 | 2025-06-02 .. 2025-06-06 | 4 | `fe94432eb27eb032cb318c0e74ff6b6f95a925bfff2a56bd7217f48cca306244` | `tv-tape-hm-e19-coof-scratch-fe94432e` | + +The first tape's excursion cells equal the corpus tape's +(`corpus/validation/order-stop-entry-reversal-grouping-01/tv_trades.csv`) on +all 1460 trades the two share. Only its twelve rows on three days are carried +here (`tv_rows.inc`, cells verbatim, generated with the tape's sha256); the +full CSV (3160 rows) is the campaign note's. The second tape is carried whole. + +`bars.inc` (`exec/H-MEASURE-scratch/report/row1-excursion/gen_fixture.py`): +the corpus 15m chart feed that `scripts/derive_corpus_feeds.py` derives from +the corpus 1m feed at gitlink eede4a2 (1m sha256 +`db8c1332da093008cfbd063e05db0b33fe8f7fd35d78cf058a366519eb9f6cc5`, 15m sha256 +`27b62431096edf1bfba71b2409f8dc183f69213dec2834560b3241750f8e7026`): +2025-04-08, 2025-05-13 and 2025-10-17 00:00 .. 02:15 UTC (ten bars each) and +2025-06-02 .. 05 00:00 .. 00:45 UTC (four bars each). diff --git a/tests/fixtures/e19_excursion_tape/bars.inc b/tests/fixtures/e19_excursion_tape/bars.inc new file mode 100644 index 00000000..1dc81012 --- /dev/null +++ b/tests/fixtures/e19_excursion_tape/bars.inc @@ -0,0 +1,70 @@ +// Generated by exec/H-MEASURE-scratch/report/row1-excursion/gen_fixture.py +// from the corpus 15m chart feed ohlcv_ETH-USDT-USDT_15m.csv sha256 27b62431096edf1bfba71b2409f8dc183f69213dec2834560b3241750f8e7026 +// (scripts/derive_corpus_feeds.py of corpus/data/ohlcv_ETH-USDT-USDT_1m.csv). +// 2025-04-08 00:00 .. 02:15 UTC +static const FeedBar kDay0[] = { + {1744070400000LL, 1552.44, 1560.59, 1549.01, 1559.12, 58882.101}, + {1744071300000LL, 1559.12, 1562.63, 1553.47, 1553.57, 35485.98}, + {1744072200000LL, 1553.57, 1558.55, 1547.05, 1555.86, 69660.902}, + {1744073100000LL, 1555.87, 1556.19, 1543.2, 1543.77, 50119.052}, + {1744074000000LL, 1543.77, 1552.37, 1540.19, 1548.59, 58773.183}, + {1744074900000LL, 1548.6, 1568.98, 1544.41, 1566.35, 99290.777}, + {1744075800000LL, 1566.34, 1586.68, 1563.12, 1583, 131948.191}, + {1744076700000LL, 1583, 1593.41, 1577.53, 1590.54, 119095.653}, + {1744077600000LL, 1590.55, 1618, 1587.68, 1610.76, 184550.453}, + {1744078500000LL, 1610.76, 1612.11, 1600.12, 1600.33, 85442.799}, +}; +// 2025-05-13 00:00 .. 02:15 UTC +static const FeedBar kDay1[] = { + {1747094400000LL, 2494.16, 2494.9, 2473.26, 2476.62, 73831.743}, + {1747095300000LL, 2476.63, 2480.76, 2465.38, 2471.35, 68894.141}, + {1747096200000LL, 2471.35, 2471.49, 2457.6, 2458.43, 56464.141}, + {1747097100000LL, 2458.44, 2469.99, 2458.01, 2461.97, 47616.124}, + {1747098000000LL, 2461.97, 2475.59, 2455, 2464.68, 54830.524}, + {1747098900000LL, 2464.68, 2472.84, 2460.5, 2460.5, 35880.385}, + {1747099800000LL, 2460.5, 2465, 2438.73, 2448.25, 108856.678}, + {1747100700000LL, 2448.25, 2457.6, 2442.03, 2451.94, 38666.956}, + {1747101600000LL, 2451.94, 2458.79, 2432.88, 2436.37, 89543.723}, + {1747102500000LL, 2436.37, 2445.99, 2414.36, 2445.63, 155453.484}, +}; +// 2025-10-17 00:00 .. 02:15 UTC +static const FeedBar kDay2[] = { + {1760659200000LL, 3892.02, 3903.63, 3886.15, 3901.02, 26848.259}, + {1760660100000LL, 3901.01, 3913.4, 3896.69, 3912.13, 33200.982}, + {1760661000000LL, 3912.15, 3924, 3906.38, 3918.01, 44511.786}, + {1760661900000LL, 3918.01, 3925.79, 3912, 3925.79, 23327.11}, + {1760662800000LL, 3925.8, 3948.06, 3920.1, 3933.02, 64621.714}, + {1760663700000LL, 3933.02, 3940.74, 3924.57, 3928.91, 25803.937}, + {1760664600000LL, 3928.92, 3932, 3907.93, 3917.78, 45447.059}, + {1760665500000LL, 3917.79, 3921.7, 3904, 3918.53, 25252.444}, + {1760666400000LL, 3918.53, 3923.34, 3906.43, 3912.77, 26222.377}, + {1760667300000LL, 3912.78, 3913.63, 3896.77, 3901.02, 28514.025}, +}; +// 2025-06-02 00:00 .. 00:45 UTC (calc_on_order_fills scratch) +static const FeedBar kCoofDay0[] = { + {1748822400000LL, 2537.89, 2539.56, 2533.53, 2535, 27293.799}, + {1748823300000LL, 2535, 2538.74, 2527.33, 2529.38, 48183.913}, + {1748824200000LL, 2529.38, 2533.46, 2525.17, 2533.24, 29590.856}, + {1748825100000LL, 2533.24, 2533.44, 2528.9, 2533.2, 14329.399}, +}; +// 2025-06-03 00:00 .. 00:45 UTC (calc_on_order_fills scratch) +static const FeedBar kCoofDay1[] = { + {1748908800000LL, 2606.49, 2616.88, 2600.28, 2615.07, 98812.201}, + {1748909700000LL, 2615.08, 2622.67, 2612.8, 2614.1, 109024.481}, + {1748910600000LL, 2614.1, 2625.85, 2614.1, 2618.8, 70426.42}, + {1748911500000LL, 2618.8, 2622.39, 2615.51, 2620.79, 32869.737}, +}; +// 2025-06-04 00:00 .. 00:45 UTC (calc_on_order_fills scratch) +static const FeedBar kCoofDay2[] = { + {1748995200000LL, 2592.16, 2602, 2590, 2593.02, 39685.429}, + {1748996100000LL, 2593.02, 2600, 2592.72, 2593.6, 17227.582}, + {1748997000000LL, 2593.6, 2593.6, 2582.41, 2587.65, 41156.387}, + {1748997900000LL, 2587.64, 2598.7, 2587.16, 2597.91, 28664.85}, +}; +// 2025-06-05 00:00 .. 00:45 UTC (calc_on_order_fills scratch) +static const FeedBar kCoofDay3[] = { + {1749081600000LL, 2606.62, 2612, 2605.81, 2606.69, 25621.402}, + {1749082500000LL, 2606.69, 2609.93, 2600.11, 2607.57, 29806.215}, + {1749083400000LL, 2607.57, 2614.01, 2607.35, 2608.35, 33828.047}, + {1749084300000LL, 2608.35, 2611.89, 2603.2, 2610.77, 26457.673}, +}; diff --git a/tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/meta.json b/tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/meta.json new file mode 100644 index 00000000..6f1749fa --- /dev/null +++ b/tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/meta.json @@ -0,0 +1,16 @@ +{ + "title": "HM E19 coof scratch", + "slug": "hm-e19-coof-scratch", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:21:34.577Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-02", + "to": "2025-06-06" + }, + "pine_sha256": "92e3ba6a638eb205f9633ae651a3c0f567cb7bd395f2034220fda0267a13881e", + "tv_trades_sha256": "fe94432eb27eb032cb318c0e74ff6b6f95a925bfff2a56bd7217f48cca306244", + "tv_trades_rows": 8 +} diff --git a/tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/metrics.json b/tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/metrics.json new file mode 100644 index 00000000..5c1cd252 --- /dev/null +++ b/tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-e19-coof-scratch", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-02", + "to": "2025-06-06", + "deepBacktesting": true, + "trades": 4, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "92e3ba6a638eb205f9633ae651a3c0f567cb7bd395f2034220fda0267a13881e", + "tvTradesCsvHash": "fe94432eb27eb032cb318c0e74ff6b6f95a925bfff2a56bd7217f48cca306244", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74423", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1748822400000, + "to": 1749168000000 + }, + "returnedRange": { + "from": 1748822400000, + "to": 1749168000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "9e4a97cfc8b3536c258b132aae7c562fef929c3a6efde718ef7716a28b1cbf91", + "decodedReportSha256": "bebd49bf98b8ae8fcb7c8cb1892e1532bf386a56eac285c48250ccf7b4deb9dd", + "closedTrades": 4, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:21:34.576Z" +} diff --git a/tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/strategy.pine b/tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/strategy.pine new file mode 100644 index 00000000..c2733ca6 --- /dev/null +++ b/tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/strategy.pine @@ -0,0 +1,17 @@ +// H-MEASURE (R5 wave H) row E19: the excursion of a calc_on_order_fills +// scratch. Every day (UTC) a long market entry from the 00:15 signal fills at +// the 00:30 open; the fill's own recalculation sees the position and closes +// it at once. Which part of the 00:30 bar belongs to that trade's +// favorable/adverse excursion? +//@version=6 +strategy("HM E19 coof scratch", overlay=true, initial_capital=1000000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100) + +m = hour * 60 + minute +if m == 15 and strategy.position_size == 0 + strategy.entry("L", strategy.long) +if strategy.position_size > 0 + strategy.close("L") diff --git a/tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/tv_trades.csv b/tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/tv_trades.csv new file mode 100644 index 00000000..c10ea211 --- /dev/null +++ b/tests/fixtures/e19_excursion_tape/hm-e19-coof-scratch/tv_trades.csv @@ -0,0 +1,9 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-02 08:30,Close entry(s) order L,2529.38,1,2529.38,0,0.00,0,0,0.00,0,0.00,0,0.00,0 +1,Entry long,2025-06-02 08:30,L,2529.38,1,2529.38,0,0.00,0,0,0.00,0,0.00,0,0.00,0 +2,Exit long,2025-06-03 08:30,Close entry(s) order L,2614.1,1,2614.1,0,0.00,0,0,0.00,0,0.00,0,0.00,0 +2,Entry long,2025-06-03 08:30,L,2614.1,1,2614.1,0,0.00,0,0,0.00,0,0.00,0,0.00,0 +3,Exit long,2025-06-04 08:30,Close entry(s) order L,2593.6,1,2593.6,0,0.00,0,0,0.00,0,0.00,0,0.00,0 +3,Entry long,2025-06-04 08:30,L,2593.6,1,2593.6,0,0.00,0,0,0.00,0,0.00,0,0.00,0 +4,Exit long,2025-06-05 08:30,Close entry(s) order L,2607.57,1,2607.57,0,0.00,0,0,0.00,0,0.00,0,0.00,0 +4,Entry long,2025-06-05 08:30,L,2607.57,1,2607.57,0,0.00,0,0,0.00,0,0.00,0,0.00,0 \ No newline at end of file diff --git a/tests/fixtures/e19_excursion_tape/hm-e19-stop-reversal-grouping/meta.json b/tests/fixtures/e19_excursion_tape/hm-e19-stop-reversal-grouping/meta.json new file mode 100644 index 00000000..b50e5827 --- /dev/null +++ b/tests/fixtures/e19_excursion_tape/hm-e19-stop-reversal-grouping/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PF probe 57 - stop reversal grouping", + "slug": "hm-e19-stop-reversal-grouping", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:05:16.756Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01" + }, + "pine_sha256": "2777345294950c2701f702f0200ae42eba1e5413eb5515c609edd225eef61d7b", + "tv_trades_sha256": "1748651d9064e7844e67ed1075a0bc43488600720878cd4a46c61be8fce44a5d", + "tv_trades_rows": 3160 +} diff --git a/tests/fixtures/e19_excursion_tape/hm-e19-stop-reversal-grouping/metrics.json b/tests/fixtures/e19_excursion_tape/hm-e19-stop-reversal-grouping/metrics.json new file mode 100644 index 00000000..df9a4b8c --- /dev/null +++ b/tests/fixtures/e19_excursion_tape/hm-e19-stop-reversal-grouping/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-e19-stop-reversal-grouping", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01", + "deepBacktesting": true, + "trades": 1580, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "2777345294950c2701f702f0200ae42eba1e5413eb5515c609edd225eef61d7b", + "tvTradesCsvHash": "1748651d9064e7844e67ed1075a0bc43488600720878cd4a46c61be8fce44a5d", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74412", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "086561faa41428a06ec69fae1c40e809eacaeaf514989dac03e63c600abd12cc", + "decodedReportSha256": "e8d753b80e5eeca074615412dc556695190f1674722b3a5f61a3fae02f8305f6", + "closedTrades": 1580, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:05:16.755Z" +} diff --git a/tests/fixtures/e19_excursion_tape/hm-e19-stop-reversal-grouping/strategy.pine b/tests/fixtures/e19_excursion_tape/hm-e19-stop-reversal-grouping/strategy.pine new file mode 100644 index 00000000..4f34f12a --- /dev/null +++ b/tests/fixtures/e19_excursion_tape/hm-e19-stop-reversal-grouping/strategy.pine @@ -0,0 +1,33 @@ +// This Pine Script® code is licensed under Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 +// © PineForge contributors 2026 +// +// PF probe 57 - stop-entry reversal grouping +// +// Purpose: isolate stop-entry reversals after a small pyramided position. This +// separates reversal/report grouping from the larger cancel-opposite probe. +// TV setup: 15m chart, same symbol/window as data/ohlcv_ETH-USDT-USDT_15m.csv. +// Export TradingView "List of trades" to trades.csv in this folder. +//@version=6 +strategy("PF probe 57 - stop reversal grouping", shorttitle="PF_P57_STOPREV", overlay=true, + initial_capital=1000000, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=2, + process_orders_on_close=false) + +// Build a two-lot long early in the day, then reverse via a short stop that is +// intentionally marketable on the next bar. Later reverse back via a long stop. +if hour == 0 and minute == 15 and strategy.position_size == 0 + strategy.entry("L1", strategy.long, qty=1, comment="long lot 1") + +if hour == 0 and minute == 30 and strategy.position_size > 0 + strategy.entry("L2", strategy.long, qty=1, comment="long lot 2") + +if hour == 0 and minute == 45 and strategy.position_size > 0 + strategy.entry("SREV", strategy.short, qty=1, stop=high, comment="short stop reversal") + +if hour == 1 and minute == 15 and strategy.position_size < 0 + strategy.entry("LREV", strategy.long, qty=1, stop=low, comment="long stop reversal") + +if hour == 1 and minute == 45 and strategy.position_size != 0 + strategy.close_all(comment="flat") diff --git a/tests/fixtures/e19_excursion_tape/tv_rows.inc b/tests/fixtures/e19_excursion_tape/tv_rows.inc new file mode 100644 index 00000000..ed360d67 --- /dev/null +++ b/tests/fixtures/e19_excursion_tape/tv_rows.inc @@ -0,0 +1,24 @@ +// Generated by exec/H-MEASURE-scratch/report/row1-excursion/gen_fixture.py from the +// lab tv tape hm-e19-stop-reversal-grouping, tv_trades.csv sha256 1748651d9064e7844e67ed1075a0bc43488600720878cd4a46c61be8fce44a5d: +// {day, tape trade number, is_long, entry ms, exit ms, entry, exit, qty, favorable, adverse} +static const TapeRow kTape[] = { + {0, 29, true, 1744072200000LL, 1744074000000LL, 1553.57, 1543.77, 1, 4.98, -10.37}, + {0, 30, true, 1744073100000LL, 1744074000000LL, 1555.87, 1543.77, 1, 0.32, -12.67}, + {0, 31, false, 1744074000000LL, 1744075800000LL, 1543.77, 1566.34, 1, 3.58, -25.21}, + {0, 32, true, 1744075800000LL, 1744077600000LL, 1566.34, 1590.55, 1, 27.07, -3.22}, + {1, 169, true, 1747096200000LL, 1747098000000LL, 2471.35, 2461.97, 1, 0.14, -13.75}, + {1, 170, true, 1747097100000LL, 1747098000000LL, 2458.44, 2461.97, 1, 11.55, -0.43}, + {1, 171, false, 1747098000000LL, 1747099800000LL, 2461.97, 2460.5, 1, 6.97, -13.62}, + {1, 172, true, 1747099800000LL, 1747101600000LL, 2460.5, 2451.94, 1, 4.5, -21.77}, + {2, 797, true, 1760661000000LL, 1760662800000LL, 3912.15, 3925.79, 1, 13.65, -5.77}, + {2, 798, true, 1760661900000LL, 1760662800000LL, 3918.01, 3925.79, 1, 7.79, -6.01}, + {2, 799, false, 1760662800000LL, 1760664600000LL, 3925.79, 3928.92, 1, 5.69, -22.27}, + {2, 800, true, 1760664600000LL, 1760666400000LL, 3928.92, 3918.53, 1, 3.08, -24.92}, +}; +// lab tv tape hm-e19-coof-scratch, tv_trades.csv sha256 fe94432eb27eb032cb318c0e74ff6b6f95a925bfff2a56bd7217f48cca306244: +static const TapeRow kCoofTape[] = { + {0, 1, true, 1748824200000LL, 1748824200000LL, 2529.38, 2529.38, 1, 0, 0}, + {1, 2, true, 1748910600000LL, 1748910600000LL, 2614.1, 2614.1, 1, 0, 0}, + {2, 3, true, 1748997000000LL, 1748997000000LL, 2593.6, 2593.6, 1, 0, 0}, + {3, 4, true, 1749083400000LL, 1749083400000LL, 2607.57, 2607.57, 1, 0, 0}, +}; diff --git a/tests/fixtures/g221_short_seed/README.md b/tests/fixtures/g221_short_seed/README.md new file mode 100644 index 00000000..20b601ef --- /dev/null +++ b/tests/fixtures/g221_short_seed/README.md @@ -0,0 +1,21 @@ +# Short-seed collision tape (R5 wave H, lane H-MEASURE, row G2-21) + +Finding 272's short-seed collision (`tests/oracle/test_oracle_short_seed_percent.cpp`) +once a day under percent-of-equity default sizing: a SHORT seed, then in one +bar `entry(Long); entry(Short); close(Long); close(Short)`, the remnant closed +from the 01:00 signal. `tests/test_short_seed_report_swap.cpp` replays it +through a `PineStrategyHost`: the rows are TradingView's, and on the one day a +remnant is closed by a close of the seed id the adapter swaps two rows' entry +incarnations (`project_short_seed_report_rows`), which TradingView does not +export. + +| tape (`lab tv`, ws-report-v1, rangeProof covered) | chart | window | trades | tv_trades.csv sha256 | campaign note | +|---|---|---|---|---|---| +| `hm-g221-short-seed` | BINANCE:ETHUSDT.P 15 | 2025-06-02 .. 2025-06-06 | 13 | `9a842c022746bc393650fc3f09f363808a53f7c078c05dde00f65e408f1734e8` | `tv-tape-hm-g221-short-seed-9a842c02` | + +`bars.inc` (`exec/H-MEASURE-scratch/report/row2-shortseed/gen_bars.py`): the +corpus 15m chart feed (sha256 +`27b62431096edf1bfba71b2409f8dc183f69213dec2834560b3241750f8e7026`, derived +from the corpus 1m feed `db8c1332…`), contiguous 2025-06-02 00:00 .. +2025-06-05 23:45 UTC (384 bars): percent sizing carries equity from day to +day, so the days cannot be replayed apart. diff --git a/tests/fixtures/g221_short_seed/bars.inc b/tests/fixtures/g221_short_seed/bars.inc new file mode 100644 index 00000000..f1e1852d --- /dev/null +++ b/tests/fixtures/g221_short_seed/bars.inc @@ -0,0 +1,389 @@ +// Generated by exec/H-MEASURE-scratch/report/row2-shortseed/gen_bars.py from the corpus +// 15m chart feed ohlcv_ETH-USDT-USDT_15m.csv sha256 27b62431096edf1bfba71b2409f8dc183f69213dec2834560b3241750f8e7026: +// 2025-06-02 00:00 .. 2025-06-05 23:45 UTC. +static const FeedBar kBars[] = { + {1748822400000LL, 2537.89, 2539.56, 2533.53, 2535, 27293.799}, + {1748823300000LL, 2535, 2538.74, 2527.33, 2529.38, 48183.913}, + {1748824200000LL, 2529.38, 2533.46, 2525.17, 2533.24, 29590.856}, + {1748825100000LL, 2533.24, 2533.44, 2528.9, 2533.2, 14329.399}, + {1748826000000LL, 2533.21, 2540.25, 2532.96, 2536.54, 27319.343}, + {1748826900000LL, 2536.53, 2540.89, 2528.24, 2529.69, 38666.805}, + {1748827800000LL, 2529.7, 2531.24, 2525.18, 2526.52, 26871.639}, + {1748828700000LL, 2526.53, 2530.19, 2523.66, 2525, 24800.534}, + {1748829600000LL, 2525, 2526.36, 2514.1, 2516.72, 44134.929}, + {1748830500000LL, 2516.72, 2522.27, 2512.24, 2518.61, 29567.662}, + {1748831400000LL, 2518.61, 2521.91, 2513.64, 2519.79, 22872.839}, + {1748832300000LL, 2519.79, 2520.58, 2506.32, 2507.65, 45083.214}, + {1748833200000LL, 2507.65, 2509.3, 2492, 2492.42, 91203.733}, + {1748834100000LL, 2492.42, 2498.22, 2484.05, 2493, 88491.12}, + {1748835000000LL, 2493.01, 2502.39, 2488.82, 2497.55, 75063.578}, + {1748835900000LL, 2497.56, 2499.24, 2492.5, 2495.12, 27839.004}, + {1748836800000LL, 2495.11, 2498.76, 2491.79, 2498.19, 33046.121}, + {1748837700000LL, 2498.18, 2500.72, 2495.19, 2495.45, 20685.546}, + {1748838600000LL, 2495.44, 2500.93, 2494.8, 2497.96, 20463.48}, + {1748839500000LL, 2497.97, 2499.19, 2486.32, 2491.73, 52372.765}, + {1748840400000LL, 2491.73, 2496.36, 2485.66, 2494.56, 28325.932}, + {1748841300000LL, 2494.57, 2496.6, 2488.12, 2493.68, 21775.597}, + {1748842200000LL, 2493.67, 2493.94, 2485.43, 2485.5, 25713.329}, + {1748843100000LL, 2485.5, 2492.4, 2482, 2491.89, 37640.287}, + {1748844000000LL, 2491.89, 2499.98, 2486.08, 2488.31, 53472.22}, + {1748844900000LL, 2488.3, 2494.27, 2483.3, 2493.46, 47834.213}, + {1748845800000LL, 2493.47, 2498.62, 2487.16, 2498.13, 31821.623}, + {1748846700000LL, 2498.12, 2498.96, 2492.38, 2493.66, 31039.436}, + {1748847600000LL, 2493.66, 2506.6, 2493.38, 2504.57, 53680.286}, + {1748848500000LL, 2504.57, 2512.74, 2500.04, 2501.66, 64020.486}, + {1748849400000LL, 2501.66, 2506.95, 2498.88, 2506.35, 35379.144}, + {1748850300000LL, 2506.34, 2508, 2502.06, 2505.78, 22300.476}, + {1748851200000LL, 2505.79, 2510.97, 2502.23, 2507.03, 27989.333}, + {1748852100000LL, 2507.03, 2514.17, 2502.25, 2511.99, 38705.616}, + {1748853000000LL, 2511.99, 2515, 2496.64, 2498.75, 53257.55}, + {1748853900000LL, 2498.75, 2501.81, 2488.46, 2493.3, 69291.462}, + {1748854800000LL, 2493.3, 2493.81, 2476.5, 2481.43, 123772.26}, + {1748855700000LL, 2481.42, 2487.01, 2476.3, 2482.56, 59443.181}, + {1748856600000LL, 2482.56, 2488.74, 2476.95, 2479.22, 60481.05}, + {1748857500000LL, 2479.21, 2482.34, 2472.74, 2480.4, 60076.753}, + {1748858400000LL, 2480.4, 2488.88, 2475.12, 2488.11, 55955.474}, + {1748859300000LL, 2488.1, 2488.11, 2476.5, 2481.49, 61120.269}, + {1748860200000LL, 2481.48, 2484.5, 2476, 2480.45, 27124.603}, + {1748861100000LL, 2480.44, 2483.84, 2479.6, 2480.93, 13889.927}, + {1748862000000LL, 2480.93, 2487.58, 2476.75, 2485.55, 29574.923}, + {1748862900000LL, 2485.55, 2487.12, 2481.44, 2481.88, 21742.002}, + {1748863800000LL, 2481.87, 2485.37, 2475.66, 2477.19, 26779.837}, + {1748864700000LL, 2477.19, 2482.32, 2474.22, 2480.03, 34593.054}, + {1748865600000LL, 2480.02, 2484.67, 2477.77, 2483.8, 32128.015}, + {1748866500000LL, 2483.8, 2483.8, 2477, 2483.09, 29368.199}, + {1748867400000LL, 2483.09, 2496.5, 2481.6, 2492.68, 70356.162}, + {1748868300000LL, 2492.68, 2497.42, 2489, 2496.96, 46083.253}, + {1748869200000LL, 2496.96, 2498.36, 2487.55, 2491.99, 44198.419}, + {1748870100000LL, 2491.99, 2501.95, 2486.61, 2499.14, 62766.572}, + {1748871000000LL, 2499.14, 2519.34, 2498.77, 2510.99, 154560.666}, + {1748871900000LL, 2511, 2512.55, 2495.18, 2503.76, 96433.531}, + {1748872800000LL, 2503.76, 2515.64, 2494.51, 2513.8, 103335.271}, + {1748873700000LL, 2513.8, 2540, 2508.5, 2530.23, 225582.694}, + {1748874600000LL, 2530.23, 2536.71, 2520.96, 2524, 110772.783}, + {1748875500000LL, 2524, 2531.47, 2515.77, 2530.36, 91868.254}, + {1748876400000LL, 2530.36, 2537, 2522, 2534.67, 63224.252}, + {1748877300000LL, 2534.68, 2556.34, 2530.6, 2538.58, 189918.706}, + {1748878200000LL, 2538.58, 2542.19, 2532.57, 2539.44, 76808.106}, + {1748879100000LL, 2539.44, 2546.66, 2537.08, 2543.24, 61237.378}, + {1748880000000LL, 2543.25, 2544.94, 2535.75, 2536.37, 48120.681}, + {1748880900000LL, 2536.37, 2541.05, 2528.24, 2533.34, 60234.98}, + {1748881800000LL, 2533.33, 2540.73, 2531.11, 2537.06, 38105.459}, + {1748882700000LL, 2537.06, 2553.84, 2535, 2545.19, 69095.507}, + {1748883600000LL, 2545.19, 2551.44, 2538.11, 2538.81, 68501.058}, + {1748884500000LL, 2538.81, 2551.9, 2537.49, 2543.01, 59518.997}, + {1748885400000LL, 2543.01, 2546.72, 2537.37, 2540.8, 36942.793}, + {1748886300000LL, 2540.81, 2551.77, 2539.28, 2548.89, 37885.702}, + {1748887200000LL, 2548.89, 2552, 2543.31, 2550.45, 36012.802}, + {1748888100000LL, 2550.43, 2565.4, 2545.75, 2555.69, 106974.333}, + {1748889000000LL, 2555.68, 2556.57, 2543.48, 2544.63, 61921.298}, + {1748889900000LL, 2544.62, 2544.66, 2536.68, 2543.38, 50165.567}, + {1748890800000LL, 2543.38, 2548, 2533.13, 2533.31, 42919.861}, + {1748891700000LL, 2533.32, 2541.66, 2533, 2537.07, 24853.981}, + {1748892600000LL, 2537.07, 2537.66, 2522.88, 2527.99, 58174.369}, + {1748893500000LL, 2528, 2533.46, 2524.65, 2531.21, 37873.432}, + {1748894400000LL, 2531.22, 2545.55, 2530.7, 2544.88, 66999.724}, + {1748895300000LL, 2544.88, 2546.9, 2539.11, 2541.13, 26921.105}, + {1748896200000LL, 2541.14, 2542.55, 2537.01, 2539.8, 23061.142}, + {1748897100000LL, 2539.8, 2540.66, 2534.26, 2538.72, 17896.287}, + {1748898000000LL, 2538.71, 2546.9, 2538.35, 2545.1, 25024.219}, + {1748898900000LL, 2545.1, 2545.59, 2539.17, 2545.29, 14265.843}, + {1748899800000LL, 2545.29, 2553.29, 2542.69, 2550.16, 37081.446}, + {1748900700000LL, 2550.17, 2560.12, 2548.88, 2559.19, 38123.423}, + {1748901600000LL, 2559.18, 2574.23, 2558.65, 2570.85, 100470.454}, + {1748902500000LL, 2570.84, 2583.76, 2568.16, 2579.52, 90320.212}, + {1748903400000LL, 2579.51, 2583.83, 2573.35, 2575.57, 49114.39}, + {1748904300000LL, 2575.57, 2583.52, 2573.3, 2583.4, 38213.152}, + {1748905200000LL, 2583.4, 2604.79, 2581.5, 2592.52, 175778.058}, + {1748906100000LL, 2592.51, 2608.84, 2591.81, 2605.8, 112751.067}, + {1748907000000LL, 2605.78, 2612, 2599.8, 2610.48, 79723.355}, + {1748907900000LL, 2610.47, 2614.2, 2603.25, 2606.48, 79078.824}, + {1748908800000LL, 2606.49, 2616.88, 2600.28, 2615.07, 98812.201}, + {1748909700000LL, 2615.08, 2622.67, 2612.8, 2614.1, 109024.481}, + {1748910600000LL, 2614.1, 2625.85, 2614.1, 2618.8, 70426.42}, + {1748911500000LL, 2618.8, 2622.39, 2615.51, 2620.79, 32869.737}, + {1748912400000LL, 2620.79, 2626.96, 2617.1, 2618.33, 58796.824}, + {1748913300000LL, 2618.34, 2627.99, 2615.97, 2624.49, 56590.913}, + {1748914200000LL, 2624.48, 2641.98, 2624.08, 2636.84, 120481.335}, + {1748915100000LL, 2636.84, 2649, 2635, 2644.93, 113262.872}, + {1748916000000LL, 2644.94, 2647.19, 2635.38, 2638.54, 88326.262}, + {1748916900000LL, 2638.54, 2639.74, 2630, 2630.03, 61904.575}, + {1748917800000LL, 2630.03, 2634, 2624.45, 2626.5, 53651.541}, + {1748918700000LL, 2626.49, 2627.13, 2613, 2615.16, 141583.952}, + {1748919600000LL, 2615.17, 2619.59, 2592.65, 2596.67, 172819.279}, + {1748920500000LL, 2596.67, 2606.79, 2593.23, 2594.65, 83736.51}, + {1748921400000LL, 2594.66, 2601.71, 2588.05, 2599.98, 75566.101}, + {1748922300000LL, 2599.99, 2605.33, 2598.73, 2602.38, 48567.606}, + {1748923200000LL, 2602.38, 2604.24, 2592.11, 2597.61, 42287.233}, + {1748924100000LL, 2597.61, 2600.42, 2594.54, 2597.73, 32580.655}, + {1748925000000LL, 2597.73, 2599.66, 2588.88, 2593.86, 41002.336}, + {1748925900000LL, 2593.85, 2595, 2589.21, 2591.17, 25687.824}, + {1748926800000LL, 2591.17, 2601.99, 2590.91, 2600.36, 42624.67}, + {1748927700000LL, 2600.36, 2603.44, 2598.6, 2602.52, 30517.306}, + {1748928600000LL, 2602.52, 2608.76, 2600.53, 2606.69, 53484.775}, + {1748929500000LL, 2606.69, 2617.39, 2604.58, 2613.3, 57653.027}, + {1748930400000LL, 2613.3, 2613.69, 2605.41, 2606.73, 32698.058}, + {1748931300000LL, 2606.72, 2608.19, 2603.21, 2605.18, 23628.579}, + {1748932200000LL, 2605.18, 2613.21, 2603.62, 2608.3, 31947.095}, + {1748933100000LL, 2608.29, 2617.89, 2608.09, 2617.31, 36300.597}, + {1748934000000LL, 2617.31, 2619.44, 2612.1, 2613.16, 34841.141}, + {1748934900000LL, 2613.16, 2614.73, 2606.8, 2609.1, 35335.865}, + {1748935800000LL, 2609.1, 2615.33, 2609.1, 2612.86, 21029.888}, + {1748936700000LL, 2612.86, 2612.87, 2606.43, 2608.68, 27882.414}, + {1748937600000LL, 2608.69, 2610.18, 2595.68, 2599.07, 66143.653}, + {1748938500000LL, 2599.09, 2605.03, 2593.47, 2604.01, 50645.914}, + {1748939400000LL, 2604, 2606.88, 2595.04, 2600.36, 37270.173}, + {1748940300000LL, 2600.35, 2608.5, 2599.35, 2606.2, 31543.126}, + {1748941200000LL, 2606.2, 2608.09, 2602, 2603.43, 26754.129}, + {1748942100000LL, 2603.42, 2605.54, 2600.01, 2600.99, 15231.886}, + {1748943000000LL, 2600.99, 2614.51, 2600.13, 2612.84, 49550.986}, + {1748943900000LL, 2612.84, 2617.88, 2611.44, 2612.85, 31058.932}, + {1748944800000LL, 2612.86, 2617.78, 2610.47, 2617.52, 26088.517}, + {1748945700000LL, 2617.48, 2617.48, 2613.14, 2615.48, 17152.473}, + {1748946600000LL, 2615.47, 2619.52, 2611.5, 2612.51, 23871.486}, + {1748947500000LL, 2612.51, 2614.36, 2608.7, 2610.36, 21688.876}, + {1748948400000LL, 2610.37, 2615.74, 2609.2, 2613.17, 29570.536}, + {1748949300000LL, 2613.17, 2613.17, 2604.28, 2607.27, 30860.644}, + {1748950200000LL, 2607.28, 2609.45, 2603.43, 2604.08, 17021.108}, + {1748951100000LL, 2604.08, 2605.83, 2598.83, 2600.21, 30836.461}, + {1748952000000LL, 2600.21, 2607.08, 2596.36, 2607.07, 52066.457}, + {1748952900000LL, 2607.07, 2607.64, 2598.84, 2606.11, 32326.85}, + {1748953800000LL, 2606.12, 2612.47, 2602, 2608.33, 33635.902}, + {1748954700000LL, 2608.33, 2611.42, 2607.74, 2608.63, 19679.392}, + {1748955600000LL, 2608.62, 2618.76, 2608.62, 2617.28, 66118.259}, + {1748956500000LL, 2617.29, 2627.89, 2617.28, 2620.33, 97795.007}, + {1748957400000LL, 2620.33, 2627.42, 2605.7, 2625.87, 132050.787}, + {1748958300000LL, 2625.86, 2633, 2615.04, 2625.49, 107017.241}, + {1748959200000LL, 2625.5, 2630.75, 2612.35, 2615.61, 75134.423}, + {1748960100000LL, 2615.6, 2637.3, 2614.9, 2631.8, 119076.341}, + {1748961000000LL, 2631.8, 2635.72, 2625.5, 2634.69, 54309.08}, + {1748961900000LL, 2634.68, 2654.98, 2629.63, 2647.16, 177882.093}, + {1748962800000LL, 2647.16, 2650.43, 2633.21, 2636.39, 105644.676}, + {1748963700000LL, 2636.39, 2645.69, 2633.37, 2641.64, 60718.339}, + {1748964600000LL, 2641.64, 2643, 2624.59, 2632.23, 120819.66}, + {1748965500000LL, 2632.24, 2632.71, 2615.25, 2622.56, 94201.636}, + {1748966400000LL, 2622.55, 2624.37, 2614.27, 2619.82, 47390.85}, + {1748967300000LL, 2619.81, 2620.71, 2607.22, 2609.46, 53008.956}, + {1748968200000LL, 2609.45, 2617.8, 2608.52, 2613.87, 40203.118}, + {1748969100000LL, 2613.87, 2615.23, 2602.34, 2604.23, 70773.325}, + {1748970000000LL, 2604.22, 2616.5, 2601.76, 2615.65, 61172.276}, + {1748970900000LL, 2615.66, 2626.26, 2615.5, 2622.27, 75501.631}, + {1748971800000LL, 2622.27, 2623.3, 2615.3, 2618.54, 30616.114}, + {1748972700000LL, 2618.54, 2621.71, 2613.3, 2618.05, 27838.41}, + {1748973600000LL, 2618.04, 2618.41, 2605.75, 2608.49, 41485.99}, + {1748974500000LL, 2608.49, 2615.91, 2605.76, 2612.59, 21700.527}, + {1748975400000LL, 2612.59, 2617.6, 2609, 2617.29, 19777.529}, + {1748976300000LL, 2617.3, 2619.36, 2614, 2617.19, 22310.989}, + {1748977200000LL, 2617.19, 2624.76, 2617, 2619.45, 25258.735}, + {1748978100000LL, 2619.45, 2627.77, 2618.37, 2624.84, 28133.706}, + {1748979000000LL, 2624.83, 2624.83, 2617.76, 2620.05, 39044.753}, + {1748979900000LL, 2620.05, 2624.16, 2613.79, 2624.16, 43989.823}, + {1748980800000LL, 2624.17, 2624.46, 2615.51, 2618.56, 27792.697}, + {1748981700000LL, 2618.57, 2618.99, 2602.58, 2603.21, 57276.039}, + {1748982600000LL, 2603.22, 2618.12, 2593.01, 2615.22, 120589.833}, + {1748983500000LL, 2615.23, 2618.56, 2609.25, 2614.1, 22961.211}, + {1748984400000LL, 2614.09, 2615.94, 2604.07, 2604.46, 40191.377}, + {1748985300000LL, 2604.47, 2610.99, 2600, 2610.98, 36598.493}, + {1748986200000LL, 2610.99, 2614.95, 2598.5, 2600.62, 33464.55}, + {1748987100000LL, 2600.62, 2604.95, 2563, 2588.05, 150145.857}, + {1748988000000LL, 2588.06, 2596.47, 2585.3, 2595.05, 51963.511}, + {1748988900000LL, 2595.04, 2599.69, 2590.22, 2599.56, 23595.982}, + {1748989800000LL, 2599.55, 2607.32, 2597.51, 2606.83, 37220.807}, + {1748990700000LL, 2606.83, 2609.48, 2602.33, 2603.42, 24628.358}, + {1748991600000LL, 2603.42, 2603.9, 2598.18, 2603.39, 25150.512}, + {1748992500000LL, 2603.38, 2603.38, 2589, 2592.45, 32405.859}, + {1748993400000LL, 2592.45, 2596.59, 2587.7, 2590.99, 23755.43}, + {1748994300000LL, 2590.98, 2594.19, 2585.07, 2592.16, 26272.817}, + {1748995200000LL, 2592.16, 2602, 2590, 2593.02, 39685.429}, + {1748996100000LL, 2593.02, 2600, 2592.72, 2593.6, 17227.582}, + {1748997000000LL, 2593.6, 2593.6, 2582.41, 2587.65, 41156.387}, + {1748997900000LL, 2587.64, 2598.7, 2587.16, 2597.91, 28664.85}, + {1748998800000LL, 2597.92, 2612.99, 2597.44, 2610.18, 74542.618}, + {1748999700000LL, 2610.17, 2622.17, 2605.5, 2614.52, 58476.995}, + {1749000600000LL, 2614.51, 2615, 2602.99, 2611.17, 37712.636}, + {1749001500000LL, 2611.18, 2613.77, 2606.48, 2611.27, 22357.212}, + {1749002400000LL, 2611.28, 2619.99, 2610.06, 2614.48, 31306.256}, + {1749003300000LL, 2614.49, 2617.95, 2612, 2613.57, 16119.385}, + {1749004200000LL, 2613.57, 2614.99, 2609.06, 2612.83, 17304.509}, + {1749005100000LL, 2612.82, 2615, 2609.4, 2611.14, 13965.233}, + {1749006000000LL, 2611.13, 2627.04, 2608.67, 2622.98, 63455.14}, + {1749006900000LL, 2622.98, 2639.84, 2622.98, 2633.09, 107170.165}, + {1749007800000LL, 2633.09, 2639.81, 2630.33, 2632.38, 36730.035}, + {1749008700000LL, 2632.37, 2635.25, 2625.6, 2626.78, 43572.31}, + {1749009600000LL, 2626.77, 2632.82, 2623.24, 2627.65, 38086.193}, + {1749010500000LL, 2627.65, 2628.6, 2621.91, 2627.97, 31381.584}, + {1749011400000LL, 2627.97, 2627.97, 2621.58, 2626.54, 26396.467}, + {1749012300000LL, 2626.55, 2637.49, 2625, 2636.47, 43992.781}, + {1749013200000LL, 2636.48, 2644.17, 2630.48, 2632.14, 103771.394}, + {1749014100000LL, 2632.14, 2636.67, 2618.37, 2621.16, 97261.067}, + {1749015000000LL, 2621.16, 2624.5, 2618.33, 2622.32, 39329.086}, + {1749015900000LL, 2622.32, 2628.18, 2621.87, 2623.66, 23708.299}, + {1749016800000LL, 2623.65, 2629.8, 2621.26, 2624.95, 22024.864}, + {1749017700000LL, 2624.96, 2625.13, 2620.21, 2624.69, 20337.034}, + {1749018600000LL, 2624.69, 2633.54, 2621.2, 2630.99, 26587.048}, + {1749019500000LL, 2631, 2633.26, 2627.41, 2628.37, 20559.499}, + {1749020400000LL, 2628.37, 2633.05, 2624.31, 2624.43, 31013.744}, + {1749021300000LL, 2624.44, 2626.35, 2621.13, 2625.96, 29020.41}, + {1749022200000LL, 2625.96, 2627.31, 2622.5, 2623.75, 12050.563}, + {1749023100000LL, 2623.76, 2628.41, 2622.5, 2626.31, 12912.844}, + {1749024000000LL, 2626.32, 2637.77, 2626.32, 2634.78, 49807.64}, + {1749024900000LL, 2634.78, 2645, 2634.06, 2638.06, 90735.9}, + {1749025800000LL, 2638.06, 2642, 2637.78, 2638.98, 33774.155}, + {1749026700000LL, 2638.98, 2640.44, 2638.13, 2639.68, 16654.091}, + {1749027600000LL, 2639.69, 2642.93, 2635.01, 2635.06, 29223.359}, + {1749028500000LL, 2635.06, 2636.52, 2630.16, 2634.86, 38599.471}, + {1749029400000LL, 2634.87, 2637.33, 2630, 2637.17, 21083.85}, + {1749030300000LL, 2637.18, 2647, 2637.17, 2642.64, 61640.156}, + {1749031200000LL, 2642.63, 2649.38, 2639.4, 2647.66, 61994.499}, + {1749032100000LL, 2647.66, 2648.86, 2637.15, 2637.99, 44748.876}, + {1749033000000LL, 2637.99, 2639.68, 2633.73, 2637.85, 30777.181}, + {1749033900000LL, 2637.85, 2638.75, 2634.02, 2637.23, 15219.975}, + {1749034800000LL, 2637.23, 2638.6, 2632.94, 2637.58, 27460.355}, + {1749035700000LL, 2637.59, 2644.06, 2628.11, 2631.72, 56377.985}, + {1749036600000LL, 2631.72, 2631.87, 2625, 2627.97, 47763.064}, + {1749037500000LL, 2627.96, 2630.24, 2623, 2623.71, 36894.647}, + {1749038400000LL, 2623.71, 2637.97, 2622, 2634.17, 75125.108}, + {1749039300000LL, 2634.18, 2634.41, 2614.54, 2618.95, 124408.097}, + {1749040200000LL, 2618.95, 2624.5, 2615.31, 2623.51, 53569.246}, + {1749041100000LL, 2623.5, 2626.25, 2613.52, 2615.72, 59929.048}, + {1749042000000LL, 2615.72, 2618.99, 2606.88, 2610.83, 80008.12}, + {1749042900000LL, 2610.83, 2620, 2606.24, 2613.11, 76544.707}, + {1749043800000LL, 2613.11, 2627.78, 2612.6, 2619.29, 78555.202}, + {1749044700000LL, 2619.3, 2625.5, 2611.7, 2613.93, 50042.155}, + {1749045600000LL, 2613.93, 2613.93, 2593.2, 2605.43, 284042.891}, + {1749046500000LL, 2605.44, 2617.1, 2603.42, 2611.76, 92690.99}, + {1749047400000LL, 2611.76, 2617.94, 2609.44, 2617.51, 44093.592}, + {1749048300000LL, 2617.5, 2642, 2616.6, 2639.31, 163987.885}, + {1749049200000LL, 2639.31, 2646.88, 2632.72, 2644.28, 124533.95}, + {1749050100000LL, 2644.28, 2659.25, 2644.13, 2648.69, 186955.37}, + {1749051000000LL, 2648.7, 2673.81, 2641.37, 2668.47, 214565.628}, + {1749051900000LL, 2668.47, 2679.64, 2662, 2666.91, 168140.491}, + {1749052800000LL, 2666.92, 2668.29, 2655.46, 2658.82, 98560.523}, + {1749053700000LL, 2658.82, 2661.62, 2648.64, 2649.03, 115249.768}, + {1749054600000LL, 2649.02, 2659.95, 2648, 2653.7, 68577.074}, + {1749055500000LL, 2653.7, 2658.87, 2650.1, 2658.72, 40457.074}, + {1749056400000LL, 2658.72, 2658.8, 2648.33, 2652.78, 36658.471}, + {1749057300000LL, 2652.78, 2653.3, 2641.08, 2641.48, 58718.69}, + {1749058200000LL, 2641.47, 2641.47, 2630, 2634.83, 112711.665}, + {1749059100000LL, 2634.84, 2637.89, 2632, 2634.09, 32975.066}, + {1749060000000LL, 2634.09, 2641.34, 2632.14, 2634.75, 39877.16}, + {1749060900000LL, 2634.75, 2640.45, 2634.21, 2635.4, 20127.067}, + {1749061800000LL, 2635.39, 2638.41, 2633.52, 2637.3, 15812.581}, + {1749062700000LL, 2637.29, 2638.58, 2624.04, 2628.01, 48194.605}, + {1749063600000LL, 2628.01, 2630.65, 2623.06, 2626.67, 36075.671}, + {1749064500000LL, 2626.67, 2628.16, 2621.6, 2623.08, 31197.498}, + {1749065400000LL, 2623.07, 2630.94, 2620.73, 2629.81, 24576.093}, + {1749066300000LL, 2629.8, 2632.14, 2622.35, 2622.36, 32026.319}, + {1749067200000LL, 2622.36, 2623.79, 2615.77, 2616.36, 38138.406}, + {1749068100000LL, 2616.35, 2616.89, 2599.06, 2612.09, 172448.133}, + {1749069000000LL, 2612.09, 2613.35, 2604.45, 2609.23, 53868.184}, + {1749069900000LL, 2609.22, 2609.46, 2596.95, 2604.33, 60114.021}, + {1749070800000LL, 2604.33, 2612.91, 2597.89, 2611.08, 40786.277}, + {1749071700000LL, 2611.08, 2611.09, 2603.35, 2608.69, 24986.587}, + {1749072600000LL, 2608.69, 2618.51, 2606.96, 2616.29, 45593.368}, + {1749073500000LL, 2616.29, 2622.49, 2616.28, 2617.78, 31653.313}, + {1749074400000LL, 2617.78, 2620.03, 2607.77, 2612.55, 32413.926}, + {1749075300000LL, 2612.55, 2615.8, 2604.2, 2606, 23403.674}, + {1749076200000LL, 2606.01, 2612.83, 2604.42, 2611.82, 20092.013}, + {1749077100000LL, 2611.82, 2611.83, 2602.34, 2604.31, 21942.334}, + {1749078000000LL, 2604.3, 2615.56, 2602.38, 2606.73, 49791.392}, + {1749078900000LL, 2606.73, 2607.84, 2598.07, 2605.48, 47777.076}, + {1749079800000LL, 2605.47, 2610.93, 2602.62, 2610.92, 23090.944}, + {1749080700000LL, 2610.91, 2611.55, 2605, 2606.62, 18865.257}, + {1749081600000LL, 2606.62, 2612, 2605.81, 2606.69, 25621.402}, + {1749082500000LL, 2606.69, 2609.93, 2600.11, 2607.57, 29806.215}, + {1749083400000LL, 2607.57, 2614.01, 2607.35, 2608.35, 33828.047}, + {1749084300000LL, 2608.35, 2611.89, 2603.2, 2610.77, 26457.673}, + {1749085200000LL, 2610.76, 2614.4, 2610.31, 2613.02, 21275.623}, + {1749086100000LL, 2613.01, 2615, 2605.61, 2608.15, 23418.085}, + {1749087000000LL, 2608.15, 2608.24, 2603.51, 2607.06, 22013.76}, + {1749087900000LL, 2607.04, 2608.7, 2603.11, 2607.62, 14875.353}, + {1749088800000LL, 2607.61, 2618.8, 2607.61, 2617.3, 35034.582}, + {1749089700000LL, 2617.3, 2618.94, 2611.07, 2612.02, 19001.839}, + {1749090600000LL, 2612.02, 2627.94, 2612.01, 2623.76, 66948.503}, + {1749091500000LL, 2623.75, 2626.36, 2618, 2618.91, 35534.585}, + {1749092400000LL, 2618.9, 2625, 2616.01, 2623.01, 26674.328}, + {1749093300000LL, 2623, 2627, 2621.45, 2622.33, 19310.305}, + {1749094200000LL, 2622.34, 2631.68, 2620.05, 2626.4, 46798.012}, + {1749095100000LL, 2626.4, 2628.96, 2623.3, 2624, 19215.292}, + {1749096000000LL, 2624.01, 2628, 2620.41, 2624.64, 18314.074}, + {1749096900000LL, 2624.63, 2631.32, 2624.63, 2627.11, 34813.044}, + {1749097800000LL, 2627.1, 2632.56, 2626, 2631.43, 19549.978}, + {1749098700000LL, 2631.43, 2633.76, 2628.73, 2629.58, 22266.93}, + {1749099600000LL, 2629.58, 2630.54, 2623.43, 2624.45, 23021.784}, + {1749100500000LL, 2624.45, 2626.97, 2618.47, 2620.59, 28049.517}, + {1749101400000LL, 2620.59, 2620.74, 2607.31, 2610.51, 80688.766}, + {1749102300000LL, 2610.5, 2614.22, 2608.11, 2612.08, 33199.361}, + {1749103200000LL, 2612.08, 2614.88, 2608.21, 2611.11, 24258.605}, + {1749104100000LL, 2611.11, 2617.13, 2605.14, 2616.11, 44952.372}, + {1749105000000LL, 2616.11, 2617.16, 2601.5, 2603.38, 61915.961}, + {1749105900000LL, 2603.39, 2606.8, 2601.22, 2603.38, 48141.761}, + {1749106800000LL, 2603.38, 2609.97, 2603.38, 2608.88, 32419.634}, + {1749107700000LL, 2608.88, 2614.56, 2608.88, 2611.55, 30958.47}, + {1749108600000LL, 2611.55, 2611.94, 2603.9, 2604.83, 32748.026}, + {1749109500000LL, 2604.83, 2605.4, 2600, 2603.21, 45480.95}, + {1749110400000LL, 2603.22, 2606.87, 2602, 2604.6, 26992.837}, + {1749111300000LL, 2604.59, 2609.4, 2596, 2607.22, 53203.175}, + {1749112200000LL, 2607.21, 2608.87, 2600.66, 2602.74, 27948.691}, + {1749113100000LL, 2602.74, 2606.66, 2600.67, 2604.23, 19522.496}, + {1749114000000LL, 2604.22, 2609.3, 2604.12, 2606.51, 24913.486}, + {1749114900000LL, 2606.52, 2608.24, 2601, 2604.38, 24482.685}, + {1749115800000LL, 2604.38, 2607.5, 2602.11, 2606.66, 13756.329}, + {1749116700000LL, 2606.66, 2609.38, 2604.6, 2608.79, 22585.618}, + {1749117600000LL, 2608.78, 2612.68, 2607.09, 2611.46, 30617.774}, + {1749118500000LL, 2611.46, 2612, 2600.8, 2601.8, 31347.731}, + {1749119400000LL, 2601.8, 2604.68, 2579.14, 2590.33, 170600.861}, + {1749120300000LL, 2590.33, 2597.43, 2585.66, 2595.64, 66530.322}, + {1749121200000LL, 2595.64, 2599, 2592.26, 2594.38, 35289.115}, + {1749122100000LL, 2594.37, 2604.13, 2592.81, 2602.73, 44833.498}, + {1749123000000LL, 2602.73, 2608.73, 2602.55, 2604.51, 45262.889}, + {1749123900000LL, 2604.5, 2608.69, 2604.5, 2606.64, 22566.97}, + {1749124800000LL, 2606.63, 2619.99, 2604.22, 2618.38, 100219.955}, + {1749125700000LL, 2618.37, 2627.7, 2617.07, 2624.95, 88498.083}, + {1749126600000LL, 2624.95, 2629.99, 2618.4, 2627.63, 75995.5}, + {1749127500000LL, 2627.63, 2640, 2625.68, 2630.27, 148969.408}, + {1749128400000LL, 2630.27, 2634.66, 2620, 2625.3, 87337.559}, + {1749129300000LL, 2625.31, 2631.06, 2620.8, 2628.2, 89382.506}, + {1749130200000LL, 2628.21, 2629.89, 2611.33, 2613.02, 109364.506}, + {1749131100000LL, 2613.02, 2613.65, 2578, 2588.64, 323879.368}, + {1749132000000LL, 2588.65, 2594.39, 2583.58, 2586.35, 137444.616}, + {1749132900000LL, 2586.34, 2588.06, 2560.01, 2581.63, 308650.5}, + {1749133800000LL, 2581.61, 2595.5, 2570.06, 2592.81, 174407.416}, + {1749134700000LL, 2592.8, 2615, 2578.03, 2586.67, 285580.818}, + {1749135600000LL, 2586.66, 2591.88, 2580.81, 2582.23, 65669.98}, + {1749136500000LL, 2582.23, 2601.61, 2580.76, 2599.18, 85103.975}, + {1749137400000LL, 2599.19, 2600.84, 2589.44, 2595.44, 44018.136}, + {1749138300000LL, 2595.45, 2596.27, 2586.99, 2588.34, 40490.151}, + {1749139200000LL, 2588.35, 2590.7, 2579.11, 2579.75, 60543.126}, + {1749140100000LL, 2579.81, 2580.1, 2556.05, 2563.62, 202311.795}, + {1749141000000LL, 2563.61, 2571.41, 2556.68, 2564.6, 106290.015}, + {1749141900000LL, 2564.6, 2565.79, 2550.01, 2558.35, 120486.021}, + {1749142800000LL, 2558.34, 2572.57, 2557.43, 2569.99, 90956.86}, + {1749143700000LL, 2570, 2576.67, 2564.9, 2570.82, 67620.476}, + {1749144600000LL, 2570.81, 2573.9, 2565.68, 2569.21, 23925.021}, + {1749145500000LL, 2569.22, 2570.2, 2562.14, 2569.22, 40029.284}, + {1749146400000LL, 2569.21, 2573.75, 2555.08, 2558.54, 69589.201}, + {1749147300000LL, 2558.55, 2577.63, 2553.9, 2576.68, 72243.036}, + {1749148200000LL, 2576.68, 2583, 2573.22, 2576.19, 69520.189}, + {1749149100000LL, 2576.19, 2581.54, 2571.57, 2575.21, 33633.101}, + {1749150000000LL, 2575.21, 2577.5, 2562.19, 2563.31, 62126.193}, + {1749150900000LL, 2563.3, 2565.44, 2506, 2539, 546141.746}, + {1749151800000LL, 2539, 2549.9, 2535.91, 2541.14, 115937.509}, + {1749152700000LL, 2541.14, 2541.68, 2525.04, 2528.08, 112567.696}, + {1749153600000LL, 2528.08, 2534.79, 2488.46, 2505.57, 247973.574}, + {1749154500000LL, 2505.57, 2506.53, 2437.87, 2472.4, 569063.196}, + {1749155400000LL, 2472.41, 2473.96, 2402.55, 2417.7, 489092.059}, + {1749156300000LL, 2417.7, 2442.97, 2392.32, 2397.53, 395410.279}, + {1749157200000LL, 2397.53, 2428.83, 2390, 2409.84, 270324.82}, + {1749158100000LL, 2409.85, 2425, 2409.01, 2418.3, 100013.999}, + {1749159000000LL, 2418.3, 2446.9, 2413.19, 2440.47, 156125.658}, + {1749159900000LL, 2440.47, 2440.77, 2421.79, 2435.97, 96009.277}, + {1749160800000LL, 2435.97, 2435.97, 2408.49, 2411, 131420.108}, + {1749161700000LL, 2411.01, 2424.74, 2407.5, 2416.88, 56023.358}, + {1749162600000LL, 2416.88, 2427.14, 2415.51, 2423.95, 42699.331}, + {1749163500000LL, 2423.95, 2434.95, 2422.57, 2428.55, 55378.829}, + {1749164400000LL, 2428.55, 2434.8, 2423.37, 2430.07, 64757.494}, + {1749165300000LL, 2430.06, 2431.67, 2417.24, 2420.7, 65818.899}, + {1749166200000LL, 2420.71, 2425, 2417.56, 2420.25, 37530.76}, + {1749167100000LL, 2420.24, 2420.25, 2410, 2412.9, 48778.019}, +}; diff --git a/tests/fixtures/g221_short_seed/hm-g221-short-seed/meta.json b/tests/fixtures/g221_short_seed/hm-g221-short-seed/meta.json new file mode 100644 index 00000000..381ded30 --- /dev/null +++ b/tests/fixtures/g221_short_seed/hm-g221-short-seed/meta.json @@ -0,0 +1,16 @@ +{ + "title": "HM G2-21 short seed", + "slug": "hm-g221-short-seed", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:17:12.049Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-02", + "to": "2025-06-06" + }, + "pine_sha256": "743d8a23dea3b530f1a04af7570387ed57d075150f72de5e6064037ae8a9a04e", + "tv_trades_sha256": "9a842c022746bc393650fc3f09f363808a53f7c078c05dde00f65e408f1734e8", + "tv_trades_rows": 26 +} diff --git a/tests/fixtures/g221_short_seed/hm-g221-short-seed/metrics.json b/tests/fixtures/g221_short_seed/hm-g221-short-seed/metrics.json new file mode 100644 index 00000000..67f61d38 --- /dev/null +++ b/tests/fixtures/g221_short_seed/hm-g221-short-seed/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-g221-short-seed", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-02", + "to": "2025-06-06", + "deepBacktesting": true, + "trades": 13, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "743d8a23dea3b530f1a04af7570387ed57d075150f72de5e6064037ae8a9a04e", + "tvTradesCsvHash": "9a842c022746bc393650fc3f09f363808a53f7c078c05dde00f65e408f1734e8", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74420", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1748822400000, + "to": 1749168000000 + }, + "returnedRange": { + "from": 1748822400000, + "to": 1749168000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "7a3e0a74412ef4ab8a86b9e94884a4bd424ca69b38330d915553d46d5b2af08e", + "decodedReportSha256": "eb7d8b7b7f6d0d6d2e42c4f3cd07bdbe3d275601b3e54654408b6764c5056072", + "closedTrades": 13, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:17:12.049Z" +} diff --git a/tests/fixtures/g221_short_seed/hm-g221-short-seed/strategy.pine b/tests/fixtures/g221_short_seed/hm-g221-short-seed/strategy.pine new file mode 100644 index 00000000..c7793886 --- /dev/null +++ b/tests/fixtures/g221_short_seed/hm-g221-short-seed/strategy.pine @@ -0,0 +1,25 @@ +// H-MEASURE (R5 wave H) row G2-21: finding 272's short-seed collision under +// percent-of-equity default sizing, once a day (UTC). A SHORT seed opens from +// the 00:15 signal; the 00:30 bar then sends, in this order, +// entry(Long), entry(Short), close(Long), close(Short); the remnant is closed +// from the 01:00 signal and anything left from the 01:30 signal. TradingView +// numbers the trades of the collision fill; the engine's report numbers them +// by entry incarnation (project_short_seed_report_rows). +//@version=6 +strategy("HM G2-21 short seed", overlay=true, initial_capital=1000000, + default_qty_type=strategy.percent_of_equity, default_qty_value=10, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +m = hour * 60 + minute +if m == 15 and strategy.position_size == 0 + strategy.entry("Short", strategy.short) +if m == 30 and strategy.position_size < 0 + strategy.entry("Long", strategy.long) + strategy.entry("Short", strategy.short) + strategy.close("Long") + strategy.close("Short") +if m == 60 and strategy.position_size != 0 + strategy.close("Short") +if m == 90 and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/g221_short_seed/hm-g221-short-seed/tv_trades.csv b/tests/fixtures/g221_short_seed/hm-g221-short-seed/tv_trades.csv new file mode 100644 index 00000000..ad13156e --- /dev/null +++ b/tests/fixtures/g221_short_seed/hm-g221-short-seed/tv_trades.csv @@ -0,0 +1,27 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-06-02 08:45,Long,2533.24,39.5353,99999.797114,-152.60626,-0.15,0,166.44362,0.17,-161.30403,-0.16,-152.60626,-0.02,1 +1,Entry short,2025-06-02 08:30,Short,2529.38,39.5353,99999.797114,-152.60626,-0.15,0,166.44362,0.17,-161.30403,-0.16,-152.60626,-0.02,1 +2,Exit long,2025-06-02 08:45,Short,2533.24,39.4691,99984.702884,0,0.00,0,0,0.00,0,0.00,-152.60626,-0.02,0 +2,Entry long,2025-06-02 08:45,Long,2533.24,39.4691,99984.702884,0,0.00,0,0,0.00,0,0.00,-152.60626,-0.02,0 +3,Exit long,2025-06-02 08:45,Short,2533.24,39.4691,99984.702884,0,0.00,0,0,0.00,0,0.00,-152.60626,-0.02,0 +3,Entry long,2025-06-02 08:45,Close entry(s) order Short,2533.24,39.4691,99984.702884,0,0.00,0,0,0.00,0,0.00,-152.60626,-0.02,0 +4,Exit short,2025-06-03 08:45,Long,2618.8,38.2482,99984.61962,-179.76654,-0.18,0,0,0.00,-449.41635,-0.45,-332.3728,-0.03,1 +4,Entry short,2025-06-03 08:30,Short,2614.1,38.2482,99984.61962,-179.76654,-0.18,0,0,0.00,-449.41635,-0.45,-332.3728,-0.03,1 +5,Exit long,2025-06-03 08:45,Short,2618.8,38.1727,99966.66676,0,0.00,0,0,0.00,0,0.00,-332.3728,-0.03,0 +5,Entry long,2025-06-03 08:45,Long,2618.8,38.1727,99966.66676,0,0.00,0,0,0.00,0,0.00,-332.3728,-0.03,0 +6,Exit long,2025-06-03 08:45,Short,2618.8,38.1727,99966.66676,0,0.00,0,0,0.00,0,0.00,-332.3728,-0.03,0 +6,Entry long,2025-06-03 08:45,Close entry(s) order Short,2618.8,38.1727,99966.66676,0,0.00,0,0,0.00,0,0.00,-332.3728,-0.03,0 +7,Exit short,2025-06-04 08:45,Long,2587.64,38.5436,99966.68096,229.71985,0.23,0,431.3029,0.43,0,0.00,-102.65294,-0.01,1 +7,Entry short,2025-06-04 08:30,Short,2593.6,38.5436,99966.68096,229.71985,0.23,0,431.3029,0.43,0,0.00,-102.65294,-0.01,1 +8,Exit long,2025-06-04 08:45,Short,2587.64,38.6411,99989.256004,0,0.00,0,0,0.00,0,0.00,-102.65294,-0.01,0 +8,Entry long,2025-06-04 08:45,Long,2587.64,38.6411,99989.256004,0,0.00,0,0,0.00,0,0.00,-102.65294,-0.01,0 +9,Exit long,2025-06-04 08:45,Short,2587.64,38.5436,99736.961104,0,0.00,0,0,0.00,0,0.00,-102.65294,-0.01,0 +9,Entry long,2025-06-04 08:45,Close entry(s) order Short,2587.64,38.5436,99736.961104,0,0.00,0,0,0.00,0,0.00,-102.65294,-0.01,0 +10,Exit short,2025-06-04 09:15,Close entry(s) order Short,2610.17,0.0975,252.2949,-2.196675,-0.87,0,0.0468,0.02,-2.471625,-0.98,-104.84962,-0.01,2 +10,Entry short,2025-06-04 08:45,Short,2587.64,0.0975,252.2949,-2.196675,-0.87,0,0.0468,0.02,-2.471625,-0.98,-104.84962,-0.01,2 +11,Exit short,2025-06-05 08:45,Long,2608.35,38.3458,99989.357706,-29.909723,-0.03,0,8.436076,0.01,-246.94695,-0.25,-134.75934,-0.01,1 +11,Entry short,2025-06-05 08:30,Short,2607.57,38.3458,99989.357706,-29.909723,-0.03,0,8.436076,0.01,-246.94695,-0.25,-134.75934,-0.01,1 +12,Exit long,2025-06-05 08:45,Short,2608.35,38.3332,99986.40222,0,0.00,0,0,0.00,0,0.00,-134.75934,-0.01,0 +12,Entry long,2025-06-05 08:45,Long,2608.35,38.3332,99986.40222,0,0.00,0,0,0.00,0,0.00,-134.75934,-0.01,0 +13,Exit long,2025-06-05 08:45,Short,2608.35,38.3332,99986.40222,0,0.00,0,0,0.00,0,0.00,-134.75934,-0.01,0 +13,Entry long,2025-06-05 08:45,Close entry(s) order Short,2608.35,38.3332,99986.40222,0,0.00,0,0,0.00,0,0.00,-134.75934,-0.01,0 \ No newline at end of file diff --git a/tests/fixtures/g232_decimal_dust/README.md b/tests/fixtures/g232_decimal_dust/README.md new file mode 100644 index 00000000..0db6c08b --- /dev/null +++ b/tests/fixtures/g232_decimal_dust/README.md @@ -0,0 +1,19 @@ +# Decimal-dust tape (R5 wave H, lane H-MEASURE, row G2-32) + +Every day: long lots 0.1, 0.2 and 1.0, one `strategy.order` sell of 0.3, +then `strategy.close_all`. TradingView's decimal quantities close the 0.1 and +0.2 lots whole: three rows a day. In binary64 `0.1 + 0.2 > 0.3`, so the +kernel's FIFO split leaves a 2.8e-17 remnant of the 0.2 lot beside the 1.0 lot; +the Pine source host's 1e-10 sweep (`PineStrategyHost::on_native_applied`) +erases it and books TradingView's rows, a bare host keeps it and closes it as +a fourth row. `tests/test_pine_dust_sweep_paired.cpp`. + +| tape (`lab tv`, ws-report-v1, rangeProof covered) | chart | window | trades | tv_trades.csv sha256 | campaign note | +|---|---|---|---|---|---| +| `hm-g232-decimal-dust` | BINANCE:ETHUSDT.P 15 | 2025-06-02 .. 2025-06-06 | 12 | `3a7568a1da42bff6e93eb212a5453116f468e6bb56cae0fe7ae2e3c9929b4e64` | `tv-tape-hm-g232-decimal-dust-3a7568a1` | + +`bars.inc` (`exec/H-MEASURE-scratch/report/row3-dust/gen_bars.py`): the corpus +15m chart feed (sha256 +`27b62431096edf1bfba71b2409f8dc183f69213dec2834560b3241750f8e7026`, derived +from the corpus 1m feed `db8c1332…`), 2025-06-02 .. 05, 00:00 .. 02:30 UTC +(eleven bars a day; fixed quantities, so each day replays alone). diff --git a/tests/fixtures/g232_decimal_dust/bars.inc b/tests/fixtures/g232_decimal_dust/bars.inc new file mode 100644 index 00000000..61158ca0 --- /dev/null +++ b/tests/fixtures/g232_decimal_dust/bars.inc @@ -0,0 +1,58 @@ +// Generated by exec/H-MEASURE-scratch/report/row3-dust/gen_bars.py from the corpus +// 15m chart feed ohlcv_ETH-USDT-USDT_15m.csv sha256 27b62431096edf1bfba71b2409f8dc183f69213dec2834560b3241750f8e7026. +// 2025-06-02 00:00 .. 02:30 UTC +static const FeedBar kDay0[] = { + {1748822400000LL, 2537.89, 2539.56, 2533.53, 2535, 27293.799}, + {1748823300000LL, 2535, 2538.74, 2527.33, 2529.38, 48183.913}, + {1748824200000LL, 2529.38, 2533.46, 2525.17, 2533.24, 29590.856}, + {1748825100000LL, 2533.24, 2533.44, 2528.9, 2533.2, 14329.399}, + {1748826000000LL, 2533.21, 2540.25, 2532.96, 2536.54, 27319.343}, + {1748826900000LL, 2536.53, 2540.89, 2528.24, 2529.69, 38666.805}, + {1748827800000LL, 2529.7, 2531.24, 2525.18, 2526.52, 26871.639}, + {1748828700000LL, 2526.53, 2530.19, 2523.66, 2525, 24800.534}, + {1748829600000LL, 2525, 2526.36, 2514.1, 2516.72, 44134.929}, + {1748830500000LL, 2516.72, 2522.27, 2512.24, 2518.61, 29567.662}, + {1748831400000LL, 2518.61, 2521.91, 2513.64, 2519.79, 22872.839}, +}; +// 2025-06-03 00:00 .. 02:30 UTC +static const FeedBar kDay1[] = { + {1748908800000LL, 2606.49, 2616.88, 2600.28, 2615.07, 98812.201}, + {1748909700000LL, 2615.08, 2622.67, 2612.8, 2614.1, 109024.481}, + {1748910600000LL, 2614.1, 2625.85, 2614.1, 2618.8, 70426.42}, + {1748911500000LL, 2618.8, 2622.39, 2615.51, 2620.79, 32869.737}, + {1748912400000LL, 2620.79, 2626.96, 2617.1, 2618.33, 58796.824}, + {1748913300000LL, 2618.34, 2627.99, 2615.97, 2624.49, 56590.913}, + {1748914200000LL, 2624.48, 2641.98, 2624.08, 2636.84, 120481.335}, + {1748915100000LL, 2636.84, 2649, 2635, 2644.93, 113262.872}, + {1748916000000LL, 2644.94, 2647.19, 2635.38, 2638.54, 88326.262}, + {1748916900000LL, 2638.54, 2639.74, 2630, 2630.03, 61904.575}, + {1748917800000LL, 2630.03, 2634, 2624.45, 2626.5, 53651.541}, +}; +// 2025-06-04 00:00 .. 02:30 UTC +static const FeedBar kDay2[] = { + {1748995200000LL, 2592.16, 2602, 2590, 2593.02, 39685.429}, + {1748996100000LL, 2593.02, 2600, 2592.72, 2593.6, 17227.582}, + {1748997000000LL, 2593.6, 2593.6, 2582.41, 2587.65, 41156.387}, + {1748997900000LL, 2587.64, 2598.7, 2587.16, 2597.91, 28664.85}, + {1748998800000LL, 2597.92, 2612.99, 2597.44, 2610.18, 74542.618}, + {1748999700000LL, 2610.17, 2622.17, 2605.5, 2614.52, 58476.995}, + {1749000600000LL, 2614.51, 2615, 2602.99, 2611.17, 37712.636}, + {1749001500000LL, 2611.18, 2613.77, 2606.48, 2611.27, 22357.212}, + {1749002400000LL, 2611.28, 2619.99, 2610.06, 2614.48, 31306.256}, + {1749003300000LL, 2614.49, 2617.95, 2612, 2613.57, 16119.385}, + {1749004200000LL, 2613.57, 2614.99, 2609.06, 2612.83, 17304.509}, +}; +// 2025-06-05 00:00 .. 02:30 UTC +static const FeedBar kDay3[] = { + {1749081600000LL, 2606.62, 2612, 2605.81, 2606.69, 25621.402}, + {1749082500000LL, 2606.69, 2609.93, 2600.11, 2607.57, 29806.215}, + {1749083400000LL, 2607.57, 2614.01, 2607.35, 2608.35, 33828.047}, + {1749084300000LL, 2608.35, 2611.89, 2603.2, 2610.77, 26457.673}, + {1749085200000LL, 2610.76, 2614.4, 2610.31, 2613.02, 21275.623}, + {1749086100000LL, 2613.01, 2615, 2605.61, 2608.15, 23418.085}, + {1749087000000LL, 2608.15, 2608.24, 2603.51, 2607.06, 22013.76}, + {1749087900000LL, 2607.04, 2608.7, 2603.11, 2607.62, 14875.353}, + {1749088800000LL, 2607.61, 2618.8, 2607.61, 2617.3, 35034.582}, + {1749089700000LL, 2617.3, 2618.94, 2611.07, 2612.02, 19001.839}, + {1749090600000LL, 2612.02, 2627.94, 2612.01, 2623.76, 66948.503}, +}; diff --git a/tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/meta.json b/tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/meta.json new file mode 100644 index 00000000..ec0c87cb --- /dev/null +++ b/tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/meta.json @@ -0,0 +1,16 @@ +{ + "title": "HM G2-32 decimal dust", + "slug": "hm-g232-decimal-dust", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:10:55.236Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-02", + "to": "2025-06-06" + }, + "pine_sha256": "e9acfa7dac32e8ec5539c56c99fdee635e4cdabb88ee6e712fc637626be05f30", + "tv_trades_sha256": "3a7568a1da42bff6e93eb212a5453116f468e6bb56cae0fe7ae2e3c9929b4e64", + "tv_trades_rows": 24 +} diff --git a/tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/metrics.json b/tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/metrics.json new file mode 100644 index 00000000..89955b8a --- /dev/null +++ b/tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-g232-decimal-dust", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-02", + "to": "2025-06-06", + "deepBacktesting": true, + "trades": 12, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "e9acfa7dac32e8ec5539c56c99fdee635e4cdabb88ee6e712fc637626be05f30", + "tvTradesCsvHash": "3a7568a1da42bff6e93eb212a5453116f468e6bb56cae0fe7ae2e3c9929b4e64", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74415", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1748822400000, + "to": 1749168000000 + }, + "returnedRange": { + "from": 1748822400000, + "to": 1749168000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "fe6f03c74ad972289414e0e7cf5f7a220459c0cac3347cf75a76899618a7ab4f", + "decodedReportSha256": "18fc008a045f603957eedb0bdc956b8c87e04456ff2e02a201e5e44ef6a08728", + "closedTrades": 12, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:10:55.236Z" +} diff --git a/tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/strategy.pine b/tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/strategy.pine new file mode 100644 index 00000000..fa237d90 --- /dev/null +++ b/tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/strategy.pine @@ -0,0 +1,24 @@ +// H-MEASURE (R5 wave H) row G2-32: a decimal close that binary64 cannot reach. +// Every day (UTC): long lots A 0.1 (00:15 signal), B 0.2 (00:30) and C 1.0 +// (00:45); at 01:00 one strategy.order sells 0.3, which TradingView closes as +// A and B whole (decimal arithmetic: 0.1 + 0.2 == 0.3). In binary64 +// 0.1 + 0.2 = 0.30000000000000004 > 0.3, so the kernel's FIFO split leaves a +// 2.8e-17 remnant of B beside C. At 02:00 close_all flattens what is left: +// on TradingView that is C alone, one row. +//@version=6 +strategy("HM G2-32 decimal dust", overlay=true, initial_capital=1000000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=3, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=false, margin_long=100, margin_short=100) + +m = hour * 60 + minute +if m == 15 and strategy.position_size == 0 + strategy.entry("A", strategy.long, qty=0.1) +if m == 30 and strategy.position_size > 0 + strategy.entry("B", strategy.long, qty=0.2) +if m == 45 and strategy.position_size > 0 + strategy.entry("C", strategy.long, qty=1.0) +if m == 60 and strategy.position_size > 0 + strategy.order("S", strategy.short, qty=0.3) +if m == 120 and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/tv_trades.csv b/tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/tv_trades.csv new file mode 100644 index 00000000..87616651 --- /dev/null +++ b/tests/fixtures/g232_decimal_dust/hm-g232-decimal-dust/tv_trades.csv @@ -0,0 +1,25 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-02 09:15,S,2536.53,0.1,252.938,0.715,0.28,0,1.087,0.43,-0.421,-0.17,0.715,0.00,3 +1,Entry long,2025-06-02 08:30,A,2529.38,0.1,252.938,0.715,0.28,0,1.087,0.43,-0.421,-0.17,0.715,0.00,3 +2,Exit long,2025-06-02 09:15,S,2536.53,0.2,506.648,0.658,0.13,0,1.402,0.28,-0.868,-0.17,1.373,0.00,2 +2,Entry long,2025-06-02 08:45,B,2533.24,0.2,506.648,0.658,0.13,0,1.402,0.28,-0.868,-0.17,1.373,0.00,2 +3,Exit long,2025-06-02 10:15,Close position order,2516.72,1,2533.21,-16.49,-0.65,0,7.68,0.30,-19.11,-0.75,-15.117,0.00,5 +3,Entry long,2025-06-02 09:00,C,2533.21,1,2533.21,-16.49,-0.65,0,7.68,0.30,-19.11,-0.75,-15.117,0.00,5 +4,Exit long,2025-06-03 09:15,S,2618.34,0.1,261.41,0.424,0.16,0,1.286,0.49,0,0.00,-14.693,0.00,3 +4,Entry long,2025-06-03 08:30,A,2614.1,0.1,261.41,0.424,0.16,0,1.286,0.49,0,0.00,-14.693,0.00,3 +5,Exit long,2025-06-03 09:15,S,2618.34,0.2,523.76,-0.092,-0.02,0,1.632,0.31,-0.658,-0.13,-14.785,0.00,2 +5,Entry long,2025-06-03 08:45,B,2618.8,0.2,523.76,-0.092,-0.02,0,1.632,0.31,-0.658,-0.13,-14.785,0.00,2 +6,Exit long,2025-06-03 10:15,Close position order,2638.54,1,2620.79,17.75,0.68,0,28.21,1.08,-4.82,-0.18,2.965,0.00,5 +6,Entry long,2025-06-03 09:00,C,2620.79,1,2620.79,17.75,0.68,0,28.21,1.08,-4.82,-0.18,2.965,0.00,5 +7,Exit long,2025-06-04 09:15,S,2610.17,0.1,259.36,1.657,0.64,0,1.939,0.75,-1.119,-0.43,4.622,0.00,3 +7,Entry long,2025-06-04 08:30,A,2593.6,0.1,259.36,1.657,0.64,0,1.939,0.75,-1.119,-0.43,4.622,0.00,3 +8,Exit long,2025-06-04 09:15,S,2610.17,0.2,517.528,4.506,0.87,0,5.07,0.98,-0.096,-0.02,9.128,0.00,2 +8,Entry long,2025-06-04 08:45,B,2587.64,0.2,517.528,4.506,0.87,0,5.07,0.98,-0.096,-0.02,9.128,0.00,2 +9,Exit long,2025-06-04 10:15,Close position order,2614.49,1,2597.92,16.57,0.64,0,24.25,0.93,-0.48,-0.02,25.698,0.00,5 +9,Entry long,2025-06-04 09:00,C,2597.92,1,2597.92,16.57,0.64,0,24.25,0.93,-0.48,-0.02,25.698,0.00,5 +10,Exit long,2025-06-05 09:15,S,2613.01,0.1,260.757,0.544,0.21,0,0.683,0.26,-0.437,-0.17,26.242,0.00,3 +10,Entry long,2025-06-05 08:30,A,2607.57,0.1,260.757,0.544,0.21,0,0.683,0.26,-0.437,-0.17,26.242,0.00,3 +11,Exit long,2025-06-05 09:15,S,2613.01,0.2,521.67,0.932,0.18,0,1.21,0.23,-1.03,-0.20,27.174,0.00,2 +11,Entry long,2025-06-05 08:45,B,2608.35,0.2,521.67,0.932,0.18,0,1.21,0.23,-1.03,-0.20,27.174,0.00,2 +12,Exit long,2025-06-05 10:15,Close position order,2617.3,1,2610.76,6.54,0.25,0,8.04,0.31,-7.65,-0.29,33.714,0.00,5 +12,Entry long,2025-06-05 09:00,C,2610.76,1,2610.76,6.54,0.25,0,8.04,0.31,-7.65,-0.29,33.714,0.00,5 \ No newline at end of file diff --git a/tests/fixtures/session_ismarket/README.md b/tests/fixtures/session_ismarket/README.md new file mode 100644 index 00000000..69ae9d8a --- /dev/null +++ b/tests/fixtures/session_ismarket/README.md @@ -0,0 +1,27 @@ +# session.ismarket tapes (R5 wave H, lane H-MEASURE, row G2-36) + +TradingView's own `session.ismarket` on every chart bar. The probe +(`strategy.pine`, the same in every directory) reverses its position at the +close of every bar (`process_orders_on_close=true`), so every entry is one +chart bar, dated at its open, and its Signal is the flag TradingView +evaluated there: `M1` in market, `M0` not. Each directory is one `lab tv` +export, byte-identical (`strategy.pine`, `tv_trades.csv` with times UTC+8, +`meta.json`, `metrics.json`); `metrics.json` `tvTradesCsvHash` is the sha256. + +| tape | chart | window | bars (entries) | flags | tv_trades.csv sha256 | campaign note | +|---|---|---|---|---|---|---| +| `hm-g236-es1-60-dst-mar` | CME_MINI:ES1! 60 | 2025-03-02 .. 03-14 (US DST 03-09) | 210 | all `M1` | `c1a6b73b032a51b6e8007d6fdbe4b59dc6aaa09ff7eafdceab7914ff46c9443d` | `tv-tape-hm-g236-es1-60-dst-mar-c1a6b73b` | +| `hm-g236-es1-60-dst-nov` | CME_MINI:ES1! 60 | 2025-10-26 .. 11-07 (US DST 11-02) | 209 | all `M1` | `a7fbd62928dc49caf7bcdc6c9d8569139eab036860aa375aa0fc2c7db9b408b9` | `tv-tape-hm-g236-es1-60-dst-nov-a7fbd629` | +| `hm-g236-es1-60-thanksgiving` | CME_MINI:ES1! 60 | 2025-11-23 .. 12-05 | 192 | all `M1` | `52512b7df99b764b8f13f8101b7e37481bdad5dfbf86620868f11fe175efaf8f` | `tv-tape-hm-g236-es1-60-thanksgiving-52512b7d` | +| `hm-g236-eurusd-60-dst-mar` | OANDA:EURUSD 60 | 2025-03-02 .. 03-14 | 220 | all `M1` | `60713d2c22442ffe14070fddb1c5cd7fbf6fc6054c0b532b4c4275de305decf1` | `tv-tape-hm-g236-eurusd-60-dst-mar-60713d2c` | +| `hm-g236-xauusd-60-dst-mar` | OANDA:XAUUSD 60 | 2025-03-02 .. 03-14 | 210 | all `M1` | `ebc226aea010125fb66b7cda3a86563846a243463723816ee529f8f044c54cf6` | `tv-tape-hm-g236-xauusd-60-dst-mar-ebc226ae` | +| `hm-g236-eth-60-24x7` | BINANCE:ETHUSDT.P 60 | 2025-03-07 .. 03-11 | 97 | all `M1` | `96b7e8890b41a9e8aa60698a43cdffd5f42c55bb8d2d0223f7985c13b5040b74` | `tv-tape-hm-g236-eth-60-24x7-96b7e889` | + +Every tape is rangeProof `covered`. The flag is a function of the bar's time +and the symbol's session and timezone only, so +`tests/test_session_ismarket_tape.cpp` replays each tape on flat bars stamped +at the tape's own entry times. The session strings it replays are the +campaign's lane facts (pineforge-lab `config/symbol-lanes-v1.json`: ES1! +`1700-1600` America/Chicago, EURUSD `1700-1700` and XAUUSD `1800-1700` +America/New_York, crypto `24x7`), the same sessions with TradingView's +weekday mask `:23456`, and a 24-hour day spelled `0000-2400` and `0000-0000`. diff --git a/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/meta.json b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/meta.json new file mode 100644 index 00000000..7f72931b --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/meta.json @@ -0,0 +1,16 @@ +{ + "title": "HM G2-36 session.ismarket", + "slug": "hm-g236-es1-60-dst-mar", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:00:57.146Z", + "chart": { + "symbol": "CME_MINI:ES1!", + "interval": "60", + "from": "2025-03-02", + "to": "2025-03-14" + }, + "pine_sha256": "7c5b7e36130c230e6ad26f5525ab527486a008e2d6eaa1a5e41da41cfa256a1d", + "tv_trades_sha256": "c1a6b73b032a51b6e8007d6fdbe4b59dc6aaa09ff7eafdceab7914ff46c9443d", + "tv_trades_rows": 420 +} diff --git a/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/metrics.json b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/metrics.json new file mode 100644 index 00000000..d5be06f8 --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-g236-es1-60-dst-mar", + "symbol": "CME_MINI:ES1!", + "interval": "60", + "from": "2025-03-02", + "to": "2025-03-14", + "deepBacktesting": true, + "trades": 210, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "7c5b7e36130c230e6ad26f5525ab527486a008e2d6eaa1a5e41da41cfa256a1d", + "tvTradesCsvHash": "c1a6b73b032a51b6e8007d6fdbe4b59dc6aaa09ff7eafdceab7914ff46c9443d", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74408", + "symbol": "CME_MINI:ES1!", + "interval": "60", + "requestedRange": { + "from": 1740873600000, + "to": 1741910400000 + }, + "returnedRange": { + "from": 1740956400000, + "to": 1741910400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "dd2a73e6d9048e68a73c3b403e5252328ffc51d40f35495673e8f8b91bba5d13", + "decodedReportSha256": "0a4ea6224dffe15b4e453a11b5a70f6f6243737d2e90fccc16edcca560c2072c", + "closedTrades": 210, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:00:57.145Z" +} diff --git a/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/strategy.pine b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/strategy.pine new file mode 100644 index 00000000..182c911e --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/strategy.pine @@ -0,0 +1,17 @@ +// H-MEASURE (R5 wave H) row G2-36: which chart bars does TradingView flag +// session.ismarket on? process_orders_on_close=true: every bar reverses the +// position at its own close, so every trade's ENTRY is one chart bar (TradingView +// dates a process_orders_on_close fill at the bar's open) and the entry's Signal +// (its comment) is the flag TradingView evaluated on that bar: "M1" when +// session.ismarket is true, "M0" when it is false. +//@version=6 +strategy("HM G2-36 session.ismarket", overlay=true, initial_capital=1000000000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=true, margin_long=100, margin_short=100) + +flag = session.ismarket ? "M1" : "M0" +if bar_index % 2 == 0 + strategy.entry("L" + flag, strategy.long, comment=flag) +else + strategy.entry("S" + flag, strategy.short, comment=flag) diff --git a/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/tv_trades.csv b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/tv_trades.csv new file mode 100644 index 00000000..813bf8c0 --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-mar/tv_trades.csv @@ -0,0 +1,421 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-03-03 08:00,M1,5955.25,1,298712.5,-950,-0.32,0,75,0.03,-1362.5,-0.46,-950,0.00,1 +1,Entry long,2025-03-03 07:00,M1,5974.25,1,298712.5,-950,-0.32,0,75,0.03,-1362.5,-0.46,-950,0.00,1 +2,Exit short,2025-03-03 09:00,M1,5965.25,1,297762.5,-500,-0.17,0,187.5,0.06,-575,-0.19,-1450,0.00,1 +2,Entry short,2025-03-03 08:00,M1,5955.25,1,297762.5,-500,-0.17,0,187.5,0.06,-575,-0.19,-1450,0.00,1 +3,Exit long,2025-03-03 10:00,M1,5964.25,1,298262.5,-50,-0.02,0,187.5,0.06,-275,-0.09,-1500,0.00,1 +3,Entry long,2025-03-03 09:00,M1,5965.25,1,298262.5,-50,-0.02,0,187.5,0.06,-275,-0.09,-1500,0.00,1 +4,Exit short,2025-03-03 11:00,M1,5965,1,298212.5,-37.5,-0.01,0,337.5,0.11,-62.5,-0.02,-1537.5,0.00,1 +4,Entry short,2025-03-03 10:00,M1,5964.25,1,298212.5,-37.5,-0.01,0,337.5,0.11,-62.5,-0.02,-1537.5,0.00,1 +5,Exit long,2025-03-03 12:00,M1,5972,1,298250,350,0.12,0,375,0.13,-37.5,-0.01,-1187.5,0.00,1 +5,Entry long,2025-03-03 11:00,M1,5965,1,298250,350,0.12,0,375,0.13,-37.5,-0.01,-1187.5,0.00,1 +6,Exit short,2025-03-03 13:00,M1,5977.25,1,298600,-262.5,-0.09,0,62.5,0.02,-362.5,-0.12,-1450,0.00,1 +6,Entry short,2025-03-03 12:00,M1,5972,1,298600,-262.5,-0.09,0,62.5,0.02,-362.5,-0.12,-1450,0.00,1 +7,Exit long,2025-03-03 14:00,M1,5979.5,1,298862.5,112.5,0.04,0,250,0.08,-275,-0.09,-1337.5,0.00,1 +7,Entry long,2025-03-03 13:00,M1,5977.25,1,298862.5,112.5,0.04,0,250,0.08,-275,-0.09,-1337.5,0.00,1 +8,Exit short,2025-03-03 15:00,M1,5971.25,1,298975,412.5,0.14,0,737.5,0.25,-62.5,-0.02,-925,0.00,1 +8,Entry short,2025-03-03 14:00,M1,5979.5,1,298975,412.5,0.14,0,737.5,0.25,-62.5,-0.02,-925,0.00,1 +9,Exit long,2025-03-03 16:00,M1,5966.75,1,298562.5,-225,-0.08,0,362.5,0.12,-775,-0.26,-1150,0.00,1 +9,Entry long,2025-03-03 15:00,M1,5971.25,1,298562.5,-225,-0.08,0,362.5,0.12,-775,-0.26,-1150,0.00,1 +10,Exit short,2025-03-03 17:00,M1,5982,1,298337.5,-762.5,-0.26,0,225,0.08,-787.5,-0.26,-1912.5,0.00,1 +10,Entry short,2025-03-03 16:00,M1,5966.75,1,298337.5,-762.5,-0.26,0,225,0.08,-787.5,-0.26,-1912.5,0.00,1 +11,Exit long,2025-03-03 18:00,M1,5982,1,299100,0,0.00,0,75,0.03,-362.5,-0.12,-1912.5,0.00,1 +11,Entry long,2025-03-03 17:00,M1,5982,1,299100,0,0.00,0,75,0.03,-362.5,-0.12,-1912.5,0.00,1 +12,Exit short,2025-03-03 19:00,M1,5988.5,1,299100,-325,-0.11,0,62.5,0.02,-362.5,-0.12,-2237.5,0.00,1 +12,Entry short,2025-03-03 18:00,M1,5982,1,299100,-325,-0.11,0,62.5,0.02,-362.5,-0.12,-2237.5,0.00,1 +13,Exit long,2025-03-03 20:00,M1,5992,1,299425,175,0.06,0,600,0.20,-112.5,-0.04,-2062.5,0.00,1 +13,Entry long,2025-03-03 19:00,M1,5988.5,1,299425,175,0.06,0,600,0.20,-112.5,-0.04,-2062.5,0.00,1 +14,Exit short,2025-03-03 21:00,M1,5985.25,1,299600,337.5,0.11,0,587.5,0.20,-62.5,-0.02,-1725,0.00,1 +14,Entry short,2025-03-03 20:00,M1,5992,1,299600,337.5,0.11,0,587.5,0.20,-62.5,-0.02,-1725,0.00,1 +15,Exit long,2025-03-03 22:00,M1,5951.5,1,299262.5,-1687.5,-0.56,0,587.5,0.20,-1875,-0.63,-3412.5,0.00,1 +15,Entry long,2025-03-03 21:00,M1,5985.25,1,299262.5,-1687.5,-0.56,0,587.5,0.20,-1875,-0.63,-3412.5,0.00,1 +16,Exit short,2025-03-03 23:00,M1,5947.25,1,297575,212.5,0.07,0,1050,0.35,-1162.5,-0.39,-3200,0.00,1 +16,Entry short,2025-03-03 22:00,M1,5951.5,1,297575,212.5,0.07,0,1050,0.35,-1162.5,-0.39,-3200,0.00,1 +17,Exit long,2025-03-04 00:00,M1,5953.5,1,297362.5,312.5,0.11,0,1075,0.36,-637.5,-0.21,-2887.5,0.00,1 +17,Entry long,2025-03-03 23:00,M1,5947.25,1,297362.5,312.5,0.11,0,1075,0.36,-637.5,-0.21,-2887.5,0.00,1 +18,Exit short,2025-03-04 01:00,M1,5943.25,1,297675,512.5,0.17,0,812.5,0.27,-600,-0.20,-2375,0.00,1 +18,Entry short,2025-03-04 00:00,M1,5953.5,1,297675,512.5,0.17,0,812.5,0.27,-600,-0.20,-2375,0.00,1 +19,Exit long,2025-03-04 02:00,M1,5914.75,1,297162.5,-1425,-0.48,0,75,0.03,-2275,-0.77,-3800,0.00,1 +19,Entry long,2025-03-04 01:00,M1,5943.25,1,297162.5,-1425,-0.48,0,75,0.03,-2275,-0.77,-3800,0.00,1 +20,Exit short,2025-03-04 03:00,M1,5883.5,1,295737.5,1562.5,0.53,0,1612.5,0.55,-737.5,-0.25,-2237.5,0.00,1 +20,Entry short,2025-03-04 02:00,M1,5914.75,1,295737.5,1562.5,0.53,0,1612.5,0.55,-737.5,-0.25,-2237.5,0.00,1 +21,Exit long,2025-03-04 04:00,M1,5862.75,1,294175,-1037.5,-0.35,0,37.5,0.01,-3087.5,-1.05,-3275,0.00,1 +21,Entry long,2025-03-04 03:00,M1,5883.5,1,294175,-1037.5,-0.35,0,37.5,0.01,-3087.5,-1.05,-3275,0.00,1 +22,Exit short,2025-03-04 05:00,M1,5875,1,293137.5,-612.5,-0.21,0,87.5,0.03,-675,-0.23,-3887.5,0.00,1 +22,Entry short,2025-03-04 04:00,M1,5862.75,1,293137.5,-612.5,-0.21,0,87.5,0.03,-675,-0.23,-3887.5,0.00,1 +23,Exit long,2025-03-04 07:00,M1,5870.25,1,293750,-237.5,-0.08,0,300,0.10,-375,-0.13,-4125,0.00,1 +23,Entry long,2025-03-04 05:00,M1,5875,1,293750,-237.5,-0.08,0,300,0.10,-375,-0.13,-4125,0.00,1 +24,Exit short,2025-03-04 08:00,M1,5877,1,293512.5,-337.5,-0.11,0,437.5,0.15,-462.5,-0.16,-4462.5,0.00,1 +24,Entry short,2025-03-04 07:00,M1,5870.25,1,293512.5,-337.5,-0.11,0,437.5,0.15,-462.5,-0.16,-4462.5,0.00,1 +25,Exit long,2025-03-04 09:00,M1,5861.25,1,293850,-787.5,-0.27,0,225,0.08,-875,-0.30,-5250,0.00,1 +25,Entry long,2025-03-04 08:00,M1,5877,1,293850,-787.5,-0.27,0,225,0.08,-875,-0.30,-5250,0.00,1 +26,Exit short,2025-03-04 10:00,M1,5871,1,293062.5,-487.5,-0.17,0,75,0.03,-625,-0.21,-5737.5,0.00,1 +26,Entry short,2025-03-04 09:00,M1,5861.25,1,293062.5,-487.5,-0.17,0,75,0.03,-625,-0.21,-5737.5,0.00,1 +27,Exit long,2025-03-04 11:00,M1,5874,1,293550,150,0.05,0,337.5,0.11,-200,-0.07,-5587.5,0.00,1 +27,Entry long,2025-03-04 10:00,M1,5871,1,293550,150,0.05,0,337.5,0.11,-200,-0.07,-5587.5,0.00,1 +28,Exit short,2025-03-04 12:00,M1,5870,1,293700,200,0.07,0,275,0.09,-200,-0.07,-5387.5,0.00,1 +28,Entry short,2025-03-04 11:00,M1,5874,1,293700,200,0.07,0,275,0.09,-200,-0.07,-5387.5,0.00,1 +29,Exit long,2025-03-04 13:00,M1,5877.75,1,293500,387.5,0.13,0,700,0.24,-537.5,-0.18,-5000,0.00,1 +29,Entry long,2025-03-04 12:00,M1,5870,1,293500,387.5,0.13,0,700,0.24,-537.5,-0.18,-5000,0.00,1 +30,Exit short,2025-03-04 14:00,M1,5878.25,1,293887.5,-25,-0.01,0,437.5,0.15,-187.5,-0.06,-5025,0.00,1 +30,Entry short,2025-03-04 13:00,M1,5877.75,1,293887.5,-25,-0.01,0,437.5,0.15,-187.5,-0.06,-5025,0.00,1 +31,Exit long,2025-03-04 15:00,M1,5863.25,1,293912.5,-750,-0.26,0,50,0.02,-912.5,-0.31,-5775,0.00,1 +31,Entry long,2025-03-04 14:00,M1,5878.25,1,293912.5,-750,-0.26,0,50,0.02,-912.5,-0.31,-5775,0.00,1 +32,Exit short,2025-03-04 16:00,M1,5867.75,1,293162.5,-225,-0.08,0,337.5,0.12,-762.5,-0.26,-6000,0.00,1 +32,Entry short,2025-03-04 15:00,M1,5863.25,1,293162.5,-225,-0.08,0,337.5,0.12,-762.5,-0.26,-6000,0.00,1 +33,Exit long,2025-03-04 17:00,M1,5863.25,1,293387.5,-225,-0.08,0,462.5,0.16,-412.5,-0.14,-6225,0.00,1 +33,Entry long,2025-03-04 16:00,M1,5867.75,1,293387.5,-225,-0.08,0,462.5,0.16,-412.5,-0.14,-6225,0.00,1 +34,Exit short,2025-03-04 18:00,M1,5855.75,1,293162.5,375,0.13,0,725,0.25,-137.5,-0.05,-5850,0.00,1 +34,Entry short,2025-03-04 17:00,M1,5863.25,1,293162.5,375,0.13,0,725,0.25,-137.5,-0.05,-5850,0.00,1 +35,Exit long,2025-03-04 19:00,M1,5856,1,292787.5,12.5,0.00,0,262.5,0.09,-337.5,-0.12,-5837.5,0.00,1 +35,Entry long,2025-03-04 18:00,M1,5855.75,1,292787.5,12.5,0.00,0,262.5,0.09,-337.5,-0.12,-5837.5,0.00,1 +36,Exit short,2025-03-04 20:00,M1,5823.75,1,292800,1612.5,0.55,0,1750,0.60,0,0.00,-4225,0.00,1 +36,Entry short,2025-03-04 19:00,M1,5856,1,292800,1612.5,0.55,0,1750,0.60,0,0.00,-4225,0.00,1 +37,Exit long,2025-03-04 21:00,M1,5820.75,1,291187.5,-150,-0.05,0,237.5,0.08,-587.5,-0.20,-4375,0.00,1 +37,Entry long,2025-03-04 20:00,M1,5823.75,1,291187.5,-150,-0.05,0,237.5,0.08,-587.5,-0.20,-4375,0.00,1 +38,Exit short,2025-03-04 22:00,M1,5779,1,291037.5,2087.5,0.72,0,2137.5,0.73,-750,-0.26,-2287.5,0.00,1 +38,Entry short,2025-03-04 21:00,M1,5820.75,1,291037.5,2087.5,0.72,0,2137.5,0.73,-750,-0.26,-2287.5,0.00,1 +39,Exit long,2025-03-04 23:00,M1,5762.5,1,288950,-825,-0.29,0,237.5,0.08,-1750,-0.61,-3112.5,0.00,1 +39,Entry long,2025-03-04 22:00,M1,5779,1,288950,-825,-0.29,0,237.5,0.08,-1750,-0.61,-3112.5,0.00,1 +40,Exit short,2025-03-05 00:00,M1,5781.5,1,288125,-950,-0.33,0,887.5,0.31,-1250,-0.43,-4062.5,0.00,1 +40,Entry short,2025-03-04 23:00,M1,5762.5,1,288125,-950,-0.33,0,887.5,0.31,-1250,-0.43,-4062.5,0.00,1 +41,Exit long,2025-03-05 01:00,M1,5786.75,1,289075,262.5,0.09,0,1287.5,0.45,-337.5,-0.12,-3800,0.00,1 +41,Entry long,2025-03-05 00:00,M1,5781.5,1,289075,262.5,0.09,0,1287.5,0.45,-337.5,-0.12,-3800,0.00,1 +42,Exit short,2025-03-05 02:00,M1,5824.5,1,289337.5,-1887.5,-0.65,0,225,0.08,-2175,-0.75,-5687.5,0.00,1 +42,Entry short,2025-03-05 01:00,M1,5786.75,1,289337.5,-1887.5,-0.65,0,225,0.08,-2175,-0.75,-5687.5,0.00,1 +43,Exit long,2025-03-05 03:00,M1,5850.5,1,291225,1300,0.45,0,1437.5,0.49,-50,-0.02,-4387.5,0.00,1 +43,Entry long,2025-03-05 02:00,M1,5824.5,1,291225,1300,0.45,0,1437.5,0.49,-50,-0.02,-4387.5,0.00,1 +44,Exit short,2025-03-05 04:00,M1,5790.75,1,292525,2987.5,1.02,0,4112.5,1.41,-1262.5,-0.43,-1400,0.00,1 +44,Entry short,2025-03-05 03:00,M1,5850.5,1,292525,2987.5,1.02,0,4112.5,1.41,-1262.5,-0.43,-1400,0.00,1 +45,Exit long,2025-03-05 05:00,M1,5825,1,289537.5,1712.5,0.59,0,2462.5,0.85,-12.5,0.00,312.5,0.00,1 +45,Entry long,2025-03-05 04:00,M1,5790.75,1,289537.5,1712.5,0.59,0,2462.5,0.85,-12.5,0.00,312.5,0.00,1 +46,Exit short,2025-03-05 07:00,M1,5834,1,291250,-450,-0.15,0,87.5,0.03,-650,-0.22,-137.5,0.00,1 +46,Entry short,2025-03-05 05:00,M1,5825,1,291250,-450,-0.15,0,87.5,0.03,-650,-0.22,-137.5,0.00,1 +47,Exit long,2025-03-05 08:00,M1,5826.5,1,291700,-375,-0.13,0,137.5,0.05,-962.5,-0.33,-512.5,0.00,1 +47,Entry long,2025-03-05 07:00,M1,5834,1,291700,-375,-0.13,0,137.5,0.05,-962.5,-0.33,-512.5,0.00,1 +48,Exit short,2025-03-05 09:00,M1,5820,1,291325,325,0.11,0,925,0.32,-100,-0.03,-187.5,0.00,1 +48,Entry short,2025-03-05 08:00,M1,5826.5,1,291325,325,0.11,0,925,0.32,-100,-0.03,-187.5,0.00,1 +49,Exit long,2025-03-05 10:00,M1,5814,1,291000,-300,-0.10,0,150,0.05,-612.5,-0.21,-487.5,0.00,1 +49,Entry long,2025-03-05 09:00,M1,5820,1,291000,-300,-0.10,0,150,0.05,-612.5,-0.21,-487.5,0.00,1 +50,Exit short,2025-03-05 11:00,M1,5828.75,1,290700,-737.5,-0.25,0,250,0.09,-937.5,-0.32,-1225,0.00,1 +50,Entry short,2025-03-05 10:00,M1,5814,1,290700,-737.5,-0.25,0,250,0.09,-937.5,-0.32,-1225,0.00,1 +51,Exit long,2025-03-05 12:00,M1,5826.75,1,291437.5,-100,-0.03,0,262.5,0.09,-187.5,-0.06,-1325,0.00,1 +51,Entry long,2025-03-05 11:00,M1,5828.75,1,291437.5,-100,-0.03,0,262.5,0.09,-187.5,-0.06,-1325,0.00,1 +52,Exit short,2025-03-05 13:00,M1,5827,1,291337.5,-12.5,0.00,0,462.5,0.16,-112.5,-0.04,-1337.5,0.00,1 +52,Entry short,2025-03-05 12:00,M1,5826.75,1,291337.5,-12.5,0.00,0,462.5,0.16,-112.5,-0.04,-1337.5,0.00,1 +53,Exit long,2025-03-05 14:00,M1,5826,1,291350,-50,-0.02,0,137.5,0.05,-437.5,-0.15,-1387.5,0.00,1 +53,Entry long,2025-03-05 13:00,M1,5827,1,291350,-50,-0.02,0,137.5,0.05,-437.5,-0.15,-1387.5,0.00,1 +54,Exit short,2025-03-05 15:00,M1,5822,1,291300,200,0.07,0,250,0.09,-325,-0.11,-1187.5,0.00,1 +54,Entry short,2025-03-05 14:00,M1,5826,1,291300,200,0.07,0,250,0.09,-325,-0.11,-1187.5,0.00,1 +55,Exit long,2025-03-05 16:00,M1,5824.5,1,291100,125,0.04,0,750,0.26,-237.5,-0.08,-1062.5,0.00,1 +55,Entry long,2025-03-05 15:00,M1,5822,1,291100,125,0.04,0,750,0.26,-237.5,-0.08,-1062.5,0.00,1 +56,Exit short,2025-03-05 17:00,M1,5839.5,1,291225,-750,-0.26,0,225,0.08,-987.5,-0.34,-1812.5,0.00,1 +56,Entry short,2025-03-05 16:00,M1,5824.5,1,291225,-750,-0.26,0,225,0.08,-987.5,-0.34,-1812.5,0.00,1 +57,Exit long,2025-03-05 18:00,M1,5823.25,1,291975,-812.5,-0.28,0,162.5,0.06,-850,-0.29,-2625,0.00,1 +57,Entry long,2025-03-05 17:00,M1,5839.5,1,291975,-812.5,-0.28,0,162.5,0.06,-850,-0.29,-2625,0.00,1 +58,Exit short,2025-03-05 19:00,M1,5809.75,1,291162.5,675,0.23,0,1350,0.46,-112.5,-0.04,-1950,0.00,1 +58,Entry short,2025-03-05 18:00,M1,5823.25,1,291162.5,675,0.23,0,1350,0.46,-112.5,-0.04,-1950,0.00,1 +59,Exit long,2025-03-05 20:00,M1,5802.5,1,290487.5,-362.5,-0.12,0,337.5,0.12,-612.5,-0.21,-2312.5,0.00,1 +59,Entry long,2025-03-05 19:00,M1,5809.75,1,290487.5,-362.5,-0.12,0,337.5,0.12,-612.5,-0.21,-2312.5,0.00,1 +60,Exit short,2025-03-05 21:00,M1,5788.5,1,290125,700,0.24,0,937.5,0.32,-550,-0.19,-1612.5,0.00,1 +60,Entry short,2025-03-05 20:00,M1,5802.5,1,290125,700,0.24,0,937.5,0.32,-550,-0.19,-1612.5,0.00,1 +61,Exit long,2025-03-05 22:00,M1,5778.25,1,289425,-512.5,-0.18,0,1212.5,0.42,-912.5,-0.32,-2125,0.00,1 +61,Entry long,2025-03-05 21:00,M1,5788.5,1,289425,-512.5,-0.18,0,1212.5,0.42,-912.5,-0.32,-2125,0.00,1 +62,Exit short,2025-03-05 23:00,M1,5789.5,1,288912.5,-562.5,-0.19,0,187.5,0.06,-1937.5,-0.67,-2687.5,0.00,1 +62,Entry short,2025-03-05 22:00,M1,5778.25,1,288912.5,-562.5,-0.19,0,187.5,0.06,-1937.5,-0.67,-2687.5,0.00,1 +63,Exit long,2025-03-06 00:00,M1,5779.5,1,289475,-500,-0.17,0,0,0.00,-1937.5,-0.67,-3187.5,0.00,1 +63,Entry long,2025-03-05 23:00,M1,5789.5,1,289475,-500,-0.17,0,0,0.00,-1937.5,-0.67,-3187.5,0.00,1 +64,Exit short,2025-03-06 01:00,M1,5828,1,288975,-2425,-0.84,0,75,0.03,-2500,-0.87,-5612.5,0.00,1 +64,Entry short,2025-03-06 00:00,M1,5779.5,1,288975,-2425,-0.84,0,75,0.03,-2500,-0.87,-5612.5,0.00,1 +65,Exit long,2025-03-06 02:00,M1,5835.25,1,291400,362.5,0.12,0,412.5,0.14,-1512.5,-0.52,-5250,0.00,1 +65,Entry long,2025-03-06 01:00,M1,5828,1,291400,362.5,0.12,0,412.5,0.14,-1512.5,-0.52,-5250,0.00,1 +66,Exit short,2025-03-06 03:00,M1,5851.25,1,291762.5,-800,-0.27,0,325,0.11,-1450,-0.50,-6050,0.00,1 +66,Entry short,2025-03-06 02:00,M1,5835.25,1,291762.5,-800,-0.27,0,325,0.11,-1450,-0.50,-6050,0.00,1 +67,Exit long,2025-03-06 04:00,M1,5850.75,1,292562.5,-25,-0.01,0,912.5,0.31,-862.5,-0.29,-6075,0.00,1 +67,Entry long,2025-03-06 03:00,M1,5851.25,1,292562.5,-25,-0.01,0,912.5,0.31,-862.5,-0.29,-6075,0.00,1 +68,Exit short,2025-03-06 05:00,M1,5845.5,1,292537.5,262.5,0.09,0,475,0.16,-62.5,-0.02,-5812.5,0.00,1 +68,Entry short,2025-03-06 04:00,M1,5850.75,1,292537.5,262.5,0.09,0,475,0.16,-62.5,-0.02,-5812.5,0.00,1 +69,Exit long,2025-03-06 07:00,M1,5845.25,1,292275,-12.5,0.00,0,400,0.14,-200,-0.07,-5825,0.00,1 +69,Entry long,2025-03-06 05:00,M1,5845.5,1,292275,-12.5,0.00,0,400,0.14,-200,-0.07,-5825,0.00,1 +70,Exit short,2025-03-06 08:00,M1,5839.5,1,292262.5,287.5,0.10,0,362.5,0.12,-237.5,-0.08,-5537.5,0.00,1 +70,Entry short,2025-03-06 07:00,M1,5845.25,1,292262.5,287.5,0.10,0,362.5,0.12,-237.5,-0.08,-5537.5,0.00,1 +71,Exit long,2025-03-06 09:00,M1,5843.25,1,291975,187.5,0.06,0,275,0.09,-50,-0.02,-5350,0.00,1 +71,Entry long,2025-03-06 08:00,M1,5839.5,1,291975,187.5,0.06,0,275,0.09,-50,-0.02,-5350,0.00,1 +72,Exit short,2025-03-06 10:00,M1,5840.75,1,292162.5,125,0.04,0,187.5,0.06,-175,-0.06,-5225,0.00,1 +72,Entry short,2025-03-06 09:00,M1,5843.25,1,292162.5,125,0.04,0,187.5,0.06,-175,-0.06,-5225,0.00,1 +73,Exit long,2025-03-06 11:00,M1,5846.5,1,292037.5,287.5,0.10,0,325,0.11,-100,-0.03,-4937.5,0.00,1 +73,Entry long,2025-03-06 10:00,M1,5840.75,1,292037.5,287.5,0.10,0,325,0.11,-100,-0.03,-4937.5,0.00,1 +74,Exit short,2025-03-06 12:00,M1,5849.25,1,292325,-137.5,-0.05,0,12.5,0.00,-175,-0.06,-5075,0.00,1 +74,Entry short,2025-03-06 11:00,M1,5846.5,1,292325,-137.5,-0.05,0,12.5,0.00,-175,-0.06,-5075,0.00,1 +75,Exit long,2025-03-06 13:00,M1,5842.75,1,292462.5,-325,-0.11,0,37.5,0.01,-325,-0.11,-5400,0.00,1 +75,Entry long,2025-03-06 12:00,M1,5849.25,1,292462.5,-325,-0.11,0,37.5,0.01,-325,-0.11,-5400,0.00,1 +76,Exit short,2025-03-06 14:00,M1,5848.25,1,292137.5,-275,-0.09,0,112.5,0.04,-337.5,-0.12,-5675,0.00,1 +76,Entry short,2025-03-06 13:00,M1,5842.75,1,292137.5,-275,-0.09,0,112.5,0.04,-337.5,-0.12,-5675,0.00,1 +77,Exit long,2025-03-06 15:00,M1,5824.5,1,292412.5,-1187.5,-0.41,0,87.5,0.03,-1225,-0.42,-6862.5,0.00,1 +77,Entry long,2025-03-06 14:00,M1,5848.25,1,292412.5,-1187.5,-0.41,0,87.5,0.03,-1225,-0.42,-6862.5,0.00,1 +78,Exit short,2025-03-06 16:00,M1,5803.75,1,291225,1037.5,0.36,0,1162.5,0.40,-312.5,-0.11,-5825,0.00,1 +78,Entry short,2025-03-06 15:00,M1,5824.5,1,291225,1037.5,0.36,0,1162.5,0.40,-312.5,-0.11,-5825,0.00,1 +79,Exit long,2025-03-06 17:00,M1,5792.5,1,290187.5,-562.5,-0.19,0,487.5,0.17,-875,-0.30,-6387.5,0.00,1 +79,Entry long,2025-03-06 16:00,M1,5803.75,1,290187.5,-562.5,-0.19,0,487.5,0.17,-875,-0.30,-6387.5,0.00,1 +80,Exit short,2025-03-06 18:00,M1,5781.5,1,289625,550,0.19,0,650,0.22,-262.5,-0.09,-5837.5,0.00,1 +80,Entry short,2025-03-06 17:00,M1,5792.5,1,289625,550,0.19,0,650,0.22,-262.5,-0.09,-5837.5,0.00,1 +81,Exit long,2025-03-06 19:00,M1,5796.75,1,289075,762.5,0.26,0,787.5,0.27,-300,-0.10,-5075,0.00,1 +81,Entry long,2025-03-06 18:00,M1,5781.5,1,289075,762.5,0.26,0,787.5,0.27,-300,-0.10,-5075,0.00,1 +82,Exit short,2025-03-06 20:00,M1,5784.75,1,289837.5,600,0.21,0,825,0.28,-50,-0.02,-4475,0.00,1 +82,Entry short,2025-03-06 19:00,M1,5796.75,1,289837.5,600,0.21,0,825,0.28,-50,-0.02,-4475,0.00,1 +83,Exit long,2025-03-06 21:00,M1,5779.5,1,289237.5,-262.5,-0.09,0,300,0.10,-775,-0.27,-4737.5,0.00,1 +83,Entry long,2025-03-06 20:00,M1,5784.75,1,289237.5,-262.5,-0.09,0,300,0.10,-775,-0.27,-4737.5,0.00,1 +84,Exit short,2025-03-06 22:00,M1,5763.25,1,288975,812.5,0.28,0,1312.5,0.45,-462.5,-0.16,-3925,0.00,1 +84,Entry short,2025-03-06 21:00,M1,5779.5,1,288975,812.5,0.28,0,1312.5,0.45,-462.5,-0.16,-3925,0.00,1 +85,Exit long,2025-03-06 23:00,M1,5806.5,1,288162.5,2162.5,0.75,0,2837.5,0.98,-175,-0.06,-1762.5,0.00,1 +85,Entry long,2025-03-06 22:00,M1,5763.25,1,288162.5,2162.5,0.75,0,2837.5,0.98,-175,-0.06,-1762.5,0.00,1 +86,Exit short,2025-03-07 00:00,M1,5764.25,1,290325,2112.5,0.73,0,2175,0.75,-737.5,-0.25,350,0.00,1 +86,Entry short,2025-03-06 23:00,M1,5806.5,1,290325,2112.5,0.73,0,2175,0.75,-737.5,-0.25,350,0.00,1 +87,Exit long,2025-03-07 01:00,M1,5749.5,1,288212.5,-737.5,-0.26,0,225,0.08,-1762.5,-0.61,-387.5,0.00,1 +87,Entry long,2025-03-07 00:00,M1,5764.25,1,288212.5,-737.5,-0.26,0,225,0.08,-1762.5,-0.61,-387.5,0.00,1 +88,Exit short,2025-03-07 02:00,M1,5740,1,287475,475,0.17,0,1337.5,0.47,-537.5,-0.19,87.5,0.00,1 +88,Entry short,2025-03-07 01:00,M1,5749.5,1,287475,475,0.17,0,1337.5,0.47,-537.5,-0.19,87.5,0.00,1 +89,Exit long,2025-03-07 03:00,M1,5729,1,287000,-550,-0.19,0,1425,0.50,-1000,-0.35,-462.5,0.00,1 +89,Entry long,2025-03-07 02:00,M1,5740,1,287000,-550,-0.19,0,1425,0.50,-1000,-0.35,-462.5,0.00,1 +90,Exit short,2025-03-07 04:00,M1,5747.75,1,286450,-937.5,-0.33,0,275,0.10,-1637.5,-0.57,-1400,0.00,1 +90,Entry short,2025-03-07 03:00,M1,5729,1,286450,-937.5,-0.33,0,275,0.10,-1637.5,-0.57,-1400,0.00,1 +91,Exit long,2025-03-07 05:00,M1,5761.25,1,287387.5,675,0.23,0,1200,0.42,-175,-0.06,-725,0.00,1 +91,Entry long,2025-03-07 04:00,M1,5747.75,1,287387.5,675,0.23,0,1200,0.42,-175,-0.06,-725,0.00,1 +92,Exit short,2025-03-07 07:00,M1,5764.75,1,288062.5,-175,-0.06,0,0,0.00,-425,-0.15,-900,0.00,1 +92,Entry short,2025-03-07 05:00,M1,5761.25,1,288062.5,-175,-0.06,0,0,0.00,-425,-0.15,-900,0.00,1 +93,Exit long,2025-03-07 08:00,M1,5763.75,1,288237.5,-50,-0.02,0,250,0.09,-425,-0.15,-950,0.00,1 +93,Entry long,2025-03-07 07:00,M1,5764.75,1,288237.5,-50,-0.02,0,250,0.09,-425,-0.15,-950,0.00,1 +94,Exit short,2025-03-07 09:00,M1,5768.75,1,288187.5,-250,-0.09,0,125,0.04,-537.5,-0.19,-1200,0.00,1 +94,Entry short,2025-03-07 08:00,M1,5763.75,1,288187.5,-250,-0.09,0,125,0.04,-537.5,-0.19,-1200,0.00,1 +95,Exit long,2025-03-07 10:00,M1,5768.25,1,288437.5,-25,-0.01,0,287.5,0.10,-275,-0.10,-1225,0.00,1 +95,Entry long,2025-03-07 09:00,M1,5768.75,1,288437.5,-25,-0.01,0,287.5,0.10,-275,-0.10,-1225,0.00,1 +96,Exit short,2025-03-07 11:00,M1,5761.25,1,288412.5,350,0.12,0,375,0.13,-37.5,-0.01,-875,0.00,1 +96,Entry short,2025-03-07 10:00,M1,5768.25,1,288412.5,350,0.12,0,375,0.13,-37.5,-0.01,-875,0.00,1 +97,Exit long,2025-03-07 12:00,M1,5764.25,1,288062.5,150,0.05,0,212.5,0.07,-200,-0.07,-725,0.00,1 +97,Entry long,2025-03-07 11:00,M1,5761.25,1,288062.5,150,0.05,0,212.5,0.07,-200,-0.07,-725,0.00,1 +98,Exit short,2025-03-07 13:00,M1,5757,1,288212.5,362.5,0.13,0,487.5,0.17,-25,-0.01,-362.5,0.00,1 +98,Entry short,2025-03-07 12:00,M1,5764.25,1,288212.5,362.5,0.13,0,487.5,0.17,-25,-0.01,-362.5,0.00,1 +99,Exit long,2025-03-07 14:00,M1,5752,1,287850,-250,-0.09,0,37.5,0.01,-350,-0.12,-612.5,0.00,1 +99,Entry long,2025-03-07 13:00,M1,5757,1,287850,-250,-0.09,0,37.5,0.01,-350,-0.12,-612.5,0.00,1 +100,Exit short,2025-03-07 15:00,M1,5762.25,1,287600,-512.5,-0.18,0,75,0.03,-612.5,-0.21,-1125,0.00,1 +100,Entry short,2025-03-07 14:00,M1,5752,1,287600,-512.5,-0.18,0,75,0.03,-612.5,-0.21,-1125,0.00,1 +101,Exit long,2025-03-07 16:00,M1,5760.25,1,288112.5,-100,-0.03,0,212.5,0.07,-425,-0.15,-1225,0.00,1 +101,Entry long,2025-03-07 15:00,M1,5762.25,1,288112.5,-100,-0.03,0,212.5,0.07,-425,-0.15,-1225,0.00,1 +102,Exit short,2025-03-07 17:00,M1,5767.75,1,288012.5,-375,-0.13,0,100,0.03,-650,-0.23,-1600,0.00,1 +102,Entry short,2025-03-07 16:00,M1,5760.25,1,288012.5,-375,-0.13,0,100,0.03,-650,-0.23,-1600,0.00,1 +103,Exit long,2025-03-07 18:00,M1,5763.5,1,288387.5,-212.5,-0.07,0,37.5,0.01,-562.5,-0.20,-1812.5,0.00,1 +103,Entry long,2025-03-07 17:00,M1,5767.75,1,288387.5,-212.5,-0.07,0,37.5,0.01,-562.5,-0.20,-1812.5,0.00,1 +104,Exit short,2025-03-07 19:00,M1,5765.25,1,288175,-87.5,-0.03,0,275,0.10,-275,-0.10,-1900,0.00,1 +104,Entry short,2025-03-07 18:00,M1,5763.5,1,288175,-87.5,-0.03,0,275,0.10,-275,-0.10,-1900,0.00,1 +105,Exit long,2025-03-07 20:00,M1,5751.5,1,288262.5,-687.5,-0.24,0,25,0.01,-987.5,-0.34,-2587.5,0.00,1 +105,Entry long,2025-03-07 19:00,M1,5765.25,1,288262.5,-687.5,-0.24,0,25,0.01,-987.5,-0.34,-2587.5,0.00,1 +106,Exit short,2025-03-07 21:00,M1,5734.5,1,287575,850,0.30,0,1112.5,0.39,-1750,-0.61,-1737.5,0.00,1 +106,Entry short,2025-03-07 20:00,M1,5751.5,1,287575,850,0.30,0,1112.5,0.39,-1750,-0.61,-1737.5,0.00,1 +107,Exit long,2025-03-07 22:00,M1,5756.25,1,286725,1087.5,0.38,0,2175,0.76,-675,-0.24,-650,0.00,1 +107,Entry long,2025-03-07 21:00,M1,5734.5,1,286725,1087.5,0.38,0,2175,0.76,-675,-0.24,-650,0.00,1 +108,Exit short,2025-03-07 23:00,M1,5726,1,287812.5,1512.5,0.53,0,2750,0.96,-587.5,-0.20,862.5,0.00,1 +108,Entry short,2025-03-07 22:00,M1,5756.25,1,287812.5,1512.5,0.53,0,2750,0.96,-587.5,-0.20,862.5,0.00,1 +109,Exit long,2025-03-08 00:00,M1,5687.75,1,286300,-1912.5,-0.67,0,187.5,0.07,-2650,-0.93,-1050,0.00,1 +109,Entry long,2025-03-07 23:00,M1,5726,1,286300,-1912.5,-0.67,0,187.5,0.07,-2650,-0.93,-1050,0.00,1 +110,Exit short,2025-03-08 01:00,M1,5718.5,1,284387.5,-1537.5,-0.54,0,662.5,0.23,-3000,-1.05,-2587.5,0.00,1 +110,Entry short,2025-03-08 00:00,M1,5687.75,1,284387.5,-1537.5,-0.54,0,662.5,0.23,-3000,-1.05,-2587.5,0.00,1 +111,Exit long,2025-03-08 02:00,M1,5769.75,1,285925,2562.5,0.90,0,2575,0.90,-25,-0.01,-25,0.00,1 +111,Entry long,2025-03-08 01:00,M1,5718.5,1,285925,2562.5,0.90,0,2575,0.90,-25,-0.01,-25,0.00,1 +112,Exit short,2025-03-08 03:00,M1,5769,1,288487.5,37.5,0.01,0,575,0.20,-1025,-0.36,12.5,0.00,1 +112,Entry short,2025-03-08 02:00,M1,5769.75,1,288487.5,37.5,0.01,0,575,0.20,-1025,-0.36,12.5,0.00,1 +113,Exit long,2025-03-08 04:00,M1,5775,1,288450,300,0.10,0,1100,0.38,-1187.5,-0.41,312.5,0.00,1 +113,Entry long,2025-03-08 03:00,M1,5769,1,288450,300,0.10,0,1100,0.38,-1187.5,-0.41,312.5,0.00,1 +114,Exit short,2025-03-08 05:00,M1,5771.75,1,288750,162.5,0.06,0,737.5,0.26,-112.5,-0.04,475,0.00,1 +114,Entry short,2025-03-08 04:00,M1,5775,1,288750,162.5,0.06,0,737.5,0.26,-112.5,-0.04,475,0.00,1 +115,Exit long,2025-03-10 06:00,M1,5728,1,288587.5,-2187.5,-0.76,0,0,0.00,-2450,-0.85,-1712.5,0.00,1 +115,Entry long,2025-03-08 05:00,M1,5771.75,1,288587.5,-2187.5,-0.76,0,0,0.00,-2450,-0.85,-1712.5,0.00,1 +116,Exit short,2025-03-10 07:00,M1,5733.25,1,286400,-262.5,-0.09,0,875,0.31,-500,-0.17,-1975,0.00,1 +116,Entry short,2025-03-10 06:00,M1,5728,1,286400,-262.5,-0.09,0,875,0.31,-500,-0.17,-1975,0.00,1 +117,Exit long,2025-03-10 08:00,M1,5738.25,1,286662.5,250,0.09,0,325,0.11,-350,-0.12,-1725,0.00,1 +117,Entry long,2025-03-10 07:00,M1,5733.25,1,286662.5,250,0.09,0,325,0.11,-350,-0.12,-1725,0.00,1 +118,Exit short,2025-03-10 09:00,M1,5752,1,286912.5,-687.5,-0.24,0,37.5,0.01,-712.5,-0.25,-2412.5,0.00,1 +118,Entry short,2025-03-10 08:00,M1,5738.25,1,286912.5,-687.5,-0.24,0,37.5,0.01,-712.5,-0.25,-2412.5,0.00,1 +119,Exit long,2025-03-10 10:00,M1,5752.25,1,287600,12.5,0.00,0,162.5,0.06,-200,-0.07,-2400,0.00,1 +119,Entry long,2025-03-10 09:00,M1,5752,1,287600,12.5,0.00,0,162.5,0.06,-200,-0.07,-2400,0.00,1 +120,Exit short,2025-03-10 11:00,M1,5750.5,1,287612.5,87.5,0.03,0,387.5,0.13,-125,-0.04,-2312.5,0.00,1 +120,Entry short,2025-03-10 10:00,M1,5752.25,1,287612.5,87.5,0.03,0,387.5,0.13,-125,-0.04,-2312.5,0.00,1 +121,Exit long,2025-03-10 12:00,M1,5745.5,1,287525,-250,-0.09,0,362.5,0.13,-275,-0.10,-2562.5,0.00,1 +121,Entry long,2025-03-10 11:00,M1,5750.5,1,287525,-250,-0.09,0,362.5,0.13,-275,-0.10,-2562.5,0.00,1 +122,Exit short,2025-03-10 13:00,M1,5743.5,1,287275,100,0.03,0,212.5,0.07,-362.5,-0.13,-2462.5,0.00,1 +122,Entry short,2025-03-10 12:00,M1,5745.5,1,287275,100,0.03,0,212.5,0.07,-362.5,-0.13,-2462.5,0.00,1 +123,Exit long,2025-03-10 14:00,M1,5748.25,1,287175,237.5,0.08,0,425,0.15,0,0.00,-2225,0.00,1 +123,Entry long,2025-03-10 13:00,M1,5743.5,1,287175,237.5,0.08,0,425,0.15,0,0.00,-2225,0.00,1 +124,Exit short,2025-03-10 15:00,M1,5743.25,1,287412.5,250,0.09,0,300,0.10,-287.5,-0.10,-1975,0.00,1 +124,Entry short,2025-03-10 14:00,M1,5748.25,1,287412.5,250,0.09,0,300,0.10,-287.5,-0.10,-1975,0.00,1 +125,Exit long,2025-03-10 16:00,M1,5705.75,1,287162.5,-1875,-0.65,0,250,0.09,-2012.5,-0.70,-3850,0.00,1 +125,Entry long,2025-03-10 15:00,M1,5743.25,1,287162.5,-1875,-0.65,0,250,0.09,-2012.5,-0.70,-3850,0.00,1 +126,Exit short,2025-03-10 17:00,M1,5718.5,1,285287.5,-637.5,-0.22,0,137.5,0.05,-1287.5,-0.45,-4487.5,0.00,1 +126,Entry short,2025-03-10 16:00,M1,5705.75,1,285287.5,-637.5,-0.22,0,137.5,0.05,-1287.5,-0.45,-4487.5,0.00,1 +127,Exit long,2025-03-10 18:00,M1,5709,1,285925,-475,-0.17,0,325,0.11,-625,-0.22,-4962.5,0.00,1 +127,Entry long,2025-03-10 17:00,M1,5718.5,1,285925,-475,-0.17,0,325,0.11,-625,-0.22,-4962.5,0.00,1 +128,Exit short,2025-03-10 19:00,M1,5706.5,1,285450,125,0.04,0,362.5,0.13,-712.5,-0.25,-4837.5,0.00,1 +128,Entry short,2025-03-10 18:00,M1,5709,1,285450,125,0.04,0,362.5,0.13,-712.5,-0.25,-4837.5,0.00,1 +129,Exit long,2025-03-10 20:00,M1,5701,1,285325,-275,-0.10,0,50,0.02,-962.5,-0.34,-5112.5,0.00,1 +129,Entry long,2025-03-10 19:00,M1,5706.5,1,285325,-275,-0.10,0,50,0.02,-962.5,-0.34,-5112.5,0.00,1 +130,Exit short,2025-03-10 21:00,M1,5689.25,1,285050,587.5,0.21,0,1087.5,0.38,-512.5,-0.18,-4525,0.00,1 +130,Entry short,2025-03-10 20:00,M1,5701,1,285050,587.5,0.21,0,1087.5,0.38,-512.5,-0.18,-4525,0.00,1 +131,Exit long,2025-03-10 22:00,M1,5655,1,284462.5,-1712.5,-0.60,0,12.5,0.00,-2137.5,-0.75,-6237.5,0.00,1 +131,Entry long,2025-03-10 21:00,M1,5689.25,1,284462.5,-1712.5,-0.60,0,12.5,0.00,-2137.5,-0.75,-6237.5,0.00,1 +132,Exit short,2025-03-10 23:00,M1,5653.75,1,282750,62.5,0.02,0,162.5,0.06,-1350,-0.48,-6175,0.00,1 +132,Entry short,2025-03-10 22:00,M1,5655,1,282750,62.5,0.02,0,162.5,0.06,-1350,-0.48,-6175,0.00,1 +133,Exit long,2025-03-11 00:00,M1,5645.75,1,282687.5,-400,-0.14,0,662.5,0.23,-687.5,-0.24,-6575,0.00,1 +133,Entry long,2025-03-10 23:00,M1,5653.75,1,282687.5,-400,-0.14,0,662.5,0.23,-687.5,-0.24,-6575,0.00,1 +134,Exit short,2025-03-11 01:00,M1,5619.75,1,282287.5,1300,0.46,0,1450,0.51,-75,-0.03,-5275,0.00,1 +134,Entry short,2025-03-11 00:00,M1,5645.75,1,282287.5,1300,0.46,0,1450,0.51,-75,-0.03,-5275,0.00,1 +135,Exit long,2025-03-11 02:00,M1,5588.75,1,280987.5,-1550,-0.55,0,575,0.20,-1675,-0.60,-6825,0.00,1 +135,Entry long,2025-03-11 01:00,M1,5619.75,1,280987.5,-1550,-0.55,0,575,0.20,-1675,-0.60,-6825,0.00,1 +136,Exit short,2025-03-11 03:00,M1,5623.5,1,279437.5,-1737.5,-0.62,0,862.5,0.31,-2612.5,-0.93,-8562.5,0.00,1 +136,Entry short,2025-03-11 02:00,M1,5588.75,1,279437.5,-1737.5,-0.62,0,862.5,0.31,-2612.5,-0.93,-8562.5,0.00,1 +137,Exit long,2025-03-11 04:00,M1,5628.75,1,281175,262.5,0.09,0,387.5,0.14,-500,-0.18,-8300,0.00,1 +137,Entry long,2025-03-11 03:00,M1,5623.5,1,281175,262.5,0.09,0,387.5,0.14,-500,-0.18,-8300,0.00,1 +138,Exit short,2025-03-11 06:00,M1,5611.25,1,281437.5,875,0.31,0,1162.5,0.41,0,0.00,-7425,0.00,1 +138,Entry short,2025-03-11 04:00,M1,5628.75,1,281437.5,875,0.31,0,1162.5,0.41,0,0.00,-7425,0.00,1 +139,Exit long,2025-03-11 07:00,M1,5602,1,280562.5,-462.5,-0.16,0,200,0.07,-1362.5,-0.49,-7887.5,0.00,1 +139,Entry long,2025-03-11 06:00,M1,5611.25,1,280562.5,-462.5,-0.16,0,200,0.07,-1362.5,-0.49,-7887.5,0.00,1 +140,Exit short,2025-03-11 08:00,M1,5573.5,1,280100,1425,0.51,0,2162.5,0.77,-262.5,-0.09,-6462.5,0.00,1 +140,Entry short,2025-03-11 07:00,M1,5602,1,280100,1425,0.51,0,2162.5,0.77,-262.5,-0.09,-6462.5,0.00,1 +141,Exit long,2025-03-11 09:00,M1,5586.5,1,278675,650,0.23,0,787.5,0.28,-775,-0.28,-5812.5,0.00,1 +141,Entry long,2025-03-11 08:00,M1,5573.5,1,278675,650,0.23,0,787.5,0.28,-775,-0.28,-5812.5,0.00,1 +142,Exit short,2025-03-11 10:00,M1,5613.5,1,279325,-1350,-0.48,0,100,0.04,-1712.5,-0.61,-7162.5,0.00,1 +142,Entry short,2025-03-11 09:00,M1,5586.5,1,279325,-1350,-0.48,0,100,0.04,-1712.5,-0.61,-7162.5,0.00,1 +143,Exit long,2025-03-11 11:00,M1,5624,1,280675,525,0.19,0,1175,0.42,0,0.00,-6637.5,0.00,1 +143,Entry long,2025-03-11 10:00,M1,5613.5,1,280675,525,0.19,0,1175,0.42,0,0.00,-6637.5,0.00,1 +144,Exit short,2025-03-11 12:00,M1,5630.5,1,281200,-325,-0.12,0,200,0.07,-675,-0.24,-6962.5,0.00,1 +144,Entry short,2025-03-11 11:00,M1,5624,1,281200,-325,-0.12,0,200,0.07,-675,-0.24,-6962.5,0.00,1 +145,Exit long,2025-03-11 13:00,M1,5633.75,1,281525,162.5,0.06,0,312.5,0.11,-237.5,-0.08,-6800,0.00,1 +145,Entry long,2025-03-11 12:00,M1,5630.5,1,281525,162.5,0.06,0,312.5,0.11,-237.5,-0.08,-6800,0.00,1 +146,Exit short,2025-03-11 14:00,M1,5630,1,281687.5,187.5,0.07,0,300,0.11,-262.5,-0.09,-6612.5,0.00,1 +146,Entry short,2025-03-11 13:00,M1,5633.75,1,281687.5,187.5,0.07,0,300,0.11,-262.5,-0.09,-6612.5,0.00,1 +147,Exit long,2025-03-11 15:00,M1,5633.5,1,281500,175,0.06,0,325,0.12,-725,-0.26,-6437.5,0.00,1 +147,Entry long,2025-03-11 14:00,M1,5630,1,281500,175,0.06,0,325,0.12,-725,-0.26,-6437.5,0.00,1 +148,Exit short,2025-03-11 16:00,M1,5644,1,281675,-525,-0.19,0,212.5,0.08,-525,-0.19,-6962.5,0.00,1 +148,Entry short,2025-03-11 15:00,M1,5633.5,1,281675,-525,-0.19,0,212.5,0.08,-525,-0.19,-6962.5,0.00,1 +149,Exit long,2025-03-11 17:00,M1,5640.75,1,282200,-162.5,-0.06,0,150,0.05,-312.5,-0.11,-7125,0.00,1 +149,Entry long,2025-03-11 16:00,M1,5644,1,282200,-162.5,-0.06,0,150,0.05,-312.5,-0.11,-7125,0.00,1 +150,Exit short,2025-03-11 18:00,M1,5639.5,1,282037.5,62.5,0.02,0,437.5,0.16,-200,-0.07,-7062.5,0.00,1 +150,Entry short,2025-03-11 17:00,M1,5640.75,1,282037.5,62.5,0.02,0,437.5,0.16,-200,-0.07,-7062.5,0.00,1 +151,Exit long,2025-03-11 19:00,M1,5651,1,281975,575,0.20,0,575,0.20,-650,-0.23,-6487.5,0.00,1 +151,Entry long,2025-03-11 18:00,M1,5639.5,1,281975,575,0.20,0,575,0.20,-650,-0.23,-6487.5,0.00,1 +152,Exit short,2025-03-11 20:00,M1,5619.25,1,282550,1587.5,0.56,0,1737.5,0.61,-37.5,-0.01,-4900,0.00,1 +152,Entry short,2025-03-11 19:00,M1,5651,1,282550,1587.5,0.56,0,1737.5,0.61,-37.5,-0.01,-4900,0.00,1 +153,Exit long,2025-03-11 21:00,M1,5613.25,1,280962.5,-300,-0.11,0,662.5,0.24,-1087.5,-0.39,-5200,0.00,1 +153,Entry long,2025-03-11 20:00,M1,5619.25,1,280962.5,-300,-0.11,0,662.5,0.24,-1087.5,-0.39,-5200,0.00,1 +154,Exit short,2025-03-11 22:00,M1,5562.75,1,280662.5,2525,0.90,0,2937.5,1.05,-1125,-0.40,-2675,0.00,1 +154,Entry short,2025-03-11 21:00,M1,5613.25,1,280662.5,2525,0.90,0,2937.5,1.05,-1125,-0.40,-2675,0.00,1 +155,Exit long,2025-03-11 23:00,M1,5586.5,1,278137.5,1187.5,0.43,0,2600,0.93,-325,-0.12,-1487.5,0.00,1 +155,Entry long,2025-03-11 22:00,M1,5562.75,1,278137.5,1187.5,0.43,0,2600,0.93,-325,-0.12,-1487.5,0.00,1 +156,Exit short,2025-03-12 00:00,M1,5557.25,1,279325,1462.5,0.52,0,1825,0.65,-237.5,-0.09,-25,0.00,1 +156,Entry short,2025-03-11 23:00,M1,5586.5,1,279325,1462.5,0.52,0,1825,0.65,-237.5,-0.09,-25,0.00,1 +157,Exit long,2025-03-12 01:00,M1,5557.25,1,277862.5,0,0.00,0,562.5,0.20,-1162.5,-0.42,-25,0.00,1 +157,Entry long,2025-03-12 00:00,M1,5557.25,1,277862.5,0,0.00,0,562.5,0.20,-1162.5,-0.42,-25,0.00,1 +158,Exit short,2025-03-12 02:00,M1,5630.25,1,277862.5,-3650,-1.31,0,75,0.03,-4325,-1.56,-3675,0.00,1 +158,Entry short,2025-03-12 01:00,M1,5557.25,1,277862.5,-3650,-1.31,0,75,0.03,-4325,-1.56,-3675,0.00,1 +159,Exit long,2025-03-12 03:00,M1,5575.75,1,281512.5,-2725,-0.97,0,537.5,0.19,-2762.5,-0.98,-6400,0.00,1 +159,Entry long,2025-03-12 02:00,M1,5630.25,1,281512.5,-2725,-0.97,0,537.5,0.19,-2762.5,-0.98,-6400,0.00,1 +160,Exit short,2025-03-12 04:00,M1,5576.75,1,278787.5,-50,-0.02,0,150,0.05,-687.5,-0.25,-6450,0.00,1 +160,Entry short,2025-03-12 03:00,M1,5575.75,1,278787.5,-50,-0.02,0,150,0.05,-687.5,-0.25,-6450,0.00,1 +161,Exit long,2025-03-12 06:00,M1,5591.75,1,278837.5,750,0.27,0,887.5,0.32,-125,-0.04,-5700,0.00,1 +161,Entry long,2025-03-12 04:00,M1,5576.75,1,278837.5,750,0.27,0,887.5,0.32,-125,-0.04,-5700,0.00,1 +162,Exit short,2025-03-12 07:00,M1,5593.5,1,279587.5,-87.5,-0.03,0,462.5,0.17,-87.5,-0.03,-5787.5,0.00,1 +162,Entry short,2025-03-12 06:00,M1,5591.75,1,279587.5,-87.5,-0.03,0,462.5,0.17,-87.5,-0.03,-5787.5,0.00,1 +163,Exit long,2025-03-12 08:00,M1,5592.25,1,279675,-62.5,-0.02,0,250,0.09,-387.5,-0.14,-5850,0.00,1 +163,Entry long,2025-03-12 07:00,M1,5593.5,1,279675,-62.5,-0.02,0,250,0.09,-387.5,-0.14,-5850,0.00,1 +164,Exit short,2025-03-12 09:00,M1,5593.75,1,279612.5,-75,-0.03,0,337.5,0.12,-175,-0.06,-5925,0.00,1 +164,Entry short,2025-03-12 08:00,M1,5592.25,1,279612.5,-75,-0.03,0,337.5,0.12,-175,-0.06,-5925,0.00,1 +165,Exit long,2025-03-12 10:00,M1,5590.75,1,279687.5,-150,-0.05,0,250,0.09,-212.5,-0.08,-6075,0.00,1 +165,Entry long,2025-03-12 09:00,M1,5593.75,1,279687.5,-150,-0.05,0,250,0.09,-212.5,-0.08,-6075,0.00,1 +166,Exit short,2025-03-12 11:00,M1,5588.75,1,279537.5,100,0.04,0,287.5,0.10,-125,-0.04,-5975,0.00,1 +166,Entry short,2025-03-12 10:00,M1,5590.75,1,279537.5,100,0.04,0,287.5,0.10,-125,-0.04,-5975,0.00,1 +167,Exit long,2025-03-12 12:00,M1,5590.75,1,279437.5,100,0.04,0,362.5,0.13,-112.5,-0.04,-5875,0.00,1 +167,Entry long,2025-03-12 11:00,M1,5588.75,1,279437.5,100,0.04,0,362.5,0.13,-112.5,-0.04,-5875,0.00,1 +168,Exit short,2025-03-12 13:00,M1,5597,1,279537.5,-312.5,-0.11,0,62.5,0.02,-350,-0.13,-6187.5,0.00,1 +168,Entry short,2025-03-12 12:00,M1,5590.75,1,279537.5,-312.5,-0.11,0,62.5,0.02,-350,-0.13,-6187.5,0.00,1 +169,Exit long,2025-03-12 14:00,M1,5582,1,279850,-750,-0.27,0,0,0.00,-900,-0.32,-6937.5,0.00,1 +169,Entry long,2025-03-12 13:00,M1,5597,1,279850,-750,-0.27,0,0,0.00,-900,-0.32,-6937.5,0.00,1 +170,Exit short,2025-03-12 15:00,M1,5596.25,1,279100,-712.5,-0.26,0,225,0.08,-775,-0.28,-7650,0.00,1 +170,Entry short,2025-03-12 14:00,M1,5582,1,279100,-712.5,-0.26,0,225,0.08,-775,-0.28,-7650,0.00,1 +171,Exit long,2025-03-12 16:00,M1,5611.25,1,279812.5,750,0.27,0,837.5,0.30,-512.5,-0.18,-6900,0.00,1 +171,Entry long,2025-03-12 15:00,M1,5596.25,1,279812.5,750,0.27,0,837.5,0.30,-512.5,-0.18,-6900,0.00,1 +172,Exit short,2025-03-12 17:00,M1,5615.5,1,280562.5,-212.5,-0.08,0,212.5,0.08,-250,-0.09,-7112.5,0.00,1 +172,Entry short,2025-03-12 16:00,M1,5611.25,1,280562.5,-212.5,-0.08,0,212.5,0.08,-250,-0.09,-7112.5,0.00,1 +173,Exit long,2025-03-12 18:00,M1,5613.75,1,280775,-87.5,-0.03,0,87.5,0.03,-500,-0.18,-7200,0.00,1 +173,Entry long,2025-03-12 17:00,M1,5615.5,1,280775,-87.5,-0.03,0,87.5,0.03,-500,-0.18,-7200,0.00,1 +174,Exit short,2025-03-12 19:00,M1,5621.75,1,280687.5,-400,-0.14,0,37.5,0.01,-675,-0.24,-7600,0.00,1 +174,Entry short,2025-03-12 18:00,M1,5613.75,1,280687.5,-400,-0.14,0,37.5,0.01,-675,-0.24,-7600,0.00,1 +175,Exit long,2025-03-12 20:00,M1,5643.75,1,281087.5,1100,0.39,0,2662.5,0.95,-262.5,-0.09,-6500,0.00,1 +175,Entry long,2025-03-12 19:00,M1,5621.75,1,281087.5,1100,0.39,0,2662.5,0.95,-262.5,-0.09,-6500,0.00,1 +176,Exit short,2025-03-12 21:00,M1,5608.5,1,282187.5,1762.5,0.62,0,2337.5,0.83,-125,-0.04,-4737.5,0.00,1 +176,Entry short,2025-03-12 20:00,M1,5643.75,1,282187.5,1762.5,0.62,0,2337.5,0.83,-125,-0.04,-4737.5,0.00,1 +177,Exit long,2025-03-12 22:00,M1,5569.5,1,280425,-1950,-0.70,0,537.5,0.19,-2012.5,-0.72,-6687.5,0.00,1 +177,Entry long,2025-03-12 21:00,M1,5608.5,1,280425,-1950,-0.70,0,537.5,0.19,-2012.5,-0.72,-6687.5,0.00,1 +178,Exit short,2025-03-12 23:00,M1,5604.75,1,278475,-1762.5,-0.63,0,962.5,0.35,-1837.5,-0.66,-8450,0.00,1 +178,Entry short,2025-03-12 22:00,M1,5569.5,1,278475,-1762.5,-0.63,0,962.5,0.35,-1837.5,-0.66,-8450,0.00,1 +179,Exit long,2025-03-13 00:00,M1,5604.5,1,280237.5,-12.5,0.00,0,1087.5,0.39,-450,-0.16,-8462.5,0.00,1 +179,Entry long,2025-03-12 23:00,M1,5604.75,1,280237.5,-12.5,0.00,0,1087.5,0.39,-450,-0.16,-8462.5,0.00,1 +180,Exit short,2025-03-13 01:00,M1,5593.75,1,280225,537.5,0.19,0,587.5,0.21,-1350,-0.48,-7925,0.00,1 +180,Entry short,2025-03-13 00:00,M1,5604.5,1,280225,537.5,0.19,0,587.5,0.21,-1350,-0.48,-7925,0.00,1 +181,Exit long,2025-03-13 02:00,M1,5626,1,279687.5,1612.5,0.58,0,1962.5,0.70,-100,-0.04,-6312.5,0.00,1 +181,Entry long,2025-03-13 01:00,M1,5593.75,1,279687.5,1612.5,0.58,0,1962.5,0.70,-100,-0.04,-6312.5,0.00,1 +182,Exit short,2025-03-13 03:00,M1,5602.75,1,281300,1162.5,0.41,0,1537.5,0.55,0,0.00,-5150,0.00,1 +182,Entry short,2025-03-13 02:00,M1,5626,1,281300,1162.5,0.41,0,1537.5,0.55,0,0.00,-5150,0.00,1 +183,Exit long,2025-03-13 04:00,M1,5597.25,1,280137.5,-275,-0.10,0,62.5,0.02,-387.5,-0.14,-5425,0.00,1 +183,Entry long,2025-03-13 03:00,M1,5602.75,1,280137.5,-275,-0.10,0,62.5,0.02,-387.5,-0.14,-5425,0.00,1 +184,Exit short,2025-03-13 06:00,M1,5609.25,1,279862.5,-600,-0.21,0,87.5,0.03,-687.5,-0.25,-6025,0.00,1 +184,Entry short,2025-03-13 04:00,M1,5597.25,1,279862.5,-600,-0.21,0,87.5,0.03,-687.5,-0.25,-6025,0.00,1 +185,Exit long,2025-03-13 07:00,M1,5620.5,1,280462.5,562.5,0.20,0,600,0.21,-25,-0.01,-5462.5,0.00,1 +185,Entry long,2025-03-13 06:00,M1,5609.25,1,280462.5,562.5,0.20,0,600,0.21,-25,-0.01,-5462.5,0.00,1 +186,Exit short,2025-03-13 08:00,M1,5611,1,281025,475,0.17,0,525,0.19,-125,-0.04,-4987.5,0.00,1 +186,Entry short,2025-03-13 07:00,M1,5620.5,1,281025,475,0.17,0,525,0.19,-125,-0.04,-4987.5,0.00,1 +187,Exit long,2025-03-13 09:00,M1,5612.25,1,280550,62.5,0.02,0,137.5,0.05,-362.5,-0.13,-4925,0.00,1 +187,Entry long,2025-03-13 08:00,M1,5611,1,280550,62.5,0.02,0,137.5,0.05,-362.5,-0.13,-4925,0.00,1 +188,Exit short,2025-03-13 10:00,M1,5603.25,1,280612.5,450,0.16,0,487.5,0.17,-50,-0.02,-4475,0.00,1 +188,Entry short,2025-03-13 09:00,M1,5612.25,1,280612.5,450,0.16,0,487.5,0.17,-50,-0.02,-4475,0.00,1 +189,Exit long,2025-03-13 11:00,M1,5588.5,1,280162.5,-737.5,-0.26,0,37.5,0.01,-850,-0.30,-5212.5,0.00,1 +189,Entry long,2025-03-13 10:00,M1,5603.25,1,280162.5,-737.5,-0.26,0,37.5,0.01,-850,-0.30,-5212.5,0.00,1 +190,Exit short,2025-03-13 12:00,M1,5580.75,1,279425,387.5,0.14,0,587.5,0.21,-237.5,-0.08,-4825,0.00,1 +190,Entry short,2025-03-13 11:00,M1,5588.5,1,279425,387.5,0.14,0,587.5,0.21,-237.5,-0.08,-4825,0.00,1 +191,Exit long,2025-03-13 13:00,M1,5577.5,1,279037.5,-162.5,-0.06,0,187.5,0.07,-500,-0.18,-4987.5,0.00,1 +191,Entry long,2025-03-13 12:00,M1,5580.75,1,279037.5,-162.5,-0.06,0,187.5,0.07,-500,-0.18,-4987.5,0.00,1 +192,Exit short,2025-03-13 14:00,M1,5582,1,278875,-225,-0.08,0,700,0.25,-375,-0.13,-5212.5,0.00,1 +192,Entry short,2025-03-13 13:00,M1,5577.5,1,278875,-225,-0.08,0,700,0.25,-375,-0.13,-5212.5,0.00,1 +193,Exit long,2025-03-13 15:00,M1,5567,1,279100,-750,-0.27,0,150,0.05,-900,-0.32,-5962.5,0.00,1 +193,Entry long,2025-03-13 14:00,M1,5582,1,279100,-750,-0.27,0,150,0.05,-900,-0.32,-5962.5,0.00,1 +194,Exit short,2025-03-13 16:00,M1,5580,1,278350,-650,-0.23,0,387.5,0.14,-675,-0.24,-6612.5,0.00,1 +194,Entry short,2025-03-13 15:00,M1,5567,1,278350,-650,-0.23,0,387.5,0.14,-675,-0.24,-6612.5,0.00,1 +195,Exit long,2025-03-13 17:00,M1,5604.5,1,279000,1225,0.44,0,1275,0.46,-400,-0.14,-5387.5,0.00,1 +195,Entry long,2025-03-13 16:00,M1,5580,1,279000,1225,0.44,0,1275,0.46,-400,-0.14,-5387.5,0.00,1 +196,Exit short,2025-03-13 18:00,M1,5599.75,1,280225,237.5,0.08,0,450,0.16,-262.5,-0.09,-5150,0.00,1 +196,Entry short,2025-03-13 17:00,M1,5604.5,1,280225,237.5,0.08,0,450,0.16,-262.5,-0.09,-5150,0.00,1 +197,Exit long,2025-03-13 19:00,M1,5590.75,1,279987.5,-450,-0.16,0,162.5,0.06,-975,-0.35,-5600,0.00,1 +197,Entry long,2025-03-13 18:00,M1,5599.75,1,279987.5,-450,-0.16,0,162.5,0.06,-975,-0.35,-5600,0.00,1 +198,Exit short,2025-03-13 20:00,M1,5594.75,1,279537.5,-200,-0.07,0,775,0.28,-662.5,-0.24,-5800,0.00,1 +198,Entry short,2025-03-13 19:00,M1,5590.75,1,279537.5,-200,-0.07,0,775,0.28,-662.5,-0.24,-5800,0.00,1 +199,Exit long,2025-03-13 21:00,M1,5563,1,279737.5,-1587.5,-0.57,0,400,0.14,-1900,-0.68,-7387.5,0.00,1 +199,Entry long,2025-03-13 20:00,M1,5594.75,1,279737.5,-1587.5,-0.57,0,400,0.14,-1900,-0.68,-7387.5,0.00,1 +200,Exit short,2025-03-13 22:00,M1,5572.75,1,278150,-487.5,-0.18,0,300,0.11,-2037.5,-0.73,-7875,0.00,1 +200,Entry short,2025-03-13 21:00,M1,5563,1,278150,-487.5,-0.18,0,300,0.11,-2037.5,-0.73,-7875,0.00,1 +201,Exit long,2025-03-13 23:00,M1,5537.75,1,278637.5,-1750,-0.63,0,487.5,0.17,-1825,-0.65,-9625,0.00,1 +201,Entry long,2025-03-13 22:00,M1,5572.75,1,278637.5,-1750,-0.63,0,487.5,0.17,-1825,-0.65,-9625,0.00,1 +202,Exit short,2025-03-14 00:00,M1,5538.75,1,276887.5,-50,-0.02,0,600,0.22,-1050,-0.38,-9675,0.00,1 +202,Entry short,2025-03-13 23:00,M1,5537.75,1,276887.5,-50,-0.02,0,600,0.22,-1050,-0.38,-9675,0.00,1 +203,Exit long,2025-03-14 01:00,M1,5513.25,1,276937.5,-1275,-0.46,0,362.5,0.13,-1475,-0.53,-10950,0.00,1 +203,Entry long,2025-03-14 00:00,M1,5538.75,1,276937.5,-1275,-0.46,0,362.5,0.13,-1475,-0.53,-10950,0.00,1 +204,Exit short,2025-03-14 02:00,M1,5552,1,275662.5,-1937.5,-0.70,0,62.5,0.02,-2112.5,-0.77,-12887.5,0.00,1 +204,Entry short,2025-03-14 01:00,M1,5513.25,1,275662.5,-1937.5,-0.70,0,62.5,0.02,-2112.5,-0.77,-12887.5,0.00,1 +205,Exit long,2025-03-14 03:00,M1,5528.25,1,277600,-1187.5,-0.43,0,350,0.13,-1900,-0.68,-14075,0.00,1 +205,Entry long,2025-03-14 02:00,M1,5552,1,277600,-1187.5,-0.43,0,350,0.13,-1900,-0.68,-14075,0.00,1 +206,Exit short,2025-03-14 04:00,M1,5537.5,1,276412.5,-462.5,-0.17,0,75,0.03,-837.5,-0.30,-14537.5,0.00,1 +206,Entry short,2025-03-14 03:00,M1,5528.25,1,276412.5,-462.5,-0.17,0,75,0.03,-837.5,-0.30,-14537.5,0.00,1 +207,Exit long,2025-03-14 06:00,M1,5552.5,1,276875,750,0.27,0,975,0.35,0,0.00,-13787.5,0.00,1 +207,Entry long,2025-03-14 04:00,M1,5537.5,1,276875,750,0.27,0,975,0.35,0,0.00,-13787.5,0.00,1 +208,Exit short,2025-03-14 07:00,M1,5552.75,1,277625,-12.5,0.00,0,112.5,0.04,-300,-0.11,-13800,0.00,1 +208,Entry short,2025-03-14 06:00,M1,5552.5,1,277625,-12.5,0.00,0,112.5,0.04,-300,-0.11,-13800,0.00,1 +209,Exit long,2025-03-14 08:00,M1,5561.75,1,277637.5,450,0.16,0,712.5,0.26,-262.5,-0.09,-13350,0.00,1 +209,Entry long,2025-03-14 07:00,M1,5552.75,1,277637.5,450,0.16,0,712.5,0.26,-262.5,-0.09,-13350,0.00,1 +210,Exit short,2025-03-14 08:00,,5561.75,1,278087.5,0,0.00,0,0,0.00,0,0.00,-13350,0.00,0 +210,Entry short,2025-03-14 08:00,M1,5561.75,1,278087.5,0,0.00,0,0,0.00,0,0.00,-13350,0.00,0 \ No newline at end of file diff --git a/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/meta.json b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/meta.json new file mode 100644 index 00000000..68c63657 --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/meta.json @@ -0,0 +1,16 @@ +{ + "title": "HM G2-36 session.ismarket", + "slug": "hm-g236-es1-60-dst-nov", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:01:22.037Z", + "chart": { + "symbol": "CME_MINI:ES1!", + "interval": "60", + "from": "2025-10-26", + "to": "2025-11-07" + }, + "pine_sha256": "7c5b7e36130c230e6ad26f5525ab527486a008e2d6eaa1a5e41da41cfa256a1d", + "tv_trades_sha256": "a7fbd62928dc49caf7bcdc6c9d8569139eab036860aa375aa0fc2c7db9b408b9", + "tv_trades_rows": 418 +} diff --git a/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/metrics.json b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/metrics.json new file mode 100644 index 00000000..cb6041dd --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-g236-es1-60-dst-nov", + "symbol": "CME_MINI:ES1!", + "interval": "60", + "from": "2025-10-26", + "to": "2025-11-07", + "deepBacktesting": true, + "trades": 209, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "7c5b7e36130c230e6ad26f5525ab527486a008e2d6eaa1a5e41da41cfa256a1d", + "tvTradesCsvHash": "a7fbd62928dc49caf7bcdc6c9d8569139eab036860aa375aa0fc2c7db9b408b9", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74408", + "symbol": "CME_MINI:ES1!", + "interval": "60", + "requestedRange": { + "from": 1761436800000, + "to": 1762473600000 + }, + "returnedRange": { + "from": 1761516000000, + "to": 1762473600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "d9ed91d21a449c6aa884b9b2984bc8562ec0bbc5e93889beb91682363d3453d3", + "decodedReportSha256": "63f8540b624b68698af1094b9771ca67f7bf9b76f7ff3ad6fe2b184067742940", + "closedTrades": 209, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:01:22.036Z" +} diff --git a/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/strategy.pine b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/strategy.pine new file mode 100644 index 00000000..182c911e --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/strategy.pine @@ -0,0 +1,17 @@ +// H-MEASURE (R5 wave H) row G2-36: which chart bars does TradingView flag +// session.ismarket on? process_orders_on_close=true: every bar reverses the +// position at its own close, so every trade's ENTRY is one chart bar (TradingView +// dates a process_orders_on_close fill at the bar's open) and the entry's Signal +// (its comment) is the flag TradingView evaluated on that bar: "M1" when +// session.ismarket is true, "M0" when it is false. +//@version=6 +strategy("HM G2-36 session.ismarket", overlay=true, initial_capital=1000000000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=true, margin_long=100, margin_short=100) + +flag = session.ismarket ? "M1" : "M0" +if bar_index % 2 == 0 + strategy.entry("L" + flag, strategy.long, comment=flag) +else + strategy.entry("S" + flag, strategy.short, comment=flag) diff --git a/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/tv_trades.csv b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/tv_trades.csv new file mode 100644 index 00000000..ee151bec --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-es1-60-dst-nov/tv_trades.csv @@ -0,0 +1,419 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-10-27 07:00,M1,6870.75,1,343600,-62.5,-0.02,0,237.5,0.07,-162.5,-0.05,-62.5,0.00,1 +1,Entry long,2025-10-27 06:00,M1,6872,1,343600,-62.5,-0.02,0,237.5,0.07,-162.5,-0.05,-62.5,0.00,1 +2,Exit short,2025-10-27 08:00,M1,6872.25,1,343537.5,-75,-0.02,0,75,0.02,-300,-0.09,-137.5,0.00,1 +2,Entry short,2025-10-27 07:00,M1,6870.75,1,343537.5,-75,-0.02,0,75,0.02,-300,-0.09,-137.5,0.00,1 +3,Exit long,2025-10-27 09:00,M1,6869.5,1,343612.5,-137.5,-0.04,0,50,0.01,-150,-0.04,-275,0.00,1 +3,Entry long,2025-10-27 08:00,M1,6872.25,1,343612.5,-137.5,-0.04,0,50,0.01,-150,-0.04,-275,0.00,1 +4,Exit short,2025-10-27 10:00,M1,6875,1,343475,-275,-0.08,0,100,0.03,-287.5,-0.08,-550,0.00,1 +4,Entry short,2025-10-27 09:00,M1,6869.5,1,343475,-275,-0.08,0,100,0.03,-287.5,-0.08,-550,0.00,1 +5,Exit long,2025-10-27 11:00,M1,6878.5,1,343750,175,0.05,0,312.5,0.09,-37.5,-0.01,-375,0.00,1 +5,Entry long,2025-10-27 10:00,M1,6875,1,343750,175,0.05,0,312.5,0.09,-37.5,-0.01,-375,0.00,1 +6,Exit short,2025-10-27 12:00,M1,6879.25,1,343925,-37.5,-0.01,0,75,0.02,-100,-0.03,-412.5,0.00,1 +6,Entry short,2025-10-27 11:00,M1,6878.5,1,343925,-37.5,-0.01,0,75,0.02,-100,-0.03,-412.5,0.00,1 +7,Exit long,2025-10-27 13:00,M1,6882.75,1,343962.5,175,0.05,0,225,0.07,-12.5,0.00,-237.5,0.00,1 +7,Entry long,2025-10-27 12:00,M1,6879.25,1,343962.5,175,0.05,0,225,0.07,-12.5,0.00,-237.5,0.00,1 +8,Exit short,2025-10-27 14:00,M1,6884.5,1,344137.5,-87.5,-0.03,0,25,0.01,-275,-0.08,-325,0.00,1 +8,Entry short,2025-10-27 13:00,M1,6882.75,1,344137.5,-87.5,-0.03,0,25,0.01,-275,-0.08,-325,0.00,1 +9,Exit long,2025-10-27 15:00,M1,6882.75,1,344225,-87.5,-0.03,0,225,0.07,-87.5,-0.03,-412.5,0.00,1 +9,Entry long,2025-10-27 14:00,M1,6884.5,1,344225,-87.5,-0.03,0,225,0.07,-87.5,-0.03,-412.5,0.00,1 +10,Exit short,2025-10-27 16:00,M1,6881,1,344137.5,87.5,0.03,0,312.5,0.09,-75,-0.02,-325,0.00,1 +10,Entry short,2025-10-27 15:00,M1,6882.75,1,344137.5,87.5,0.03,0,312.5,0.09,-75,-0.02,-325,0.00,1 +11,Exit long,2025-10-27 17:00,M1,6887,1,344050,300,0.09,0,300,0.09,-112.5,-0.03,-25,0.00,1 +11,Entry long,2025-10-27 16:00,M1,6881,1,344050,300,0.09,0,300,0.09,-112.5,-0.03,-25,0.00,1 +12,Exit short,2025-10-27 18:00,M1,6886.5,1,344350,25,0.01,0,150,0.04,-75,-0.02,0,0.00,1 +12,Entry short,2025-10-27 17:00,M1,6887,1,344350,25,0.01,0,150,0.04,-75,-0.02,0,0.00,1 +13,Exit long,2025-10-27 19:00,M1,6880.25,1,344325,-312.5,-0.09,0,125,0.04,-387.5,-0.11,-312.5,0.00,1 +13,Entry long,2025-10-27 18:00,M1,6886.5,1,344325,-312.5,-0.09,0,125,0.04,-387.5,-0.11,-312.5,0.00,1 +14,Exit short,2025-10-27 20:00,M1,6883,1,344012.5,-137.5,-0.04,0,50,0.01,-250,-0.07,-450,0.00,1 +14,Entry short,2025-10-27 19:00,M1,6880.25,1,344012.5,-137.5,-0.04,0,50,0.01,-250,-0.07,-450,0.00,1 +15,Exit long,2025-10-27 21:00,M1,6886.75,1,344150,187.5,0.05,0,300,0.09,-300,-0.09,-262.5,0.00,1 +15,Entry long,2025-10-27 20:00,M1,6883,1,344150,187.5,0.05,0,300,0.09,-300,-0.09,-262.5,0.00,1 +16,Exit short,2025-10-27 22:00,M1,6883,1,344337.5,187.5,0.05,0,350,0.10,-287.5,-0.08,-75,0.00,1 +16,Entry short,2025-10-27 21:00,M1,6886.75,1,344337.5,187.5,0.05,0,350,0.10,-287.5,-0.08,-75,0.00,1 +17,Exit long,2025-10-27 23:00,M1,6890.25,1,344150,362.5,0.11,0,462.5,0.13,-87.5,-0.03,287.5,0.00,1 +17,Entry long,2025-10-27 22:00,M1,6883,1,344150,362.5,0.11,0,462.5,0.13,-87.5,-0.03,287.5,0.00,1 +18,Exit short,2025-10-28 00:00,M1,6893.75,1,344512.5,-175,-0.05,0,25,0.01,-237.5,-0.07,112.5,0.00,1 +18,Entry short,2025-10-27 23:00,M1,6890.25,1,344512.5,-175,-0.05,0,25,0.01,-237.5,-0.07,112.5,0.00,1 +19,Exit long,2025-10-28 01:00,M1,6895.5,1,344687.5,87.5,0.03,0,112.5,0.03,-300,-0.09,200,0.00,1 +19,Entry long,2025-10-28 00:00,M1,6893.75,1,344687.5,87.5,0.03,0,112.5,0.03,-300,-0.09,200,0.00,1 +20,Exit short,2025-10-28 02:00,M1,6897.5,1,344775,-100,-0.03,0,87.5,0.03,-162.5,-0.05,100,0.00,1 +20,Entry short,2025-10-28 01:00,M1,6895.5,1,344775,-100,-0.03,0,87.5,0.03,-162.5,-0.05,100,0.00,1 +21,Exit long,2025-10-28 03:00,M1,6907.5,1,344875,500,0.14,0,712.5,0.21,-62.5,-0.02,600,0.00,1 +21,Entry long,2025-10-28 02:00,M1,6897.5,1,344875,500,0.14,0,712.5,0.21,-62.5,-0.02,600,0.00,1 +22,Exit short,2025-10-28 04:00,M1,6914,1,345375,-325,-0.09,0,25,0.01,-437.5,-0.13,275,0.00,1 +22,Entry short,2025-10-28 03:00,M1,6907.5,1,345375,-325,-0.09,0,25,0.01,-437.5,-0.13,275,0.00,1 +23,Exit long,2025-10-28 06:00,M1,6911.75,1,345700,-112.5,-0.03,0,0,0.00,-187.5,-0.05,162.5,0.00,1 +23,Entry long,2025-10-28 04:00,M1,6914,1,345700,-112.5,-0.03,0,0,0.00,-187.5,-0.05,162.5,0.00,1 +24,Exit short,2025-10-28 07:00,M1,6911,1,345587.5,37.5,0.01,0,50,0.01,-300,-0.09,200,0.00,1 +24,Entry short,2025-10-28 06:00,M1,6911.75,1,345587.5,37.5,0.01,0,50,0.01,-300,-0.09,200,0.00,1 +25,Exit long,2025-10-28 08:00,M1,6909,1,345550,-100,-0.03,0,50,0.01,-125,-0.04,100,0.00,1 +25,Entry long,2025-10-28 07:00,M1,6911,1,345550,-100,-0.03,0,50,0.01,-125,-0.04,100,0.00,1 +26,Exit short,2025-10-28 09:00,M1,6907.25,1,345450,87.5,0.03,0,150,0.04,-62.5,-0.02,187.5,0.00,1 +26,Entry short,2025-10-28 08:00,M1,6909,1,345450,87.5,0.03,0,150,0.04,-62.5,-0.02,187.5,0.00,1 +27,Exit long,2025-10-28 10:00,M1,6907.75,1,345362.5,25,0.01,0,100,0.03,-100,-0.03,212.5,0.00,1 +27,Entry long,2025-10-28 09:00,M1,6907.25,1,345362.5,25,0.01,0,100,0.03,-100,-0.03,212.5,0.00,1 +28,Exit short,2025-10-28 11:00,M1,6908.25,1,345387.5,-25,-0.01,0,87.5,0.03,-112.5,-0.03,187.5,0.00,1 +28,Entry short,2025-10-28 10:00,M1,6907.75,1,345387.5,-25,-0.01,0,87.5,0.03,-112.5,-0.03,187.5,0.00,1 +29,Exit long,2025-10-28 12:00,M1,6905,1,345412.5,-162.5,-0.05,0,37.5,0.01,-175,-0.05,25,0.00,1 +29,Entry long,2025-10-28 11:00,M1,6908.25,1,345412.5,-162.5,-0.05,0,37.5,0.01,-175,-0.05,25,0.00,1 +30,Exit short,2025-10-28 13:00,M1,6904.75,1,345250,12.5,0.00,0,137.5,0.04,-75,-0.02,37.5,0.00,1 +30,Entry short,2025-10-28 12:00,M1,6905,1,345250,12.5,0.00,0,137.5,0.04,-75,-0.02,37.5,0.00,1 +31,Exit long,2025-10-28 14:00,M1,6903.75,1,345237.5,-50,-0.01,0,250,0.07,-62.5,-0.02,-12.5,0.00,1 +31,Entry long,2025-10-28 13:00,M1,6904.75,1,345237.5,-50,-0.01,0,250,0.07,-62.5,-0.02,-12.5,0.00,1 +32,Exit short,2025-10-28 15:00,M1,6903.5,1,345187.5,12.5,0.00,0,75,0.02,-250,-0.07,0,0.00,1 +32,Entry short,2025-10-28 14:00,M1,6903.75,1,345187.5,12.5,0.00,0,75,0.02,-250,-0.07,0,0.00,1 +33,Exit long,2025-10-28 16:00,M1,6906.75,1,345175,162.5,0.05,0,225,0.07,-100,-0.03,162.5,0.00,1 +33,Entry long,2025-10-28 15:00,M1,6903.5,1,345175,162.5,0.05,0,225,0.07,-100,-0.03,162.5,0.00,1 +34,Exit short,2025-10-28 17:00,M1,6907.25,1,345337.5,-25,-0.01,0,137.5,0.04,-75,-0.02,137.5,0.00,1 +34,Entry short,2025-10-28 16:00,M1,6906.75,1,345337.5,-25,-0.01,0,137.5,0.04,-75,-0.02,137.5,0.00,1 +35,Exit long,2025-10-28 18:00,M1,6907.5,1,345362.5,12.5,0.00,0,162.5,0.05,-62.5,-0.02,150,0.00,1 +35,Entry long,2025-10-28 17:00,M1,6907.25,1,345362.5,12.5,0.00,0,162.5,0.05,-62.5,-0.02,150,0.00,1 +36,Exit short,2025-10-28 19:00,M1,6912.5,1,345375,-250,-0.07,0,25,0.01,-325,-0.09,-100,0.00,1 +36,Entry short,2025-10-28 18:00,M1,6907.5,1,345375,-250,-0.07,0,25,0.01,-325,-0.09,-100,0.00,1 +37,Exit long,2025-10-28 20:00,M1,6916,1,345625,175,0.05,0,287.5,0.08,-12.5,0.00,75,0.00,1 +37,Entry long,2025-10-28 19:00,M1,6912.5,1,345625,175,0.05,0,287.5,0.08,-12.5,0.00,75,0.00,1 +38,Exit short,2025-10-28 21:00,M1,6911.5,1,345800,225,0.07,0,462.5,0.13,-575,-0.17,300,0.00,1 +38,Entry short,2025-10-28 20:00,M1,6916,1,345800,225,0.07,0,462.5,0.13,-575,-0.17,300,0.00,1 +39,Exit long,2025-10-28 22:00,M1,6913.75,1,345575,112.5,0.03,0,375,0.11,-75,-0.02,412.5,0.00,1 +39,Entry long,2025-10-28 21:00,M1,6911.5,1,345575,112.5,0.03,0,375,0.11,-75,-0.02,412.5,0.00,1 +40,Exit short,2025-10-28 23:00,M1,6911,1,345687.5,137.5,0.04,0,537.5,0.16,-187.5,-0.05,550,0.00,1 +40,Entry short,2025-10-28 22:00,M1,6913.75,1,345687.5,137.5,0.04,0,537.5,0.16,-187.5,-0.05,550,0.00,1 +41,Exit long,2025-10-29 00:00,M1,6924.25,1,345550,662.5,0.19,0,950,0.27,-12.5,0.00,1212.5,0.00,1 +41,Entry long,2025-10-28 23:00,M1,6911,1,345550,662.5,0.19,0,950,0.27,-12.5,0.00,1212.5,0.00,1 +42,Exit short,2025-10-29 01:00,M1,6931.5,1,346212.5,-362.5,-0.10,0,12.5,0.00,-412.5,-0.12,850,0.00,1 +42,Entry short,2025-10-29 00:00,M1,6924.25,1,346212.5,-362.5,-0.10,0,12.5,0.00,-412.5,-0.12,850,0.00,1 +43,Exit long,2025-10-29 02:00,M1,6937,1,346575,275,0.08,0,412.5,0.12,-25,-0.01,1125,0.00,1 +43,Entry long,2025-10-29 01:00,M1,6931.5,1,346575,275,0.08,0,412.5,0.12,-25,-0.01,1125,0.00,1 +44,Exit short,2025-10-29 03:00,M1,6925.5,1,346850,575,0.17,0,637.5,0.18,-387.5,-0.11,1700,0.00,1 +44,Entry short,2025-10-29 02:00,M1,6937,1,346850,575,0.17,0,637.5,0.18,-387.5,-0.11,1700,0.00,1 +45,Exit long,2025-10-29 04:00,M1,6927.5,1,346275,100,0.03,0,125,0.04,-200,-0.06,1800,0.00,1 +45,Entry long,2025-10-29 03:00,M1,6925.5,1,346275,100,0.03,0,125,0.04,-200,-0.06,1800,0.00,1 +46,Exit short,2025-10-29 06:00,M1,6926,1,346375,75,0.02,0,112.5,0.03,-200,-0.06,1875,0.00,1 +46,Entry short,2025-10-29 04:00,M1,6927.5,1,346375,75,0.02,0,112.5,0.03,-200,-0.06,1875,0.00,1 +47,Exit long,2025-10-29 07:00,M1,6927.75,1,346300,87.5,0.03,0,150,0.04,-75,-0.02,1962.5,0.00,1 +47,Entry long,2025-10-29 06:00,M1,6926,1,346300,87.5,0.03,0,150,0.04,-75,-0.02,1962.5,0.00,1 +48,Exit short,2025-10-29 08:00,M1,6926,1,346387.5,87.5,0.03,0,162.5,0.05,-137.5,-0.04,2050,0.00,1 +48,Entry short,2025-10-29 07:00,M1,6927.75,1,346387.5,87.5,0.03,0,162.5,0.05,-137.5,-0.04,2050,0.00,1 +49,Exit long,2025-10-29 09:00,M1,6932.75,1,346300,337.5,0.10,0,450,0.13,-25,-0.01,2387.5,0.00,1 +49,Entry long,2025-10-29 08:00,M1,6926,1,346300,337.5,0.10,0,450,0.13,-25,-0.01,2387.5,0.00,1 +50,Exit short,2025-10-29 10:00,M1,6937.5,1,346637.5,-237.5,-0.07,0,12.5,0.00,-362.5,-0.10,2150,0.00,1 +50,Entry short,2025-10-29 09:00,M1,6932.75,1,346637.5,-237.5,-0.07,0,12.5,0.00,-362.5,-0.10,2150,0.00,1 +51,Exit long,2025-10-29 11:00,M1,6938,1,346875,25,0.01,0,87.5,0.03,-150,-0.04,2175,0.00,1 +51,Entry long,2025-10-29 10:00,M1,6937.5,1,346875,25,0.01,0,87.5,0.03,-150,-0.04,2175,0.00,1 +52,Exit short,2025-10-29 12:00,M1,6938.75,1,346900,-37.5,-0.01,0,250,0.07,-62.5,-0.02,2137.5,0.00,1 +52,Entry short,2025-10-29 11:00,M1,6938,1,346900,-37.5,-0.01,0,250,0.07,-62.5,-0.02,2137.5,0.00,1 +53,Exit long,2025-10-29 13:00,M1,6940.5,1,346937.5,87.5,0.03,0,125,0.04,-25,-0.01,2225,0.00,1 +53,Entry long,2025-10-29 12:00,M1,6938.75,1,346937.5,87.5,0.03,0,125,0.04,-25,-0.01,2225,0.00,1 +54,Exit short,2025-10-29 14:00,M1,6943,1,347025,-125,-0.04,0,50,0.01,-175,-0.05,2100,0.00,1 +54,Entry short,2025-10-29 13:00,M1,6940.5,1,347025,-125,-0.04,0,50,0.01,-175,-0.05,2100,0.00,1 +55,Exit long,2025-10-29 15:00,M1,6945,1,347150,100,0.03,0,125,0.04,-175,-0.05,2200,0.00,1 +55,Entry long,2025-10-29 14:00,M1,6943,1,347150,100,0.03,0,125,0.04,-175,-0.05,2200,0.00,1 +56,Exit short,2025-10-29 16:00,M1,6937.25,1,347250,387.5,0.11,0,462.5,0.13,-212.5,-0.06,2587.5,0.00,1 +56,Entry short,2025-10-29 15:00,M1,6945,1,347250,387.5,0.11,0,462.5,0.13,-212.5,-0.06,2587.5,0.00,1 +57,Exit long,2025-10-29 17:00,M1,6935.75,1,346862.5,-75,-0.02,0,25,0.01,-325,-0.09,2512.5,0.00,1 +57,Entry long,2025-10-29 16:00,M1,6937.25,1,346862.5,-75,-0.02,0,25,0.01,-325,-0.09,2512.5,0.00,1 +58,Exit short,2025-10-29 18:00,M1,6938.75,1,346787.5,-150,-0.04,0,62.5,0.02,-337.5,-0.10,2362.5,0.00,1 +58,Entry short,2025-10-29 17:00,M1,6935.75,1,346787.5,-150,-0.04,0,62.5,0.02,-337.5,-0.10,2362.5,0.00,1 +59,Exit long,2025-10-29 19:00,M1,6943,1,346937.5,212.5,0.06,0,487.5,0.14,-100,-0.03,2575,0.00,1 +59,Entry long,2025-10-29 18:00,M1,6938.75,1,346937.5,212.5,0.06,0,487.5,0.14,-100,-0.03,2575,0.00,1 +60,Exit short,2025-10-29 20:00,M1,6942,1,347150,50,0.01,0,275,0.08,-150,-0.04,2625,0.00,1 +60,Entry short,2025-10-29 19:00,M1,6943,1,347150,50,0.01,0,275,0.08,-150,-0.04,2625,0.00,1 +61,Exit long,2025-10-29 21:00,M1,6950.75,1,347100,437.5,0.13,0,487.5,0.14,-287.5,-0.08,3062.5,0.00,1 +61,Entry long,2025-10-29 20:00,M1,6942,1,347100,437.5,0.13,0,487.5,0.14,-287.5,-0.08,3062.5,0.00,1 +62,Exit short,2025-10-29 22:00,M1,6938,1,347537.5,637.5,0.18,0,912.5,0.26,-62.5,-0.02,3700,0.00,1 +62,Entry short,2025-10-29 21:00,M1,6950.75,1,347537.5,637.5,0.18,0,912.5,0.26,-62.5,-0.02,3700,0.00,1 +63,Exit long,2025-10-29 23:00,M1,6934.75,1,346900,-162.5,-0.05,0,237.5,0.07,-262.5,-0.08,3537.5,0.00,1 +63,Entry long,2025-10-29 22:00,M1,6938,1,346900,-162.5,-0.05,0,237.5,0.07,-262.5,-0.08,3537.5,0.00,1 +64,Exit short,2025-10-30 00:00,M1,6940,1,346737.5,-262.5,-0.08,0,250,0.07,-387.5,-0.11,3275,0.00,1 +64,Entry short,2025-10-29 23:00,M1,6934.75,1,346737.5,-262.5,-0.08,0,250,0.07,-387.5,-0.11,3275,0.00,1 +65,Exit long,2025-10-30 01:00,M1,6935,1,347000,-250,-0.07,0,125,0.04,-475,-0.14,3025,0.00,1 +65,Entry long,2025-10-30 00:00,M1,6940,1,347000,-250,-0.07,0,125,0.04,-475,-0.14,3025,0.00,1 +66,Exit short,2025-10-30 02:00,M1,6914.75,1,346750,1012.5,0.29,0,2637.5,0.76,-475,-0.14,4037.5,0.00,1 +66,Entry short,2025-10-30 01:00,M1,6935,1,346750,1012.5,0.29,0,2637.5,0.76,-475,-0.14,4037.5,0.00,1 +67,Exit long,2025-10-30 03:00,M1,6928,1,345737.5,662.5,0.19,0,700,0.20,-987.5,-0.29,4700,0.00,1 +67,Entry long,2025-10-30 02:00,M1,6914.75,1,345737.5,662.5,0.19,0,700,0.20,-987.5,-0.29,4700,0.00,1 +68,Exit short,2025-10-30 04:00,M1,6918,1,346400,500,0.14,0,1125,0.32,-875,-0.25,5200,0.00,1 +68,Entry short,2025-10-30 03:00,M1,6928,1,346400,500,0.14,0,1125,0.32,-875,-0.25,5200,0.00,1 +69,Exit long,2025-10-30 06:00,M1,6913,1,345900,-250,-0.07,0,212.5,0.06,-775,-0.22,4950,0.00,1 +69,Entry long,2025-10-30 04:00,M1,6918,1,345900,-250,-0.07,0,212.5,0.06,-775,-0.22,4950,0.00,1 +70,Exit short,2025-10-30 07:00,M1,6922.25,1,345650,-462.5,-0.13,0,237.5,0.07,-587.5,-0.17,4487.5,0.00,1 +70,Entry short,2025-10-30 06:00,M1,6913,1,345650,-462.5,-0.13,0,237.5,0.07,-587.5,-0.17,4487.5,0.00,1 +71,Exit long,2025-10-30 08:00,M1,6933,1,346112.5,537.5,0.16,0,887.5,0.26,0,0.00,5025,0.00,1 +71,Entry long,2025-10-30 07:00,M1,6922.25,1,346112.5,537.5,0.16,0,887.5,0.26,0,0.00,5025,0.00,1 +72,Exit short,2025-10-30 09:00,M1,6935.75,1,346650,-137.5,-0.04,0,337.5,0.10,-312.5,-0.09,4887.5,0.00,1 +72,Entry short,2025-10-30 08:00,M1,6933,1,346650,-137.5,-0.04,0,337.5,0.10,-312.5,-0.09,4887.5,0.00,1 +73,Exit long,2025-10-30 10:00,M1,6944.25,1,346787.5,425,0.12,0,650,0.19,-12.5,0.00,5312.5,0.00,1 +73,Entry long,2025-10-30 09:00,M1,6935.75,1,346787.5,425,0.12,0,650,0.19,-12.5,0.00,5312.5,0.00,1 +74,Exit short,2025-10-30 11:00,M1,6948,1,347212.5,-187.5,-0.05,0,12.5,0.00,-475,-0.14,5125,0.00,1 +74,Entry short,2025-10-30 10:00,M1,6944.25,1,347212.5,-187.5,-0.05,0,12.5,0.00,-475,-0.14,5125,0.00,1 +75,Exit long,2025-10-30 12:00,M1,6914.25,1,347400,-1687.5,-0.49,0,50,0.01,-2350,-0.68,3437.5,0.00,1 +75,Entry long,2025-10-30 11:00,M1,6948,1,347400,-1687.5,-0.49,0,50,0.01,-2350,-0.68,3437.5,0.00,1 +76,Exit short,2025-10-30 13:00,M1,6929.75,1,345712.5,-775,-0.22,0,100,0.03,-1237.5,-0.36,2662.5,0.00,1 +76,Entry short,2025-10-30 12:00,M1,6914.25,1,345712.5,-775,-0.22,0,100,0.03,-1237.5,-0.36,2662.5,0.00,1 +77,Exit long,2025-10-30 14:00,M1,6919.25,1,346487.5,-525,-0.15,0,25,0.01,-687.5,-0.20,2137.5,0.00,1 +77,Entry long,2025-10-30 13:00,M1,6929.75,1,346487.5,-525,-0.15,0,25,0.01,-687.5,-0.20,2137.5,0.00,1 +78,Exit short,2025-10-30 15:00,M1,6927.25,1,345962.5,-400,-0.12,0,375,0.11,-675,-0.20,1737.5,0.00,1 +78,Entry short,2025-10-30 14:00,M1,6919.25,1,345962.5,-400,-0.12,0,375,0.11,-675,-0.20,1737.5,0.00,1 +79,Exit long,2025-10-30 16:00,M1,6922,1,346362.5,-262.5,-0.08,0,112.5,0.03,-562.5,-0.16,1475,0.00,1 +79,Entry long,2025-10-30 15:00,M1,6927.25,1,346362.5,-262.5,-0.08,0,112.5,0.03,-562.5,-0.16,1475,0.00,1 +80,Exit short,2025-10-30 17:00,M1,6908.25,1,346100,687.5,0.20,0,812.5,0.23,-187.5,-0.05,2162.5,0.00,1 +80,Entry short,2025-10-30 16:00,M1,6922,1,346100,687.5,0.20,0,812.5,0.23,-187.5,-0.05,2162.5,0.00,1 +81,Exit long,2025-10-30 18:00,M1,6918,1,345412.5,487.5,0.14,0,600,0.17,-75,-0.02,2650,0.00,1 +81,Entry long,2025-10-30 17:00,M1,6908.25,1,345412.5,487.5,0.14,0,600,0.17,-75,-0.02,2650,0.00,1 +82,Exit short,2025-10-30 19:00,M1,6921.75,1,345900,-187.5,-0.05,0,75,0.02,-587.5,-0.17,2462.5,0.00,1 +82,Entry short,2025-10-30 18:00,M1,6918,1,345900,-187.5,-0.05,0,75,0.02,-587.5,-0.17,2462.5,0.00,1 +83,Exit long,2025-10-30 20:00,M1,6891,1,346087.5,-1537.5,-0.44,0,150,0.04,-1562.5,-0.45,925,0.00,1 +83,Entry long,2025-10-30 19:00,M1,6921.75,1,346087.5,-1537.5,-0.44,0,150,0.04,-1562.5,-0.45,925,0.00,1 +84,Exit short,2025-10-30 21:00,M1,6904.25,1,344550,-662.5,-0.19,0,1162.5,0.34,-987.5,-0.29,262.5,0.00,1 +84,Entry short,2025-10-30 20:00,M1,6891,1,344550,-662.5,-0.19,0,1162.5,0.34,-987.5,-0.29,262.5,0.00,1 +85,Exit long,2025-10-30 22:00,M1,6898.25,1,345212.5,-300,-0.09,0,475,0.14,-900,-0.26,-37.5,0.00,1 +85,Entry long,2025-10-30 21:00,M1,6904.25,1,345212.5,-300,-0.09,0,475,0.14,-900,-0.26,-37.5,0.00,1 +86,Exit short,2025-10-30 23:00,M1,6887.75,1,344912.5,525,0.15,0,725,0.21,-562.5,-0.16,487.5,0.00,1 +86,Entry short,2025-10-30 22:00,M1,6898.25,1,344912.5,525,0.15,0,725,0.21,-562.5,-0.16,487.5,0.00,1 +87,Exit long,2025-10-31 00:00,M1,6898.25,1,344387.5,525,0.15,0,900,0.26,-87.5,-0.03,1012.5,0.00,1 +87,Entry long,2025-10-30 23:00,M1,6887.75,1,344387.5,525,0.15,0,900,0.26,-87.5,-0.03,1012.5,0.00,1 +88,Exit short,2025-10-31 01:00,M1,6898.25,1,344912.5,0,0.00,0,712.5,0.21,-112.5,-0.03,1012.5,0.00,1 +88,Entry short,2025-10-31 00:00,M1,6898.25,1,344912.5,0,0.00,0,712.5,0.21,-112.5,-0.03,1012.5,0.00,1 +89,Exit long,2025-10-31 02:00,M1,6877.5,1,344912.5,-1037.5,-0.30,0,0,0.00,-1150,-0.33,-25,0.00,1 +89,Entry long,2025-10-31 01:00,M1,6898.25,1,344912.5,-1037.5,-0.30,0,0,0.00,-1150,-0.33,-25,0.00,1 +90,Exit short,2025-10-31 03:00,M1,6853.75,1,343875,1187.5,0.35,0,1250,0.36,-37.5,-0.01,1162.5,0.00,1 +90,Entry short,2025-10-31 02:00,M1,6877.5,1,343875,1187.5,0.35,0,1250,0.36,-37.5,-0.01,1162.5,0.00,1 +91,Exit long,2025-10-31 04:00,M1,6894,1,342687.5,2012.5,0.59,0,2137.5,0.62,-137.5,-0.04,3175,0.00,1 +91,Entry long,2025-10-31 03:00,M1,6853.75,1,342687.5,2012.5,0.59,0,2137.5,0.62,-137.5,-0.04,3175,0.00,1 +92,Exit short,2025-10-31 06:00,M1,6893.25,1,344700,37.5,0.01,0,162.5,0.05,-300,-0.09,3212.5,0.00,1 +92,Entry short,2025-10-31 04:00,M1,6894,1,344700,37.5,0.01,0,162.5,0.05,-300,-0.09,3212.5,0.00,1 +93,Exit long,2025-10-31 07:00,M1,6898.5,1,344662.5,262.5,0.08,0,300,0.09,-262.5,-0.08,3475,0.00,1 +93,Entry long,2025-10-31 06:00,M1,6893.25,1,344662.5,262.5,0.08,0,300,0.09,-262.5,-0.08,3475,0.00,1 +94,Exit short,2025-10-31 08:00,M1,6901,1,344925,-125,-0.04,0,462.5,0.13,-237.5,-0.07,3350,0.00,1 +94,Entry short,2025-10-31 07:00,M1,6898.5,1,344925,-125,-0.04,0,462.5,0.13,-237.5,-0.07,3350,0.00,1 +95,Exit long,2025-10-31 09:00,M1,6903.25,1,345050,112.5,0.03,0,150,0.04,-275,-0.08,3462.5,0.00,1 +95,Entry long,2025-10-31 08:00,M1,6901,1,345050,112.5,0.03,0,150,0.04,-275,-0.08,3462.5,0.00,1 +96,Exit short,2025-10-31 10:00,M1,6897,1,345162.5,312.5,0.09,0,500,0.14,-62.5,-0.02,3775,0.00,1 +96,Entry short,2025-10-31 09:00,M1,6903.25,1,345162.5,312.5,0.09,0,500,0.14,-62.5,-0.02,3775,0.00,1 +97,Exit long,2025-10-31 11:00,M1,6900,1,344850,150,0.04,0,175,0.05,-200,-0.06,3925,0.00,1 +97,Entry long,2025-10-31 10:00,M1,6897,1,344850,150,0.04,0,175,0.05,-200,-0.06,3925,0.00,1 +98,Exit short,2025-10-31 12:00,M1,6901,1,345000,-50,-0.01,0,12.5,0.00,-150,-0.04,3875,0.00,1 +98,Entry short,2025-10-31 11:00,M1,6900,1,345000,-50,-0.01,0,12.5,0.00,-150,-0.04,3875,0.00,1 +99,Exit long,2025-10-31 13:00,M1,6895.25,1,345050,-287.5,-0.08,0,12.5,0.00,-350,-0.10,3587.5,0.00,1 +99,Entry long,2025-10-31 12:00,M1,6901,1,345050,-287.5,-0.08,0,12.5,0.00,-350,-0.10,3587.5,0.00,1 +100,Exit short,2025-10-31 14:00,M1,6895.75,1,344762.5,-25,-0.01,0,50,0.01,-362.5,-0.11,3562.5,0.00,1 +100,Entry short,2025-10-31 13:00,M1,6895.25,1,344762.5,-25,-0.01,0,50,0.01,-362.5,-0.11,3562.5,0.00,1 +101,Exit long,2025-10-31 15:00,M1,6905.75,1,344787.5,500,0.15,0,525,0.15,-137.5,-0.04,4062.5,0.00,1 +101,Entry long,2025-10-31 14:00,M1,6895.75,1,344787.5,500,0.15,0,525,0.15,-137.5,-0.04,4062.5,0.00,1 +102,Exit short,2025-10-31 16:00,M1,6903.25,1,345287.5,125,0.04,0,475,0.14,-12.5,0.00,4187.5,0.00,1 +102,Entry short,2025-10-31 15:00,M1,6905.75,1,345287.5,125,0.04,0,475,0.14,-12.5,0.00,4187.5,0.00,1 +103,Exit long,2025-10-31 17:00,M1,6899.75,1,345162.5,-175,-0.05,0,137.5,0.04,-225,-0.07,4012.5,0.00,1 +103,Entry long,2025-10-31 16:00,M1,6903.25,1,345162.5,-175,-0.05,0,137.5,0.04,-225,-0.07,4012.5,0.00,1 +104,Exit short,2025-10-31 18:00,M1,6898,1,344987.5,87.5,0.03,0,325,0.09,-50,-0.01,4100,0.00,1 +104,Entry short,2025-10-31 17:00,M1,6899.75,1,344987.5,87.5,0.03,0,325,0.09,-50,-0.01,4100,0.00,1 +105,Exit long,2025-10-31 19:00,M1,6907.75,1,344900,487.5,0.14,0,675,0.20,-162.5,-0.05,4587.5,0.00,1 +105,Entry long,2025-10-31 18:00,M1,6898,1,344900,487.5,0.14,0,675,0.20,-162.5,-0.05,4587.5,0.00,1 +106,Exit short,2025-10-31 20:00,M1,6905.75,1,345387.5,100,0.03,0,300,0.09,-537.5,-0.16,4687.5,0.00,1 +106,Entry short,2025-10-31 19:00,M1,6907.75,1,345387.5,100,0.03,0,300,0.09,-537.5,-0.16,4687.5,0.00,1 +107,Exit long,2025-10-31 21:00,M1,6886.25,1,345287.5,-975,-0.28,0,25,0.01,-1287.5,-0.37,3712.5,0.00,1 +107,Entry long,2025-10-31 20:00,M1,6905.75,1,345287.5,-975,-0.28,0,25,0.01,-1287.5,-0.37,3712.5,0.00,1 +108,Exit short,2025-10-31 22:00,M1,6896.5,1,344312.5,-512.5,-0.15,0,625,0.18,-662.5,-0.19,3200,0.00,1 +108,Entry short,2025-10-31 21:00,M1,6886.25,1,344312.5,-512.5,-0.15,0,625,0.18,-662.5,-0.19,3200,0.00,1 +109,Exit long,2025-10-31 23:00,M1,6873.5,1,344825,-1150,-0.33,0,150,0.04,-1200,-0.35,2050,0.00,1 +109,Entry long,2025-10-31 22:00,M1,6896.5,1,344825,-1150,-0.33,0,150,0.04,-1200,-0.35,2050,0.00,1 +110,Exit short,2025-11-01 00:00,M1,6855,1,343675,925,0.27,0,1050,0.31,-137.5,-0.04,2975,0.00,1 +110,Entry short,2025-10-31 23:00,M1,6873.5,1,343675,925,0.27,0,1050,0.31,-137.5,-0.04,2975,0.00,1 +111,Exit long,2025-11-01 01:00,M1,6860.5,1,342750,275,0.08,0,575,0.17,-575,-0.17,3250,0.00,1 +111,Entry long,2025-11-01 00:00,M1,6855,1,342750,275,0.08,0,575,0.17,-575,-0.17,3250,0.00,1 +112,Exit short,2025-11-01 02:00,M1,6878,1,343025,-875,-0.26,0,50,0.01,-937.5,-0.27,2375,0.00,1 +112,Entry short,2025-11-01 01:00,M1,6860.5,1,343025,-875,-0.26,0,50,0.01,-937.5,-0.27,2375,0.00,1 +113,Exit long,2025-11-01 03:00,M1,6872,1,343900,-300,-0.09,0,1100,0.32,-312.5,-0.09,2075,0.00,1 +113,Entry long,2025-11-01 02:00,M1,6878,1,343900,-300,-0.09,0,1100,0.32,-312.5,-0.09,2075,0.00,1 +114,Exit short,2025-11-01 04:00,M1,6872.25,1,343600,-12.5,0.00,0,262.5,0.08,-137.5,-0.04,2062.5,0.00,1 +114,Entry short,2025-11-01 03:00,M1,6872,1,343600,-12.5,0.00,0,262.5,0.08,-137.5,-0.04,2062.5,0.00,1 +115,Exit long,2025-11-03 07:00,M1,6892.5,1,343612.5,1012.5,0.29,0,1137.5,0.33,0,0.00,3075,0.00,1 +115,Entry long,2025-11-01 04:00,M1,6872.25,1,343612.5,1012.5,0.29,0,1137.5,0.33,0,0.00,3075,0.00,1 +116,Exit short,2025-11-03 08:00,M1,6898.25,1,344625,-287.5,-0.08,0,25,0.01,-300,-0.09,2787.5,0.00,1 +116,Entry short,2025-11-03 07:00,M1,6892.5,1,344625,-287.5,-0.08,0,25,0.01,-300,-0.09,2787.5,0.00,1 +117,Exit long,2025-11-03 09:00,M1,6891.5,1,344912.5,-337.5,-0.10,0,50,0.01,-350,-0.10,2450,0.00,1 +117,Entry long,2025-11-03 08:00,M1,6898.25,1,344912.5,-337.5,-0.10,0,50,0.01,-350,-0.10,2450,0.00,1 +118,Exit short,2025-11-03 10:00,M1,6888.5,1,344575,150,0.04,0,212.5,0.06,-225,-0.07,2600,0.00,1 +118,Entry short,2025-11-03 09:00,M1,6891.5,1,344575,150,0.04,0,212.5,0.06,-225,-0.07,2600,0.00,1 +119,Exit long,2025-11-03 11:00,M1,6886.25,1,344425,-112.5,-0.03,0,25,0.01,-387.5,-0.11,2487.5,0.00,1 +119,Entry long,2025-11-03 10:00,M1,6888.5,1,344425,-112.5,-0.03,0,25,0.01,-387.5,-0.11,2487.5,0.00,1 +120,Exit short,2025-11-03 12:00,M1,6887,1,344312.5,-37.5,-0.01,0,125,0.04,-87.5,-0.03,2450,0.00,1 +120,Entry short,2025-11-03 11:00,M1,6886.25,1,344312.5,-37.5,-0.01,0,125,0.04,-87.5,-0.03,2450,0.00,1 +121,Exit long,2025-11-03 13:00,M1,6884.75,1,344350,-112.5,-0.03,0,87.5,0.03,-250,-0.07,2337.5,0.00,1 +121,Entry long,2025-11-03 12:00,M1,6887,1,344350,-112.5,-0.03,0,87.5,0.03,-250,-0.07,2337.5,0.00,1 +122,Exit short,2025-11-03 14:00,M1,6880.5,1,344237.5,212.5,0.06,0,350,0.10,-162.5,-0.05,2550,0.00,1 +122,Entry short,2025-11-03 13:00,M1,6884.75,1,344237.5,212.5,0.06,0,350,0.10,-162.5,-0.05,2550,0.00,1 +123,Exit long,2025-11-03 15:00,M1,6876,1,344025,-225,-0.07,0,0,0.00,-337.5,-0.10,2325,0.00,1 +123,Entry long,2025-11-03 14:00,M1,6880.5,1,344025,-225,-0.07,0,0,0.00,-337.5,-0.10,2325,0.00,1 +124,Exit short,2025-11-03 16:00,M1,6887.25,1,343800,-562.5,-0.16,0,137.5,0.04,-675,-0.20,1762.5,0.00,1 +124,Entry short,2025-11-03 15:00,M1,6876,1,343800,-562.5,-0.16,0,137.5,0.04,-675,-0.20,1762.5,0.00,1 +125,Exit long,2025-11-03 17:00,M1,6894.75,1,344362.5,375,0.11,0,750,0.22,-12.5,0.00,2137.5,0.00,1 +125,Entry long,2025-11-03 16:00,M1,6887.25,1,344362.5,375,0.11,0,750,0.22,-12.5,0.00,2137.5,0.00,1 +126,Exit short,2025-11-03 18:00,M1,6895.75,1,344737.5,-50,-0.01,0,75,0.02,-300,-0.09,2087.5,0.00,1 +126,Entry short,2025-11-03 17:00,M1,6894.75,1,344737.5,-50,-0.01,0,75,0.02,-300,-0.09,2087.5,0.00,1 +127,Exit long,2025-11-03 19:00,M1,6896,1,344787.5,12.5,0.00,0,362.5,0.11,0,0.00,2100,0.00,1 +127,Entry long,2025-11-03 18:00,M1,6895.75,1,344787.5,12.5,0.00,0,362.5,0.11,0,0.00,2100,0.00,1 +128,Exit short,2025-11-03 20:00,M1,6891.75,1,344800,212.5,0.06,0,225,0.07,-212.5,-0.06,2312.5,0.00,1 +128,Entry short,2025-11-03 19:00,M1,6896,1,344800,212.5,0.06,0,225,0.07,-212.5,-0.06,2312.5,0.00,1 +129,Exit long,2025-11-03 21:00,M1,6896.5,1,344587.5,237.5,0.07,0,275,0.08,-300,-0.09,2550,0.00,1 +129,Entry long,2025-11-03 20:00,M1,6891.75,1,344587.5,237.5,0.07,0,275,0.08,-300,-0.09,2550,0.00,1 +130,Exit short,2025-11-03 22:00,M1,6875.75,1,344825,1037.5,0.30,0,1212.5,0.35,-650,-0.19,3587.5,0.00,1 +130,Entry short,2025-11-03 21:00,M1,6896.5,1,344825,1037.5,0.30,0,1212.5,0.35,-650,-0.19,3587.5,0.00,1 +131,Exit long,2025-11-03 23:00,M1,6874.25,1,343787.5,-75,-0.02,0,237.5,0.07,-1312.5,-0.38,3512.5,0.00,1 +131,Entry long,2025-11-03 22:00,M1,6875.75,1,343787.5,-75,-0.02,0,237.5,0.07,-1312.5,-0.38,3512.5,0.00,1 +132,Exit short,2025-11-04 00:00,M1,6884.75,1,343712.5,-525,-0.15,0,275,0.08,-625,-0.18,2987.5,0.00,1 +132,Entry short,2025-11-03 23:00,M1,6874.25,1,343712.5,-525,-0.15,0,275,0.08,-625,-0.18,2987.5,0.00,1 +133,Exit long,2025-11-04 01:00,M1,6889.25,1,344237.5,225,0.07,0,287.5,0.08,-500,-0.15,3212.5,0.00,1 +133,Entry long,2025-11-04 00:00,M1,6884.75,1,344237.5,225,0.07,0,287.5,0.08,-500,-0.15,3212.5,0.00,1 +134,Exit short,2025-11-04 02:00,M1,6886.75,1,344462.5,125,0.04,0,212.5,0.06,-200,-0.06,3337.5,0.00,1 +134,Entry short,2025-11-04 01:00,M1,6889.25,1,344462.5,125,0.04,0,212.5,0.06,-200,-0.06,3337.5,0.00,1 +135,Exit long,2025-11-04 03:00,M1,6885,1,344337.5,-87.5,-0.03,0,137.5,0.04,-512.5,-0.15,3250,0.00,1 +135,Entry long,2025-11-04 02:00,M1,6886.75,1,344337.5,-87.5,-0.03,0,137.5,0.04,-512.5,-0.15,3250,0.00,1 +136,Exit short,2025-11-04 04:00,M1,6885.25,1,344250,-12.5,0.00,0,462.5,0.13,-125,-0.04,3237.5,0.00,1 +136,Entry short,2025-11-04 03:00,M1,6885,1,344250,-12.5,0.00,0,462.5,0.13,-125,-0.04,3237.5,0.00,1 +137,Exit long,2025-11-04 05:00,M1,6884,1,344262.5,-62.5,-0.02,0,362.5,0.11,-150,-0.04,3175,0.00,1 +137,Entry long,2025-11-04 04:00,M1,6885.25,1,344262.5,-62.5,-0.02,0,362.5,0.11,-150,-0.04,3175,0.00,1 +138,Exit short,2025-11-04 07:00,M1,6876.25,1,344200,387.5,0.11,0,462.5,0.13,0,0.00,3562.5,0.00,1 +138,Entry short,2025-11-04 05:00,M1,6884,1,344200,387.5,0.11,0,462.5,0.13,0,0.00,3562.5,0.00,1 +139,Exit long,2025-11-04 08:00,M1,6869.25,1,343812.5,-350,-0.10,0,0,0.00,-687.5,-0.20,3212.5,0.00,1 +139,Entry long,2025-11-04 07:00,M1,6876.25,1,343812.5,-350,-0.10,0,0,0.00,-687.5,-0.20,3212.5,0.00,1 +140,Exit short,2025-11-04 09:00,M1,6866,1,343462.5,162.5,0.05,0,362.5,0.11,-100,-0.03,3375,0.00,1 +140,Entry short,2025-11-04 08:00,M1,6869.25,1,343462.5,162.5,0.05,0,362.5,0.11,-100,-0.03,3375,0.00,1 +141,Exit long,2025-11-04 10:00,M1,6853,1,343300,-650,-0.19,0,12.5,0.00,-650,-0.19,2725,0.00,1 +141,Entry long,2025-11-04 09:00,M1,6866,1,343300,-650,-0.19,0,12.5,0.00,-650,-0.19,2725,0.00,1 +142,Exit short,2025-11-04 11:00,M1,6852.5,1,342650,25,0.01,0,37.5,0.01,-450,-0.13,2750,0.00,1 +142,Entry short,2025-11-04 10:00,M1,6853,1,342650,25,0.01,0,37.5,0.01,-450,-0.13,2750,0.00,1 +143,Exit long,2025-11-04 12:00,M1,6849.25,1,342625,-162.5,-0.05,0,112.5,0.03,-325,-0.09,2587.5,0.00,1 +143,Entry long,2025-11-04 11:00,M1,6852.5,1,342625,-162.5,-0.05,0,112.5,0.03,-325,-0.09,2587.5,0.00,1 +144,Exit short,2025-11-04 13:00,M1,6830,1,342462.5,962.5,0.28,0,1137.5,0.33,-50,-0.01,3550,0.00,1 +144,Entry short,2025-11-04 12:00,M1,6849.25,1,342462.5,962.5,0.28,0,1137.5,0.33,-50,-0.01,3550,0.00,1 +145,Exit long,2025-11-04 14:00,M1,6820.5,1,341500,-475,-0.14,0,25,0.01,-762.5,-0.22,3075,0.00,1 +145,Entry long,2025-11-04 13:00,M1,6830,1,341500,-475,-0.14,0,25,0.01,-762.5,-0.22,3075,0.00,1 +146,Exit short,2025-11-04 15:00,M1,6821.5,1,341025,-50,-0.01,0,437.5,0.13,-200,-0.06,3025,0.00,1 +146,Entry short,2025-11-04 14:00,M1,6820.5,1,341025,-50,-0.01,0,437.5,0.13,-200,-0.06,3025,0.00,1 +147,Exit long,2025-11-04 16:00,M1,6814.25,1,341075,-362.5,-0.11,0,12.5,0.00,-750,-0.22,2662.5,0.00,1 +147,Entry long,2025-11-04 15:00,M1,6821.5,1,341075,-362.5,-0.11,0,12.5,0.00,-750,-0.22,2662.5,0.00,1 +148,Exit short,2025-11-04 17:00,M1,6792.75,1,340712.5,1075,0.32,0,1137.5,0.33,-225,-0.07,3737.5,0.00,1 +148,Entry short,2025-11-04 16:00,M1,6814.25,1,340712.5,1075,0.32,0,1137.5,0.33,-225,-0.07,3737.5,0.00,1 +149,Exit long,2025-11-04 18:00,M1,6813.5,1,339637.5,1037.5,0.31,0,1125,0.33,-325,-0.10,4775,0.00,1 +149,Entry long,2025-11-04 17:00,M1,6792.75,1,339637.5,1037.5,0.31,0,1125,0.33,-325,-0.10,4775,0.00,1 +150,Exit short,2025-11-04 19:00,M1,6812.75,1,340675,37.5,0.01,0,137.5,0.04,-212.5,-0.06,4812.5,0.00,1 +150,Entry short,2025-11-04 18:00,M1,6813.5,1,340675,37.5,0.01,0,137.5,0.04,-212.5,-0.06,4812.5,0.00,1 +151,Exit long,2025-11-04 20:00,M1,6814.5,1,340637.5,87.5,0.03,0,387.5,0.11,-387.5,-0.11,4900,0.00,1 +151,Entry long,2025-11-04 19:00,M1,6812.75,1,340637.5,87.5,0.03,0,387.5,0.11,-387.5,-0.11,4900,0.00,1 +152,Exit short,2025-11-04 21:00,M1,6809,1,340725,275,0.08,0,612.5,0.18,-100,-0.03,5175,0.00,1 +152,Entry short,2025-11-04 20:00,M1,6814.5,1,340725,275,0.08,0,612.5,0.18,-100,-0.03,5175,0.00,1 +153,Exit long,2025-11-04 22:00,M1,6836.5,1,340450,1375,0.40,0,1437.5,0.42,-662.5,-0.19,6550,0.00,1 +153,Entry long,2025-11-04 21:00,M1,6809,1,340450,1375,0.40,0,1437.5,0.42,-662.5,-0.19,6550,0.00,1 +154,Exit short,2025-11-04 23:00,M1,6831.25,1,341825,262.5,0.08,0,537.5,0.16,-637.5,-0.19,6812.5,0.00,1 +154,Entry short,2025-11-04 22:00,M1,6836.5,1,341825,262.5,0.08,0,537.5,0.16,-637.5,-0.19,6812.5,0.00,1 +155,Exit long,2025-11-05 00:00,M1,6831.5,1,341562.5,12.5,0.00,0,337.5,0.10,-362.5,-0.11,6825,0.00,1 +155,Entry long,2025-11-04 23:00,M1,6831.25,1,341562.5,12.5,0.00,0,337.5,0.10,-362.5,-0.11,6825,0.00,1 +156,Exit short,2025-11-05 01:00,M1,6809.25,1,341575,1112.5,0.33,0,1275,0.37,-12.5,0.00,7937.5,0.00,1 +156,Entry short,2025-11-05 00:00,M1,6831.5,1,341575,1112.5,0.33,0,1275,0.37,-12.5,0.00,7937.5,0.00,1 +157,Exit long,2025-11-05 02:00,M1,6810.5,1,340462.5,62.5,0.02,0,200,0.06,-650,-0.19,8000,0.00,1 +157,Entry long,2025-11-05 01:00,M1,6809.25,1,340462.5,62.5,0.02,0,200,0.06,-650,-0.19,8000,0.00,1 +158,Exit short,2025-11-05 03:00,M1,6801.25,1,340525,462.5,0.14,0,787.5,0.23,-275,-0.08,8462.5,0.00,1 +158,Entry short,2025-11-05 02:00,M1,6810.5,1,340525,462.5,0.14,0,787.5,0.23,-275,-0.08,8462.5,0.00,1 +159,Exit long,2025-11-05 04:00,M1,6803.5,1,340062.5,112.5,0.03,0,687.5,0.20,-275,-0.08,8575,0.00,1 +159,Entry long,2025-11-05 03:00,M1,6801.25,1,340062.5,112.5,0.03,0,687.5,0.20,-275,-0.08,8575,0.00,1 +160,Exit short,2025-11-05 05:00,M1,6806.25,1,340175,-137.5,-0.04,0,337.5,0.10,-225,-0.07,8437.5,0.00,1 +160,Entry short,2025-11-05 04:00,M1,6803.5,1,340175,-137.5,-0.04,0,337.5,0.10,-225,-0.07,8437.5,0.00,1 +161,Exit long,2025-11-05 07:00,M1,6793.25,1,340312.5,-650,-0.19,0,12.5,0.00,-662.5,-0.19,7787.5,0.00,1 +161,Entry long,2025-11-05 05:00,M1,6806.25,1,340312.5,-650,-0.19,0,12.5,0.00,-662.5,-0.19,7787.5,0.00,1 +162,Exit short,2025-11-05 08:00,M1,6773.25,1,339662.5,1000,0.29,0,1012.5,0.30,-12.5,0.00,8787.5,0.00,1 +162,Entry short,2025-11-05 07:00,M1,6793.25,1,339662.5,1000,0.29,0,1012.5,0.30,-12.5,0.00,8787.5,0.00,1 +163,Exit long,2025-11-05 09:00,M1,6760,1,338662.5,-662.5,-0.20,0,12.5,0.00,-1237.5,-0.37,8125,0.00,1 +163,Entry long,2025-11-05 08:00,M1,6773.25,1,338662.5,-662.5,-0.20,0,12.5,0.00,-1237.5,-0.37,8125,0.00,1 +164,Exit short,2025-11-05 10:00,M1,6775.25,1,338000,-762.5,-0.23,0,362.5,0.11,-850,-0.25,7362.5,0.00,1 +164,Entry short,2025-11-05 09:00,M1,6760,1,338000,-762.5,-0.23,0,362.5,0.11,-850,-0.25,7362.5,0.00,1 +165,Exit long,2025-11-05 11:00,M1,6795,1,338762.5,987.5,0.29,0,1000,0.30,0,0.00,8350,0.00,1 +165,Entry long,2025-11-05 10:00,M1,6775.25,1,338762.5,987.5,0.29,0,1000,0.30,0,0.00,8350,0.00,1 +166,Exit short,2025-11-05 12:00,M1,6786.75,1,339750,412.5,0.12,0,612.5,0.18,-37.5,-0.01,8762.5,0.00,1 +166,Entry short,2025-11-05 11:00,M1,6795,1,339750,412.5,0.12,0,612.5,0.18,-37.5,-0.01,8762.5,0.00,1 +167,Exit long,2025-11-05 13:00,M1,6789,1,339337.5,112.5,0.03,0,600,0.18,-112.5,-0.03,8875,0.00,1 +167,Entry long,2025-11-05 12:00,M1,6786.75,1,339337.5,112.5,0.03,0,600,0.18,-112.5,-0.03,8875,0.00,1 +168,Exit short,2025-11-05 14:00,M1,6797.75,1,339450,-437.5,-0.13,0,287.5,0.08,-612.5,-0.18,8437.5,0.00,1 +168,Entry short,2025-11-05 13:00,M1,6789,1,339450,-437.5,-0.13,0,287.5,0.08,-612.5,-0.18,8437.5,0.00,1 +169,Exit long,2025-11-05 15:00,M1,6787,1,339887.5,-537.5,-0.16,0,50,0.01,-575,-0.17,7900,0.00,1 +169,Entry long,2025-11-05 14:00,M1,6797.75,1,339887.5,-537.5,-0.16,0,50,0.01,-575,-0.17,7900,0.00,1 +170,Exit short,2025-11-05 16:00,M1,6795,1,339350,-400,-0.12,0,250,0.07,-675,-0.20,7500,0.00,1 +170,Entry short,2025-11-05 15:00,M1,6787,1,339350,-400,-0.12,0,250,0.07,-675,-0.20,7500,0.00,1 +171,Exit long,2025-11-05 17:00,M1,6794,1,339750,-50,-0.01,0,312.5,0.09,-362.5,-0.11,7450,0.00,1 +171,Entry long,2025-11-05 16:00,M1,6795,1,339750,-50,-0.01,0,312.5,0.09,-362.5,-0.11,7450,0.00,1 +172,Exit short,2025-11-05 18:00,M1,6773.5,1,339700,1025,0.30,0,1037.5,0.31,-237.5,-0.07,8475,0.00,1 +172,Entry short,2025-11-05 17:00,M1,6794,1,339700,1025,0.30,0,1037.5,0.31,-237.5,-0.07,8475,0.00,1 +173,Exit long,2025-11-05 19:00,M1,6787.25,1,338675,687.5,0.20,0,775,0.23,-87.5,-0.03,9162.5,0.00,1 +173,Entry long,2025-11-05 18:00,M1,6773.5,1,338675,687.5,0.20,0,775,0.23,-87.5,-0.03,9162.5,0.00,1 +174,Exit short,2025-11-05 20:00,M1,6795.25,1,339362.5,-400,-0.12,0,262.5,0.08,-462.5,-0.14,8762.5,0.00,1 +174,Entry short,2025-11-05 19:00,M1,6787.25,1,339362.5,-400,-0.12,0,262.5,0.08,-462.5,-0.14,8762.5,0.00,1 +175,Exit long,2025-11-05 21:00,M1,6805.5,1,339762.5,512.5,0.15,0,625,0.18,-237.5,-0.07,9275,0.00,1 +175,Entry long,2025-11-05 20:00,M1,6795.25,1,339762.5,512.5,0.15,0,625,0.18,-237.5,-0.07,9275,0.00,1 +176,Exit short,2025-11-05 22:00,M1,6822,1,340275,-825,-0.24,0,787.5,0.23,-950,-0.28,8450,0.00,1 +176,Entry short,2025-11-05 21:00,M1,6805.5,1,340275,-825,-0.24,0,787.5,0.23,-950,-0.28,8450,0.00,1 +177,Exit long,2025-11-05 23:00,M1,6827,1,341100,250,0.07,0,462.5,0.14,-625,-0.18,8700,0.00,1 +177,Entry long,2025-11-05 22:00,M1,6822,1,341100,250,0.07,0,462.5,0.14,-625,-0.18,8700,0.00,1 +178,Exit short,2025-11-06 00:00,M1,6842.5,1,341350,-775,-0.23,0,550,0.16,-975,-0.29,7925,0.00,1 +178,Entry short,2025-11-05 23:00,M1,6827,1,341350,-775,-0.23,0,550,0.16,-975,-0.29,7925,0.00,1 +179,Exit long,2025-11-06 01:00,M1,6851,1,342125,425,0.12,0,762.5,0.22,-175,-0.05,8350,0.00,1 +179,Entry long,2025-11-06 00:00,M1,6842.5,1,342125,425,0.12,0,762.5,0.22,-175,-0.05,8350,0.00,1 +180,Exit short,2025-11-06 02:00,M1,6855.5,1,342550,-225,-0.07,0,100,0.03,-325,-0.09,8125,0.00,1 +180,Entry short,2025-11-06 01:00,M1,6851,1,342550,-225,-0.07,0,100,0.03,-325,-0.09,8125,0.00,1 +181,Exit long,2025-11-06 03:00,M1,6851.25,1,342775,-212.5,-0.06,0,0,0.00,-462.5,-0.13,7912.5,0.00,1 +181,Entry long,2025-11-06 02:00,M1,6855.5,1,342775,-212.5,-0.06,0,0,0.00,-462.5,-0.13,7912.5,0.00,1 +182,Exit short,2025-11-06 04:00,M1,6824.25,1,342562.5,1350,0.39,0,1412.5,0.41,-162.5,-0.05,9262.5,0.00,1 +182,Entry short,2025-11-06 03:00,M1,6851.25,1,342562.5,1350,0.39,0,1412.5,0.41,-162.5,-0.05,9262.5,0.00,1 +183,Exit long,2025-11-06 05:00,M1,6827.5,1,341212.5,162.5,0.05,0,325,0.10,-212.5,-0.06,9425,0.00,1 +183,Entry long,2025-11-06 04:00,M1,6824.25,1,341212.5,162.5,0.05,0,325,0.10,-212.5,-0.06,9425,0.00,1 +184,Exit short,2025-11-06 07:00,M1,6834.75,1,341375,-362.5,-0.11,0,337.5,0.10,-375,-0.11,9062.5,0.00,1 +184,Entry short,2025-11-06 05:00,M1,6827.5,1,341375,-362.5,-0.11,0,337.5,0.10,-375,-0.11,9062.5,0.00,1 +185,Exit long,2025-11-06 08:00,M1,6821,1,341737.5,-687.5,-0.20,0,12.5,0.00,-1212.5,-0.35,8375,0.00,1 +185,Entry long,2025-11-06 07:00,M1,6834.75,1,341737.5,-687.5,-0.20,0,12.5,0.00,-1212.5,-0.35,8375,0.00,1 +186,Exit short,2025-11-06 09:00,M1,6815,1,341050,300,0.09,0,775,0.23,0,0.00,8675,0.00,1 +186,Entry short,2025-11-06 08:00,M1,6821,1,341050,300,0.09,0,775,0.23,0,0.00,8675,0.00,1 +187,Exit long,2025-11-06 10:00,M1,6822.25,1,340750,362.5,0.11,0,400,0.12,-350,-0.10,9037.5,0.00,1 +187,Entry long,2025-11-06 09:00,M1,6815,1,340750,362.5,0.11,0,400,0.12,-350,-0.10,9037.5,0.00,1 +188,Exit short,2025-11-06 11:00,M1,6823.25,1,341112.5,-50,-0.01,0,150,0.04,-187.5,-0.05,8987.5,0.00,1 +188,Entry short,2025-11-06 10:00,M1,6822.25,1,341112.5,-50,-0.01,0,150,0.04,-187.5,-0.05,8987.5,0.00,1 +189,Exit long,2025-11-06 12:00,M1,6826.5,1,341162.5,162.5,0.05,0,212.5,0.06,-162.5,-0.05,9150,0.00,1 +189,Entry long,2025-11-06 11:00,M1,6823.25,1,341162.5,162.5,0.05,0,212.5,0.06,-162.5,-0.05,9150,0.00,1 +190,Exit short,2025-11-06 13:00,M1,6818,1,341325,425,0.12,0,500,0.15,-37.5,-0.01,9575,0.00,1 +190,Entry short,2025-11-06 12:00,M1,6826.5,1,341325,425,0.12,0,500,0.15,-37.5,-0.01,9575,0.00,1 +191,Exit long,2025-11-06 14:00,M1,6815.25,1,340900,-137.5,-0.04,0,162.5,0.05,-225,-0.07,9437.5,0.00,1 +191,Entry long,2025-11-06 13:00,M1,6818,1,340900,-137.5,-0.04,0,162.5,0.05,-225,-0.07,9437.5,0.00,1 +192,Exit short,2025-11-06 15:00,M1,6808.5,1,340762.5,337.5,0.10,0,375,0.11,-325,-0.10,9775,0.00,1 +192,Entry short,2025-11-06 14:00,M1,6815.25,1,340762.5,337.5,0.10,0,375,0.11,-325,-0.10,9775,0.00,1 +193,Exit long,2025-11-06 16:00,M1,6812.75,1,340425,212.5,0.06,0,312.5,0.09,-225,-0.07,9987.5,0.00,1 +193,Entry long,2025-11-06 15:00,M1,6808.5,1,340425,212.5,0.06,0,312.5,0.09,-225,-0.07,9987.5,0.00,1 +194,Exit short,2025-11-06 17:00,M1,6823.75,1,340637.5,-550,-0.16,0,25,0.01,-737.5,-0.22,9437.5,0.00,1 +194,Entry short,2025-11-06 16:00,M1,6812.75,1,340637.5,-550,-0.16,0,25,0.01,-737.5,-0.22,9437.5,0.00,1 +195,Exit long,2025-11-06 18:00,M1,6829,1,341187.5,262.5,0.08,0,475,0.14,-50,-0.01,9700,0.00,1 +195,Entry long,2025-11-06 17:00,M1,6823.75,1,341187.5,262.5,0.08,0,475,0.14,-50,-0.01,9700,0.00,1 +196,Exit short,2025-11-06 19:00,M1,6828.5,1,341450,25,0.01,0,100,0.03,-250,-0.07,9725,0.00,1 +196,Entry short,2025-11-06 18:00,M1,6829,1,341450,25,0.01,0,100,0.03,-250,-0.07,9725,0.00,1 +197,Exit long,2025-11-06 20:00,M1,6836.25,1,341425,387.5,0.11,0,487.5,0.14,-25,-0.01,10112.5,0.00,1 +197,Entry long,2025-11-06 19:00,M1,6828.5,1,341425,387.5,0.11,0,487.5,0.14,-25,-0.01,10112.5,0.00,1 +198,Exit short,2025-11-06 21:00,M1,6832,1,341812.5,212.5,0.06,0,262.5,0.08,-362.5,-0.11,10325,0.00,1 +198,Entry short,2025-11-06 20:00,M1,6836.25,1,341812.5,212.5,0.06,0,262.5,0.08,-362.5,-0.11,10325,0.00,1 +199,Exit long,2025-11-06 22:00,M1,6788.5,1,341600,-2175,-0.64,0,112.5,0.03,-2200,-0.64,8150,0.00,1 +199,Entry long,2025-11-06 21:00,M1,6832,1,341600,-2175,-0.64,0,112.5,0.03,-2200,-0.64,8150,0.00,1 +200,Exit short,2025-11-06 23:00,M1,6756.25,1,339425,1612.5,0.48,0,1712.5,0.50,-612.5,-0.18,9762.5,0.00,1 +200,Entry short,2025-11-06 22:00,M1,6788.5,1,339425,1612.5,0.48,0,1712.5,0.50,-612.5,-0.18,9762.5,0.00,1 +201,Exit long,2025-11-07 00:00,M1,6738.75,1,337812.5,-875,-0.26,0,525,0.16,-975,-0.29,8887.5,0.00,1 +201,Entry long,2025-11-06 23:00,M1,6756.25,1,337812.5,-875,-0.26,0,525,0.16,-975,-0.29,8887.5,0.00,1 +202,Exit short,2025-11-07 01:00,M1,6757.75,1,336937.5,-950,-0.28,0,262.5,0.08,-1187.5,-0.35,7937.5,0.00,1 +202,Entry short,2025-11-07 00:00,M1,6738.75,1,336937.5,-950,-0.28,0,262.5,0.08,-1187.5,-0.35,7937.5,0.00,1 +203,Exit long,2025-11-07 02:00,M1,6762,1,337887.5,212.5,0.06,0,562.5,0.17,-987.5,-0.29,8150,0.00,1 +203,Entry long,2025-11-07 01:00,M1,6757.75,1,337887.5,212.5,0.06,0,562.5,0.17,-987.5,-0.29,8150,0.00,1 +204,Exit short,2025-11-07 03:00,M1,6777,1,338100,-750,-0.22,0,125,0.04,-1125,-0.33,7400,0.00,1 +204,Entry short,2025-11-07 02:00,M1,6762,1,338100,-750,-0.22,0,125,0.04,-1125,-0.33,7400,0.00,1 +205,Exit long,2025-11-07 04:00,M1,6750,1,338850,-1350,-0.40,0,187.5,0.06,-1725,-0.51,6050,0.00,1 +205,Entry long,2025-11-07 03:00,M1,6777,1,338850,-1350,-0.40,0,187.5,0.06,-1725,-0.51,6050,0.00,1 +206,Exit short,2025-11-07 05:00,M1,6764.75,1,337500,-737.5,-0.22,0,62.5,0.02,-875,-0.26,5312.5,0.00,1 +206,Entry short,2025-11-07 04:00,M1,6750,1,337500,-737.5,-0.22,0,62.5,0.02,-875,-0.26,5312.5,0.00,1 +207,Exit long,2025-11-07 07:00,M1,6765,1,338237.5,12.5,0.00,0,225,0.07,-437.5,-0.13,5325,0.00,1 +207,Entry long,2025-11-07 05:00,M1,6764.75,1,338237.5,12.5,0.00,0,225,0.07,-437.5,-0.13,5325,0.00,1 +208,Exit short,2025-11-07 08:00,M1,6766.5,1,338250,-75,-0.02,0,375,0.11,-350,-0.10,5250,0.00,1 +208,Entry short,2025-11-07 07:00,M1,6765,1,338250,-75,-0.02,0,375,0.11,-350,-0.10,5250,0.00,1 +209,Exit long,2025-11-07 08:00,,6766.5,1,338325,0,0.00,0,0,0.00,0,0.00,5250,0.00,0 +209,Entry long,2025-11-07 08:00,M1,6766.5,1,338325,0,0.00,0,0,0.00,0,0.00,5250,0.00,0 \ No newline at end of file diff --git a/tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/meta.json b/tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/meta.json new file mode 100644 index 00000000..5031841b --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/meta.json @@ -0,0 +1,16 @@ +{ + "title": "HM G2-36 session.ismarket", + "slug": "hm-g236-es1-60-thanksgiving", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:01:26.476Z", + "chart": { + "symbol": "CME_MINI:ES1!", + "interval": "60", + "from": "2025-11-23", + "to": "2025-12-05" + }, + "pine_sha256": "7c5b7e36130c230e6ad26f5525ab527486a008e2d6eaa1a5e41da41cfa256a1d", + "tv_trades_sha256": "52512b7df99b764b8f13f8101b7e37481bdad5dfbf86620868f11fe175efaf8f", + "tv_trades_rows": 384 +} diff --git a/tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/metrics.json b/tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/metrics.json new file mode 100644 index 00000000..b4ab42bf --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-g236-es1-60-thanksgiving", + "symbol": "CME_MINI:ES1!", + "interval": "60", + "from": "2025-11-23", + "to": "2025-12-05", + "deepBacktesting": true, + "trades": 192, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "7c5b7e36130c230e6ad26f5525ab527486a008e2d6eaa1a5e41da41cfa256a1d", + "tvTradesCsvHash": "52512b7df99b764b8f13f8101b7e37481bdad5dfbf86620868f11fe175efaf8f", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74408", + "symbol": "CME_MINI:ES1!", + "interval": "60", + "requestedRange": { + "from": 1763856000000, + "to": 1764892800000 + }, + "returnedRange": { + "from": 1763938800000, + "to": 1764892800000 + }, + "rangeProof": "covered", + "rawFrameSha256": "77632bdb23ad173909228b126aad2cf528d2b5fe17a0fe9a0e98f9ecb51daec6", + "decodedReportSha256": "b6a530f0c09cc07420fd224fe4942a0d05a680e51b2efe4cc069dd789b8b5a97", + "closedTrades": 192, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:01:26.475Z" +} diff --git a/tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/strategy.pine b/tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/strategy.pine new file mode 100644 index 00000000..182c911e --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/strategy.pine @@ -0,0 +1,17 @@ +// H-MEASURE (R5 wave H) row G2-36: which chart bars does TradingView flag +// session.ismarket on? process_orders_on_close=true: every bar reverses the +// position at its own close, so every trade's ENTRY is one chart bar (TradingView +// dates a process_orders_on_close fill at the bar's open) and the entry's Signal +// (its comment) is the flag TradingView evaluated on that bar: "M1" when +// session.ismarket is true, "M0" when it is false. +//@version=6 +strategy("HM G2-36 session.ismarket", overlay=true, initial_capital=1000000000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=true, margin_long=100, margin_short=100) + +flag = session.ismarket ? "M1" : "M0" +if bar_index % 2 == 0 + strategy.entry("L" + flag, strategy.long, comment=flag) +else + strategy.entry("S" + flag, strategy.short, comment=flag) diff --git a/tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/tv_trades.csv b/tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/tv_trades.csv new file mode 100644 index 00000000..d4ed74d6 --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-es1-60-thanksgiving/tv_trades.csv @@ -0,0 +1,385 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-11-24 08:00,M1,6647.5,1,332662.5,-287.5,-0.09,0,0,0.00,-812.5,-0.24,-287.5,0.00,1 +1,Entry long,2025-11-24 07:00,M1,6653.25,1,332662.5,-287.5,-0.09,0,0,0.00,-812.5,-0.24,-287.5,0.00,1 +2,Exit short,2025-11-24 09:00,M1,6647,1,332375,25,0.01,0,275,0.08,-312.5,-0.09,-262.5,0.00,1 +2,Entry short,2025-11-24 08:00,M1,6647.5,1,332375,25,0.01,0,275,0.08,-312.5,-0.09,-262.5,0.00,1 +3,Exit long,2025-11-24 10:00,M1,6661,1,332350,700,0.21,0,712.5,0.21,-162.5,-0.05,437.5,0.00,1 +3,Entry long,2025-11-24 09:00,M1,6647,1,332350,700,0.21,0,712.5,0.21,-162.5,-0.05,437.5,0.00,1 +4,Exit short,2025-11-24 11:00,M1,6655.5,1,333050,275,0.08,0,350,0.11,-75,-0.02,712.5,0.00,1 +4,Entry short,2025-11-24 10:00,M1,6661,1,333050,275,0.08,0,350,0.11,-75,-0.02,712.5,0.00,1 +5,Exit long,2025-11-24 12:00,M1,6648.75,1,332775,-337.5,-0.10,0,62.5,0.02,-487.5,-0.15,375,0.00,1 +5,Entry long,2025-11-24 11:00,M1,6655.5,1,332775,-337.5,-0.10,0,62.5,0.02,-487.5,-0.15,375,0.00,1 +6,Exit short,2025-11-24 13:00,M1,6656.75,1,332437.5,-400,-0.12,0,62.5,0.02,-525,-0.16,-25,0.00,1 +6,Entry short,2025-11-24 12:00,M1,6648.75,1,332437.5,-400,-0.12,0,62.5,0.02,-525,-0.16,-25,0.00,1 +7,Exit long,2025-11-24 14:00,M1,6652.5,1,332837.5,-212.5,-0.06,0,12.5,0.00,-387.5,-0.12,-237.5,0.00,1 +7,Entry long,2025-11-24 13:00,M1,6656.75,1,332837.5,-212.5,-0.06,0,12.5,0.00,-387.5,-0.12,-237.5,0.00,1 +8,Exit short,2025-11-24 15:00,M1,6660.25,1,332625,-387.5,-0.12,0,275,0.08,-512.5,-0.15,-625,0.00,1 +8,Entry short,2025-11-24 14:00,M1,6652.5,1,332625,-387.5,-0.12,0,275,0.08,-512.5,-0.15,-625,0.00,1 +9,Exit long,2025-11-24 16:00,M1,6668,1,333012.5,387.5,0.12,0,462.5,0.14,-637.5,-0.19,-237.5,0.00,1 +9,Entry long,2025-11-24 15:00,M1,6660.25,1,333012.5,387.5,0.12,0,462.5,0.14,-637.5,-0.19,-237.5,0.00,1 +10,Exit short,2025-11-24 17:00,M1,6638,1,333400,1500,0.45,0,2150,0.64,-75,-0.02,1262.5,0.00,1 +10,Entry short,2025-11-24 16:00,M1,6668,1,333400,1500,0.45,0,2150,0.64,-75,-0.02,1262.5,0.00,1 +11,Exit long,2025-11-24 18:00,M1,6632.75,1,331900,-262.5,-0.08,0,200,0.06,-625,-0.19,1000,0.00,1 +11,Entry long,2025-11-24 17:00,M1,6638,1,331900,-262.5,-0.08,0,200,0.06,-625,-0.19,1000,0.00,1 +12,Exit short,2025-11-24 19:00,M1,6645.75,1,331637.5,-650,-0.20,0,325,0.10,-675,-0.20,350,0.00,1 +12,Entry short,2025-11-24 18:00,M1,6632.75,1,331637.5,-650,-0.20,0,325,0.10,-675,-0.20,350,0.00,1 +13,Exit long,2025-11-24 20:00,M1,6657.25,1,332287.5,575,0.17,0,650,0.20,-325,-0.10,925,0.00,1 +13,Entry long,2025-11-24 19:00,M1,6645.75,1,332287.5,575,0.17,0,650,0.20,-325,-0.10,925,0.00,1 +14,Exit short,2025-11-24 21:00,M1,6653,1,332862.5,212.5,0.06,0,275,0.08,-500,-0.15,1137.5,0.00,1 +14,Entry short,2025-11-24 20:00,M1,6657.25,1,332862.5,212.5,0.06,0,275,0.08,-500,-0.15,1137.5,0.00,1 +15,Exit long,2025-11-24 22:00,M1,6671.25,1,332650,912.5,0.27,0,1687.5,0.51,-312.5,-0.09,2050,0.00,1 +15,Entry long,2025-11-24 21:00,M1,6653,1,332650,912.5,0.27,0,1687.5,0.51,-312.5,-0.09,2050,0.00,1 +16,Exit short,2025-11-24 23:00,M1,6696.75,1,333562.5,-1275,-0.38,0,50,0.01,-1625,-0.49,775,0.00,1 +16,Entry short,2025-11-24 22:00,M1,6671.25,1,333562.5,-1275,-0.38,0,50,0.01,-1625,-0.49,775,0.00,1 +17,Exit long,2025-11-25 00:00,M1,6706.25,1,334837.5,475,0.14,0,975,0.29,0,0.00,1250,0.00,1 +17,Entry long,2025-11-24 23:00,M1,6696.75,1,334837.5,475,0.14,0,975,0.29,0,0.00,1250,0.00,1 +18,Exit short,2025-11-25 01:00,M1,6722,1,335312.5,-787.5,-0.23,0,300,0.09,-950,-0.28,462.5,0.00,1 +18,Entry short,2025-11-25 00:00,M1,6706.25,1,335312.5,-787.5,-0.23,0,300,0.09,-950,-0.28,462.5,0.00,1 +19,Exit long,2025-11-25 02:00,M1,6723.75,1,336100,87.5,0.03,0,350,0.10,-150,-0.04,550,0.00,1 +19,Entry long,2025-11-25 01:00,M1,6722,1,336100,87.5,0.03,0,350,0.10,-150,-0.04,550,0.00,1 +20,Exit short,2025-11-25 03:00,M1,6714.5,1,336187.5,462.5,0.14,0,687.5,0.20,-287.5,-0.09,1012.5,0.00,1 +20,Entry short,2025-11-25 02:00,M1,6723.75,1,336187.5,462.5,0.14,0,687.5,0.20,-287.5,-0.09,1012.5,0.00,1 +21,Exit long,2025-11-25 04:00,M1,6718.75,1,335725,212.5,0.06,0,862.5,0.26,0,0.00,1225,0.00,1 +21,Entry long,2025-11-25 03:00,M1,6714.5,1,335725,212.5,0.06,0,862.5,0.26,0,0.00,1225,0.00,1 +22,Exit short,2025-11-25 05:00,M1,6725,1,335937.5,-312.5,-0.09,0,150,0.04,-437.5,-0.13,912.5,0.00,1 +22,Entry short,2025-11-25 04:00,M1,6718.75,1,335937.5,-312.5,-0.09,0,150,0.04,-437.5,-0.13,912.5,0.00,1 +23,Exit long,2025-11-25 07:00,M1,6722.25,1,336250,-137.5,-0.04,0,162.5,0.05,-150,-0.04,775,0.00,1 +23,Entry long,2025-11-25 05:00,M1,6725,1,336250,-137.5,-0.04,0,162.5,0.05,-150,-0.04,775,0.00,1 +24,Exit short,2025-11-25 08:00,M1,6723.25,1,336112.5,-50,-0.01,0,325,0.10,-87.5,-0.03,725,0.00,1 +24,Entry short,2025-11-25 07:00,M1,6722.25,1,336112.5,-50,-0.01,0,325,0.10,-87.5,-0.03,725,0.00,1 +25,Exit long,2025-11-25 09:00,M1,6715.25,1,336162.5,-400,-0.12,0,37.5,0.01,-437.5,-0.13,325,0.00,1 +25,Entry long,2025-11-25 08:00,M1,6723.25,1,336162.5,-400,-0.12,0,37.5,0.01,-437.5,-0.13,325,0.00,1 +26,Exit short,2025-11-25 10:00,M1,6720.5,1,335762.5,-262.5,-0.08,0,12.5,0.00,-325,-0.10,62.5,0.00,1 +26,Entry short,2025-11-25 09:00,M1,6715.25,1,335762.5,-262.5,-0.08,0,12.5,0.00,-325,-0.10,62.5,0.00,1 +27,Exit long,2025-11-25 11:00,M1,6715,1,336025,-275,-0.08,0,75,0.02,-350,-0.10,-212.5,0.00,1 +27,Entry long,2025-11-25 10:00,M1,6720.5,1,336025,-275,-0.08,0,75,0.02,-350,-0.10,-212.5,0.00,1 +28,Exit short,2025-11-25 12:00,M1,6719.75,1,335750,-237.5,-0.07,0,12.5,0.00,-387.5,-0.12,-450,0.00,1 +28,Entry short,2025-11-25 11:00,M1,6715,1,335750,-237.5,-0.07,0,12.5,0.00,-387.5,-0.12,-450,0.00,1 +29,Exit long,2025-11-25 13:00,M1,6717,1,335987.5,-137.5,-0.04,0,200,0.06,-250,-0.07,-587.5,0.00,1 +29,Entry long,2025-11-25 12:00,M1,6719.75,1,335987.5,-137.5,-0.04,0,200,0.06,-250,-0.07,-587.5,0.00,1 +30,Exit short,2025-11-25 14:00,M1,6713.75,1,335850,162.5,0.05,0,237.5,0.07,-162.5,-0.05,-425,0.00,1 +30,Entry short,2025-11-25 13:00,M1,6717,1,335850,162.5,0.05,0,237.5,0.07,-162.5,-0.05,-425,0.00,1 +31,Exit long,2025-11-25 15:00,M1,6718,1,335687.5,212.5,0.06,0,387.5,0.12,-150,-0.04,-212.5,0.00,1 +31,Entry long,2025-11-25 14:00,M1,6713.75,1,335687.5,212.5,0.06,0,387.5,0.12,-150,-0.04,-212.5,0.00,1 +32,Exit short,2025-11-25 16:00,M1,6709.25,1,335900,437.5,0.13,0,562.5,0.17,-125,-0.04,225,0.00,1 +32,Entry short,2025-11-25 15:00,M1,6718,1,335900,437.5,0.13,0,562.5,0.17,-125,-0.04,225,0.00,1 +33,Exit long,2025-11-25 17:00,M1,6708,1,335462.5,-62.5,-0.02,0,325,0.10,-375,-0.11,162.5,0.00,1 +33,Entry long,2025-11-25 16:00,M1,6709.25,1,335462.5,-62.5,-0.02,0,325,0.10,-375,-0.11,162.5,0.00,1 +34,Exit short,2025-11-25 18:00,M1,6713,1,335400,-250,-0.07,0,250,0.07,-362.5,-0.11,-87.5,0.00,1 +34,Entry short,2025-11-25 17:00,M1,6708,1,335400,-250,-0.07,0,250,0.07,-362.5,-0.11,-87.5,0.00,1 +35,Exit long,2025-11-25 19:00,M1,6712,1,335650,-50,-0.01,0,87.5,0.03,-325,-0.10,-137.5,0.00,1 +35,Entry long,2025-11-25 18:00,M1,6713,1,335650,-50,-0.01,0,87.5,0.03,-325,-0.10,-137.5,0.00,1 +36,Exit short,2025-11-25 20:00,M1,6717.5,1,335600,-275,-0.08,0,50,0.01,-1025,-0.31,-412.5,0.00,1 +36,Entry short,2025-11-25 19:00,M1,6712,1,335600,-275,-0.08,0,50,0.01,-1025,-0.31,-412.5,0.00,1 +37,Exit long,2025-11-25 21:00,M1,6718,1,335875,25,0.01,0,650,0.19,-137.5,-0.04,-387.5,0.00,1 +37,Entry long,2025-11-25 20:00,M1,6717.5,1,335875,25,0.01,0,650,0.19,-137.5,-0.04,-387.5,0.00,1 +38,Exit short,2025-11-25 22:00,M1,6718.25,1,335900,-12.5,0.00,0,2175,0.65,-600,-0.18,-400,0.00,1 +38,Entry short,2025-11-25 21:00,M1,6718,1,335900,-12.5,0.00,0,2175,0.65,-600,-0.18,-400,0.00,1 +39,Exit long,2025-11-25 23:00,M1,6728.5,1,335912.5,512.5,0.15,0,1125,0.33,-1462.5,-0.44,112.5,0.00,1 +39,Entry long,2025-11-25 22:00,M1,6718.25,1,335912.5,512.5,0.15,0,1125,0.33,-1462.5,-0.44,112.5,0.00,1 +40,Exit short,2025-11-26 00:00,M1,6747.5,1,336425,-950,-0.28,0,500,0.15,-1062.5,-0.32,-837.5,0.00,1 +40,Entry short,2025-11-25 23:00,M1,6728.5,1,336425,-950,-0.28,0,500,0.15,-1062.5,-0.32,-837.5,0.00,1 +41,Exit long,2025-11-26 01:00,M1,6763.75,1,337375,812.5,0.24,0,1337.5,0.40,-12.5,0.00,-25,0.00,1 +41,Entry long,2025-11-26 00:00,M1,6747.5,1,337375,812.5,0.24,0,1337.5,0.40,-12.5,0.00,-25,0.00,1 +42,Exit short,2025-11-26 02:00,M1,6769,1,338187.5,-262.5,-0.08,0,50,0.01,-537.5,-0.16,-287.5,0.00,1 +42,Entry short,2025-11-26 01:00,M1,6763.75,1,338187.5,-262.5,-0.08,0,50,0.01,-537.5,-0.16,-287.5,0.00,1 +43,Exit long,2025-11-26 03:00,M1,6769.75,1,338450,37.5,0.01,0,125,0.04,-700,-0.21,-250,0.00,1 +43,Entry long,2025-11-26 02:00,M1,6769,1,338450,37.5,0.01,0,125,0.04,-700,-0.21,-250,0.00,1 +44,Exit short,2025-11-26 04:00,M1,6780.25,1,338487.5,-525,-0.16,0,12.5,0.00,-1137.5,-0.34,-775,0.00,1 +44,Entry short,2025-11-26 03:00,M1,6769.75,1,338487.5,-525,-0.16,0,12.5,0.00,-1137.5,-0.34,-775,0.00,1 +45,Exit long,2025-11-26 05:00,M1,6780.75,1,339012.5,25,0.01,0,200,0.06,-87.5,-0.03,-750,0.00,1 +45,Entry long,2025-11-26 04:00,M1,6780.25,1,339012.5,25,0.01,0,200,0.06,-87.5,-0.03,-750,0.00,1 +46,Exit short,2025-11-26 07:00,M1,6783.25,1,339037.5,-125,-0.04,0,125,0.04,-237.5,-0.07,-875,0.00,1 +46,Entry short,2025-11-26 05:00,M1,6780.75,1,339037.5,-125,-0.04,0,125,0.04,-237.5,-0.07,-875,0.00,1 +47,Exit long,2025-11-26 08:00,M1,6789.75,1,339162.5,325,0.10,0,375,0.11,-150,-0.04,-550,0.00,1 +47,Entry long,2025-11-26 07:00,M1,6783.25,1,339162.5,325,0.10,0,375,0.11,-150,-0.04,-550,0.00,1 +48,Exit short,2025-11-26 09:00,M1,6797.5,1,339487.5,-387.5,-0.11,0,100,0.03,-412.5,-0.12,-937.5,0.00,1 +48,Entry short,2025-11-26 08:00,M1,6789.75,1,339487.5,-387.5,-0.11,0,100,0.03,-412.5,-0.12,-937.5,0.00,1 +49,Exit long,2025-11-26 10:00,M1,6799,1,339875,75,0.02,0,100,0.03,-87.5,-0.03,-862.5,0.00,1 +49,Entry long,2025-11-26 09:00,M1,6797.5,1,339875,75,0.02,0,100,0.03,-87.5,-0.03,-862.5,0.00,1 +50,Exit short,2025-11-26 11:00,M1,6800,1,339950,-50,-0.01,0,75,0.02,-137.5,-0.04,-912.5,0.00,1 +50,Entry short,2025-11-26 10:00,M1,6799,1,339950,-50,-0.01,0,75,0.02,-137.5,-0.04,-912.5,0.00,1 +51,Exit long,2025-11-26 12:00,M1,6797.5,1,340000,-125,-0.04,0,50,0.01,-200,-0.06,-1037.5,0.00,1 +51,Entry long,2025-11-26 11:00,M1,6800,1,340000,-125,-0.04,0,50,0.01,-200,-0.06,-1037.5,0.00,1 +52,Exit short,2025-11-26 13:00,M1,6803,1,339875,-275,-0.08,0,12.5,0.00,-337.5,-0.10,-1312.5,0.00,1 +52,Entry short,2025-11-26 12:00,M1,6797.5,1,339875,-275,-0.08,0,12.5,0.00,-337.5,-0.10,-1312.5,0.00,1 +53,Exit long,2025-11-26 14:00,M1,6805,1,340150,100,0.03,0,212.5,0.06,-112.5,-0.03,-1212.5,0.00,1 +53,Entry long,2025-11-26 13:00,M1,6803,1,340150,100,0.03,0,212.5,0.06,-112.5,-0.03,-1212.5,0.00,1 +54,Exit short,2025-11-26 15:00,M1,6807.25,1,340250,-112.5,-0.03,0,12.5,0.00,-287.5,-0.08,-1325,0.00,1 +54,Entry short,2025-11-26 14:00,M1,6805,1,340250,-112.5,-0.03,0,12.5,0.00,-287.5,-0.08,-1325,0.00,1 +55,Exit long,2025-11-26 16:00,M1,6800.5,1,340362.5,-337.5,-0.10,0,87.5,0.03,-350,-0.10,-1662.5,0.00,1 +55,Entry long,2025-11-26 15:00,M1,6807.25,1,340362.5,-337.5,-0.10,0,87.5,0.03,-350,-0.10,-1662.5,0.00,1 +56,Exit short,2025-11-26 17:00,M1,6795.25,1,340025,262.5,0.08,0,362.5,0.11,-25,-0.01,-1400,0.00,1 +56,Entry short,2025-11-26 16:00,M1,6800.5,1,340025,262.5,0.08,0,362.5,0.11,-25,-0.01,-1400,0.00,1 +57,Exit long,2025-11-26 18:00,M1,6803.25,1,339762.5,400,0.12,0,450,0.13,0,0.00,-1000,0.00,1 +57,Entry long,2025-11-26 17:00,M1,6795.25,1,339762.5,400,0.12,0,450,0.13,0,0.00,-1000,0.00,1 +58,Exit short,2025-11-26 19:00,M1,6798,1,340162.5,262.5,0.08,0,325,0.10,-87.5,-0.03,-737.5,0.00,1 +58,Entry short,2025-11-26 18:00,M1,6803.25,1,340162.5,262.5,0.08,0,325,0.10,-87.5,-0.03,-737.5,0.00,1 +59,Exit long,2025-11-26 20:00,M1,6796.5,1,339900,-75,-0.02,0,62.5,0.02,-550,-0.16,-812.5,0.00,1 +59,Entry long,2025-11-26 19:00,M1,6798,1,339900,-75,-0.02,0,62.5,0.02,-550,-0.16,-812.5,0.00,1 +60,Exit short,2025-11-26 21:00,M1,6803.75,1,339825,-362.5,-0.11,0,87.5,0.03,-400,-0.12,-1175,0.00,1 +60,Entry short,2025-11-26 20:00,M1,6796.5,1,339825,-362.5,-0.11,0,87.5,0.03,-400,-0.12,-1175,0.00,1 +61,Exit long,2025-11-26 22:00,M1,6802,1,340187.5,-87.5,-0.03,0,1012.5,0.30,-312.5,-0.09,-1262.5,0.00,1 +61,Entry long,2025-11-26 21:00,M1,6803.75,1,340187.5,-87.5,-0.03,0,1012.5,0.30,-312.5,-0.09,-1262.5,0.00,1 +62,Exit short,2025-11-26 23:00,M1,6824.5,1,340100,-1125,-0.33,0,312.5,0.09,-1150,-0.34,-2387.5,0.00,1 +62,Entry short,2025-11-26 22:00,M1,6802,1,340100,-1125,-0.33,0,312.5,0.09,-1150,-0.34,-2387.5,0.00,1 +63,Exit long,2025-11-27 00:00,M1,6843,1,341225,925,0.27,0,1100,0.32,-37.5,-0.01,-1462.5,0.00,1 +63,Entry long,2025-11-26 23:00,M1,6824.5,1,341225,925,0.27,0,1100,0.32,-37.5,-0.01,-1462.5,0.00,1 +64,Exit short,2025-11-27 01:00,M1,6840.5,1,342150,125,0.04,0,475,0.14,-187.5,-0.05,-1337.5,0.00,1 +64,Entry short,2025-11-27 00:00,M1,6843,1,342150,125,0.04,0,475,0.14,-187.5,-0.05,-1337.5,0.00,1 +65,Exit long,2025-11-27 02:00,M1,6833,1,342025,-375,-0.11,0,225,0.07,-775,-0.23,-1712.5,0.00,1 +65,Entry long,2025-11-27 01:00,M1,6840.5,1,342025,-375,-0.11,0,225,0.07,-775,-0.23,-1712.5,0.00,1 +66,Exit short,2025-11-27 03:00,M1,6838,1,341650,-250,-0.07,0,125,0.04,-512.5,-0.15,-1962.5,0.00,1 +66,Entry short,2025-11-27 02:00,M1,6833,1,341650,-250,-0.07,0,125,0.04,-512.5,-0.15,-1962.5,0.00,1 +67,Exit long,2025-11-27 04:00,M1,6825.25,1,341900,-637.5,-0.19,0,362.5,0.11,-637.5,-0.19,-2600,0.00,1 +67,Entry long,2025-11-27 03:00,M1,6838,1,341900,-637.5,-0.19,0,362.5,0.11,-637.5,-0.19,-2600,0.00,1 +68,Exit short,2025-11-27 05:00,M1,6830.25,1,341262.5,-250,-0.07,0,12.5,0.00,-400,-0.12,-2850,0.00,1 +68,Entry short,2025-11-27 04:00,M1,6825.25,1,341262.5,-250,-0.07,0,12.5,0.00,-400,-0.12,-2850,0.00,1 +69,Exit long,2025-11-27 07:00,M1,6829.25,1,341512.5,-50,-0.01,0,162.5,0.05,-137.5,-0.04,-2900,0.00,1 +69,Entry long,2025-11-27 05:00,M1,6830.25,1,341512.5,-50,-0.01,0,162.5,0.05,-137.5,-0.04,-2900,0.00,1 +70,Exit short,2025-11-27 08:00,M1,6832.25,1,341462.5,-150,-0.04,0,50,0.01,-162.5,-0.05,-3050,0.00,1 +70,Entry short,2025-11-27 07:00,M1,6829.25,1,341462.5,-150,-0.04,0,50,0.01,-162.5,-0.05,-3050,0.00,1 +71,Exit long,2025-11-27 09:00,M1,6832.75,1,341612.5,25,0.01,0,75,0.02,-225,-0.07,-3025,0.00,1 +71,Entry long,2025-11-27 08:00,M1,6832.25,1,341612.5,25,0.01,0,75,0.02,-225,-0.07,-3025,0.00,1 +72,Exit short,2025-11-27 10:00,M1,6829,1,341637.5,187.5,0.05,0,212.5,0.06,-50,-0.01,-2837.5,0.00,1 +72,Entry short,2025-11-27 09:00,M1,6832.75,1,341637.5,187.5,0.05,0,212.5,0.06,-50,-0.01,-2837.5,0.00,1 +73,Exit long,2025-11-27 11:00,M1,6828.25,1,341450,-37.5,-0.01,0,37.5,0.01,-125,-0.04,-2875,0.00,1 +73,Entry long,2025-11-27 10:00,M1,6829,1,341450,-37.5,-0.01,0,37.5,0.01,-125,-0.04,-2875,0.00,1 +74,Exit short,2025-11-27 12:00,M1,6830,1,341412.5,-87.5,-0.03,0,50,0.01,-125,-0.04,-2962.5,0.00,1 +74,Entry short,2025-11-27 11:00,M1,6828.25,1,341412.5,-87.5,-0.03,0,50,0.01,-125,-0.04,-2962.5,0.00,1 +75,Exit long,2025-11-27 13:00,M1,6830.75,1,341500,37.5,0.01,0,162.5,0.05,0,0.00,-2925,0.00,1 +75,Entry long,2025-11-27 12:00,M1,6830,1,341500,37.5,0.01,0,162.5,0.05,0,0.00,-2925,0.00,1 +76,Exit short,2025-11-27 14:00,M1,6825.5,1,341537.5,262.5,0.08,0,287.5,0.08,-25,-0.01,-2662.5,0.00,1 +76,Entry short,2025-11-27 13:00,M1,6830.75,1,341537.5,262.5,0.08,0,287.5,0.08,-25,-0.01,-2662.5,0.00,1 +77,Exit long,2025-11-27 15:00,M1,6829.75,1,341275,212.5,0.06,0,237.5,0.07,-25,-0.01,-2450,0.00,1 +77,Entry long,2025-11-27 14:00,M1,6825.5,1,341275,212.5,0.06,0,237.5,0.07,-25,-0.01,-2450,0.00,1 +78,Exit short,2025-11-27 16:00,M1,6826.75,1,341487.5,150,0.04,0,275,0.08,-150,-0.04,-2300,0.00,1 +78,Entry short,2025-11-27 15:00,M1,6829.75,1,341487.5,150,0.04,0,275,0.08,-150,-0.04,-2300,0.00,1 +79,Exit long,2025-11-27 17:00,M1,6825.25,1,341337.5,-75,-0.02,0,237.5,0.07,-87.5,-0.03,-2375,0.00,1 +79,Entry long,2025-11-27 16:00,M1,6826.75,1,341337.5,-75,-0.02,0,237.5,0.07,-87.5,-0.03,-2375,0.00,1 +80,Exit short,2025-11-27 18:00,M1,6828.5,1,341262.5,-162.5,-0.05,0,50,0.01,-175,-0.05,-2537.5,0.00,1 +80,Entry short,2025-11-27 17:00,M1,6825.25,1,341262.5,-162.5,-0.05,0,50,0.01,-175,-0.05,-2537.5,0.00,1 +81,Exit long,2025-11-27 19:00,M1,6827.5,1,341425,-50,-0.01,0,50,0.01,-162.5,-0.05,-2587.5,0.00,1 +81,Entry long,2025-11-27 18:00,M1,6828.5,1,341425,-50,-0.01,0,50,0.01,-162.5,-0.05,-2587.5,0.00,1 +82,Exit short,2025-11-27 20:00,M1,6827,1,341375,25,0.01,0,50,0.01,-137.5,-0.04,-2562.5,0.00,1 +82,Entry short,2025-11-27 19:00,M1,6827.5,1,341375,25,0.01,0,50,0.01,-137.5,-0.04,-2562.5,0.00,1 +83,Exit long,2025-11-27 21:00,M1,6828,1,341350,50,0.01,0,150,0.04,-112.5,-0.03,-2512.5,0.00,1 +83,Entry long,2025-11-27 20:00,M1,6827,1,341350,50,0.01,0,150,0.04,-112.5,-0.03,-2512.5,0.00,1 +84,Exit short,2025-11-27 22:00,M1,6827.5,1,341400,25,0.01,0,112.5,0.03,-187.5,-0.05,-2487.5,0.00,1 +84,Entry short,2025-11-27 21:00,M1,6828,1,341400,25,0.01,0,112.5,0.03,-187.5,-0.05,-2487.5,0.00,1 +85,Exit long,2025-11-27 23:00,M1,6827,1,341375,-25,-0.01,0,75,0.02,-125,-0.04,-2512.5,0.00,1 +85,Entry long,2025-11-27 22:00,M1,6827.5,1,341375,-25,-0.01,0,75,0.02,-125,-0.04,-2512.5,0.00,1 +86,Exit short,2025-11-28 00:00,M1,6827,1,341350,0,0.00,0,100,0.03,-87.5,-0.03,-2512.5,0.00,1 +86,Entry short,2025-11-27 23:00,M1,6827,1,341350,0,0.00,0,100,0.03,-87.5,-0.03,-2512.5,0.00,1 +87,Exit long,2025-11-28 01:00,M1,6828,1,341350,50,0.01,0,75,0.02,-75,-0.02,-2462.5,0.00,1 +87,Entry long,2025-11-28 00:00,M1,6827,1,341350,50,0.01,0,75,0.02,-75,-0.02,-2462.5,0.00,1 +88,Exit short,2025-11-28 07:00,M1,6836.5,1,341400,-425,-0.12,0,0,0.00,-437.5,-0.13,-2887.5,0.00,1 +88,Entry short,2025-11-28 01:00,M1,6828,1,341400,-425,-0.12,0,0,0.00,-437.5,-0.13,-2887.5,0.00,1 +89,Exit long,2025-11-28 08:00,M1,6836.25,1,341825,-12.5,0.00,0,137.5,0.04,-75,-0.02,-2900,0.00,1 +89,Entry long,2025-11-28 07:00,M1,6836.5,1,341825,-12.5,0.00,0,137.5,0.04,-75,-0.02,-2900,0.00,1 +90,Exit short,2025-11-28 09:00,M1,6835.25,1,341812.5,50,0.01,0,87.5,0.03,-250,-0.07,-2850,0.00,1 +90,Entry short,2025-11-28 08:00,M1,6836.25,1,341812.5,50,0.01,0,87.5,0.03,-250,-0.07,-2850,0.00,1 +91,Exit long,2025-11-28 10:00,M1,6835,1,341762.5,-12.5,0.00,0,100,0.03,-150,-0.04,-2862.5,0.00,1 +91,Entry long,2025-11-28 09:00,M1,6835.25,1,341762.5,-12.5,0.00,0,100,0.03,-150,-0.04,-2862.5,0.00,1 +92,Exit short,2025-11-28 21:00,M1,6837.25,1,341750,-112.5,-0.03,0,0,0.00,-475,-0.14,-2975,0.00,1 +92,Entry short,2025-11-28 10:00,M1,6835,1,341750,-112.5,-0.03,0,0,0.00,-475,-0.14,-2975,0.00,1 +93,Exit long,2025-11-28 22:00,M1,6838.5,1,341862.5,62.5,0.02,0,350,0.10,-300,-0.09,-2912.5,0.00,1 +93,Entry long,2025-11-28 21:00,M1,6837.25,1,341862.5,62.5,0.02,0,350,0.10,-300,-0.09,-2912.5,0.00,1 +94,Exit short,2025-11-28 23:00,M1,6851,1,341925,-625,-0.18,0,100,0.03,-700,-0.20,-3537.5,0.00,1 +94,Entry short,2025-11-28 22:00,M1,6838.5,1,341925,-625,-0.18,0,100,0.03,-700,-0.20,-3537.5,0.00,1 +95,Exit long,2025-11-29 00:00,M1,6854.25,1,342550,162.5,0.05,0,250,0.07,-425,-0.12,-3375,0.00,1 +95,Entry long,2025-11-28 23:00,M1,6851,1,342550,162.5,0.05,0,250,0.07,-425,-0.12,-3375,0.00,1 +96,Exit short,2025-11-29 01:00,M1,6860.5,1,342712.5,-312.5,-0.09,0,450,0.13,-475,-0.14,-3687.5,0.00,1 +96,Entry short,2025-11-29 00:00,M1,6854.25,1,342712.5,-312.5,-0.09,0,450,0.13,-475,-0.14,-3687.5,0.00,1 +97,Exit long,2025-11-29 02:00,M1,6857.25,1,343025,-162.5,-0.05,0,150,0.04,-262.5,-0.08,-3850,0.00,1 +97,Entry long,2025-11-29 01:00,M1,6860.5,1,343025,-162.5,-0.05,0,150,0.04,-262.5,-0.08,-3850,0.00,1 +98,Exit short,2025-12-01 07:00,M1,6853,1,342862.5,212.5,0.06,0,437.5,0.13,-362.5,-0.11,-3637.5,0.00,1 +98,Entry short,2025-11-29 02:00,M1,6857.25,1,342862.5,212.5,0.06,0,437.5,0.13,-362.5,-0.11,-3637.5,0.00,1 +99,Exit long,2025-12-01 08:00,M1,6828.75,1,342650,-1212.5,-0.35,0,75,0.02,-1337.5,-0.39,-4850,0.00,1 +99,Entry long,2025-12-01 07:00,M1,6853,1,342650,-1212.5,-0.35,0,75,0.02,-1337.5,-0.39,-4850,0.00,1 +100,Exit short,2025-12-01 09:00,M1,6825.25,1,341437.5,175,0.05,0,575,0.17,-112.5,-0.03,-4675,0.00,1 +100,Entry short,2025-12-01 08:00,M1,6828.75,1,341437.5,175,0.05,0,575,0.17,-112.5,-0.03,-4675,0.00,1 +101,Exit long,2025-12-01 10:00,M1,6819.75,1,341262.5,-275,-0.08,0,87.5,0.03,-400,-0.12,-4950,0.00,1 +101,Entry long,2025-12-01 09:00,M1,6825.25,1,341262.5,-275,-0.08,0,87.5,0.03,-400,-0.12,-4950,0.00,1 +102,Exit short,2025-12-01 11:00,M1,6814.75,1,340987.5,250,0.07,0,412.5,0.12,0,0.00,-4700,0.00,1 +102,Entry short,2025-12-01 10:00,M1,6819.75,1,340987.5,250,0.07,0,412.5,0.12,0,0.00,-4700,0.00,1 +103,Exit long,2025-12-01 12:00,M1,6807.75,1,340737.5,-350,-0.10,0,50,0.01,-350,-0.10,-5050,0.00,1 +103,Entry long,2025-12-01 11:00,M1,6814.75,1,340737.5,-350,-0.10,0,50,0.01,-350,-0.10,-5050,0.00,1 +104,Exit short,2025-12-01 13:00,M1,6811.75,1,340387.5,-200,-0.06,0,287.5,0.08,-237.5,-0.07,-5250,0.00,1 +104,Entry short,2025-12-01 12:00,M1,6807.75,1,340387.5,-200,-0.06,0,287.5,0.08,-237.5,-0.07,-5250,0.00,1 +105,Exit long,2025-12-01 14:00,M1,6815.25,1,340587.5,175,0.05,0,187.5,0.06,-175,-0.05,-5075,0.00,1 +105,Entry long,2025-12-01 13:00,M1,6811.75,1,340587.5,175,0.05,0,187.5,0.06,-175,-0.05,-5075,0.00,1 +106,Exit short,2025-12-01 15:00,M1,6816,1,340762.5,-37.5,-0.01,0,325,0.10,-112.5,-0.03,-5112.5,0.00,1 +106,Entry short,2025-12-01 14:00,M1,6815.25,1,340762.5,-37.5,-0.01,0,325,0.10,-112.5,-0.03,-5112.5,0.00,1 +107,Exit long,2025-12-01 16:00,M1,6823,1,340800,350,0.10,0,575,0.17,-187.5,-0.06,-4762.5,0.00,1 +107,Entry long,2025-12-01 15:00,M1,6816,1,340800,350,0.10,0,575,0.17,-187.5,-0.06,-4762.5,0.00,1 +108,Exit short,2025-12-01 17:00,M1,6816.25,1,341150,337.5,0.10,0,400,0.12,-337.5,-0.10,-4425,0.00,1 +108,Entry short,2025-12-01 16:00,M1,6823,1,341150,337.5,0.10,0,400,0.12,-337.5,-0.10,-4425,0.00,1 +109,Exit long,2025-12-01 18:00,M1,6820.5,1,340812.5,212.5,0.06,0,475,0.14,0,0.00,-4212.5,0.00,1 +109,Entry long,2025-12-01 17:00,M1,6816.25,1,340812.5,212.5,0.06,0,475,0.14,0,0.00,-4212.5,0.00,1 +110,Exit short,2025-12-01 19:00,M1,6819.5,1,341025,50,0.01,0,237.5,0.07,-62.5,-0.02,-4162.5,0.00,1 +110,Entry short,2025-12-01 18:00,M1,6820.5,1,341025,50,0.01,0,237.5,0.07,-62.5,-0.02,-4162.5,0.00,1 +111,Exit long,2025-12-01 20:00,M1,6808.75,1,340975,-537.5,-0.16,0,87.5,0.03,-725,-0.21,-4700,0.00,1 +111,Entry long,2025-12-01 19:00,M1,6819.5,1,340975,-537.5,-0.16,0,87.5,0.03,-725,-0.21,-4700,0.00,1 +112,Exit short,2025-12-01 21:00,M1,6817.25,1,340437.5,-425,-0.12,0,87.5,0.03,-475,-0.14,-5125,0.00,1 +112,Entry short,2025-12-01 20:00,M1,6808.75,1,340437.5,-425,-0.12,0,87.5,0.03,-475,-0.14,-5125,0.00,1 +113,Exit long,2025-12-01 22:00,M1,6839.75,1,340862.5,1125,0.33,0,1350,0.40,-275,-0.08,-4000,0.00,1 +113,Entry long,2025-12-01 21:00,M1,6817.25,1,340862.5,1125,0.33,0,1350,0.40,-275,-0.08,-4000,0.00,1 +114,Exit short,2025-12-01 23:00,M1,6842.25,1,341987.5,-125,-0.04,0,1300,0.38,-212.5,-0.06,-4125,0.00,1 +114,Entry short,2025-12-01 22:00,M1,6839.75,1,341987.5,-125,-0.04,0,1300,0.38,-212.5,-0.06,-4125,0.00,1 +115,Exit long,2025-12-02 00:00,M1,6845,1,342112.5,137.5,0.04,0,387.5,0.11,-775,-0.23,-3987.5,0.00,1 +115,Entry long,2025-12-01 23:00,M1,6842.25,1,342112.5,137.5,0.04,0,387.5,0.11,-775,-0.23,-3987.5,0.00,1 +116,Exit short,2025-12-02 01:00,M1,6846,1,342250,-50,-0.01,0,325,0.09,-425,-0.12,-4037.5,0.00,1 +116,Entry short,2025-12-02 00:00,M1,6845,1,342250,-50,-0.01,0,325,0.09,-425,-0.12,-4037.5,0.00,1 +117,Exit long,2025-12-02 02:00,M1,6853.75,1,342300,387.5,0.11,0,487.5,0.14,-37.5,-0.01,-3650,0.00,1 +117,Entry long,2025-12-02 01:00,M1,6846,1,342300,387.5,0.11,0,487.5,0.14,-37.5,-0.01,-3650,0.00,1 +118,Exit short,2025-12-02 03:00,M1,6827.75,1,342687.5,1300,0.38,0,1500,0.44,-25,-0.01,-2350,0.00,1 +118,Entry short,2025-12-02 02:00,M1,6853.75,1,342687.5,1300,0.38,0,1500,0.44,-25,-0.01,-2350,0.00,1 +119,Exit long,2025-12-02 04:00,M1,6829,1,341387.5,62.5,0.02,0,425,0.12,-425,-0.12,-2287.5,0.00,1 +119,Entry long,2025-12-02 03:00,M1,6827.75,1,341387.5,62.5,0.02,0,425,0.12,-425,-0.12,-2287.5,0.00,1 +120,Exit short,2025-12-02 05:00,M1,6826.75,1,341450,112.5,0.03,0,212.5,0.06,-50,-0.01,-2175,0.00,1 +120,Entry short,2025-12-02 04:00,M1,6829,1,341450,112.5,0.03,0,212.5,0.06,-50,-0.01,-2175,0.00,1 +121,Exit long,2025-12-02 07:00,M1,6825,1,341337.5,-87.5,-0.03,0,225,0.07,-237.5,-0.07,-2262.5,0.00,1 +121,Entry long,2025-12-02 05:00,M1,6826.75,1,341337.5,-87.5,-0.03,0,225,0.07,-237.5,-0.07,-2262.5,0.00,1 +122,Exit short,2025-12-02 08:00,M1,6826.5,1,341250,-75,-0.02,0,150,0.04,-200,-0.06,-2337.5,0.00,1 +122,Entry short,2025-12-02 07:00,M1,6825,1,341250,-75,-0.02,0,150,0.04,-200,-0.06,-2337.5,0.00,1 +123,Exit long,2025-12-02 09:00,M1,6827.25,1,341325,37.5,0.01,0,337.5,0.10,-75,-0.02,-2300,0.00,1 +123,Entry long,2025-12-02 08:00,M1,6826.5,1,341325,37.5,0.01,0,337.5,0.10,-75,-0.02,-2300,0.00,1 +124,Exit short,2025-12-02 10:00,M1,6829.25,1,341362.5,-100,-0.03,0,212.5,0.06,-187.5,-0.05,-2400,0.00,1 +124,Entry short,2025-12-02 09:00,M1,6827.25,1,341362.5,-100,-0.03,0,212.5,0.06,-187.5,-0.05,-2400,0.00,1 +125,Exit long,2025-12-02 11:00,M1,6824.25,1,341462.5,-250,-0.07,0,12.5,0.00,-700,-0.21,-2650,0.00,1 +125,Entry long,2025-12-02 10:00,M1,6829.25,1,341462.5,-250,-0.07,0,12.5,0.00,-700,-0.21,-2650,0.00,1 +126,Exit short,2025-12-02 12:00,M1,6820.75,1,341212.5,175,0.05,0,225,0.07,-75,-0.02,-2475,0.00,1 +126,Entry short,2025-12-02 11:00,M1,6824.25,1,341212.5,175,0.05,0,225,0.07,-75,-0.02,-2475,0.00,1 +127,Exit long,2025-12-02 13:00,M1,6820.75,1,341037.5,0,0.00,0,112.5,0.03,-100,-0.03,-2475,0.00,1 +127,Entry long,2025-12-02 12:00,M1,6820.75,1,341037.5,0,0.00,0,112.5,0.03,-100,-0.03,-2475,0.00,1 +128,Exit short,2025-12-02 14:00,M1,6821,1,341037.5,-12.5,0.00,0,325,0.10,-162.5,-0.05,-2487.5,0.00,1 +128,Entry short,2025-12-02 13:00,M1,6820.75,1,341037.5,-12.5,0.00,0,325,0.10,-162.5,-0.05,-2487.5,0.00,1 +129,Exit long,2025-12-02 15:00,M1,6819.25,1,341050,-87.5,-0.03,0,200,0.06,-175,-0.05,-2575,0.00,1 +129,Entry long,2025-12-02 14:00,M1,6821,1,341050,-87.5,-0.03,0,200,0.06,-175,-0.05,-2575,0.00,1 +130,Exit short,2025-12-02 16:00,M1,6829.5,1,340962.5,-512.5,-0.15,0,350,0.10,-575,-0.17,-3087.5,0.00,1 +130,Entry short,2025-12-02 15:00,M1,6819.25,1,340962.5,-512.5,-0.15,0,350,0.10,-575,-0.17,-3087.5,0.00,1 +131,Exit long,2025-12-02 17:00,M1,6834.5,1,341475,250,0.07,0,375,0.11,-12.5,0.00,-2837.5,0.00,1 +131,Entry long,2025-12-02 16:00,M1,6829.5,1,341475,250,0.07,0,375,0.11,-12.5,0.00,-2837.5,0.00,1 +132,Exit short,2025-12-02 18:00,M1,6845,1,341725,-525,-0.15,0,62.5,0.02,-612.5,-0.18,-3362.5,0.00,1 +132,Entry short,2025-12-02 17:00,M1,6834.5,1,341725,-525,-0.15,0,62.5,0.02,-612.5,-0.18,-3362.5,0.00,1 +133,Exit long,2025-12-02 19:00,M1,6843.5,1,342250,-75,-0.02,0,200,0.06,-125,-0.04,-3437.5,0.00,1 +133,Entry long,2025-12-02 18:00,M1,6845,1,342250,-75,-0.02,0,200,0.06,-125,-0.04,-3437.5,0.00,1 +134,Exit short,2025-12-02 20:00,M1,6845.5,1,342175,-100,-0.03,0,325,0.09,-112.5,-0.03,-3537.5,0.00,1 +134,Entry short,2025-12-02 19:00,M1,6843.5,1,342175,-100,-0.03,0,325,0.09,-112.5,-0.03,-3537.5,0.00,1 +135,Exit long,2025-12-02 21:00,M1,6848.5,1,342275,150,0.04,0,150,0.04,-162.5,-0.05,-3387.5,0.00,1 +135,Entry long,2025-12-02 20:00,M1,6845.5,1,342275,150,0.04,0,150,0.04,-162.5,-0.05,-3387.5,0.00,1 +136,Exit short,2025-12-02 22:00,M1,6848.25,1,342425,12.5,0.00,0,437.5,0.13,-312.5,-0.09,-3375,0.00,1 +136,Entry short,2025-12-02 21:00,M1,6848.5,1,342425,12.5,0.00,0,437.5,0.13,-312.5,-0.09,-3375,0.00,1 +137,Exit long,2025-12-02 23:00,M1,6860.25,1,342412.5,600,0.18,0,687.5,0.20,-750,-0.22,-2775,0.00,1 +137,Entry long,2025-12-02 22:00,M1,6848.25,1,342412.5,600,0.18,0,687.5,0.20,-750,-0.22,-2775,0.00,1 +138,Exit short,2025-12-03 00:00,M1,6830.5,1,343012.5,1487.5,0.43,0,2112.5,0.62,-162.5,-0.05,-1287.5,0.00,1 +138,Entry short,2025-12-02 23:00,M1,6860.25,1,343012.5,1487.5,0.43,0,2112.5,0.62,-162.5,-0.05,-1287.5,0.00,1 +139,Exit long,2025-12-03 01:00,M1,6826.25,1,341525,-212.5,-0.06,0,1137.5,0.33,-225,-0.07,-1500,0.00,1 +139,Entry long,2025-12-03 00:00,M1,6830.5,1,341525,-212.5,-0.06,0,1137.5,0.33,-225,-0.07,-1500,0.00,1 +140,Exit short,2025-12-03 02:00,M1,6835.75,1,341312.5,-475,-0.14,0,100,0.03,-725,-0.21,-1975,0.00,1 +140,Entry short,2025-12-03 01:00,M1,6826.25,1,341312.5,-475,-0.14,0,100,0.03,-725,-0.21,-1975,0.00,1 +141,Exit long,2025-12-03 03:00,M1,6848,1,341787.5,612.5,0.18,0,750,0.22,-175,-0.05,-1362.5,0.00,1 +141,Entry long,2025-12-03 02:00,M1,6835.75,1,341787.5,612.5,0.18,0,750,0.22,-175,-0.05,-1362.5,0.00,1 +142,Exit short,2025-12-03 04:00,M1,6839.25,1,342400,437.5,0.13,0,500,0.15,-275,-0.08,-925,0.00,1 +142,Entry short,2025-12-03 03:00,M1,6848,1,342400,437.5,0.13,0,500,0.15,-275,-0.08,-925,0.00,1 +143,Exit long,2025-12-03 05:00,M1,6842.75,1,341962.5,175,0.05,0,187.5,0.05,-62.5,-0.02,-750,0.00,1 +143,Entry long,2025-12-03 04:00,M1,6839.25,1,341962.5,175,0.05,0,187.5,0.05,-62.5,-0.02,-750,0.00,1 +144,Exit short,2025-12-03 07:00,M1,6845.75,1,342137.5,-150,-0.04,0,62.5,0.02,-250,-0.07,-900,0.00,1 +144,Entry short,2025-12-03 05:00,M1,6842.75,1,342137.5,-150,-0.04,0,62.5,0.02,-250,-0.07,-900,0.00,1 +145,Exit long,2025-12-03 08:00,M1,6848.5,1,342287.5,137.5,0.04,0,250,0.07,-425,-0.12,-762.5,0.00,1 +145,Entry long,2025-12-03 07:00,M1,6845.75,1,342287.5,137.5,0.04,0,250,0.07,-425,-0.12,-762.5,0.00,1 +146,Exit short,2025-12-03 09:00,M1,6849.75,1,342425,-62.5,-0.02,0,50,0.01,-162.5,-0.05,-825,0.00,1 +146,Entry short,2025-12-03 08:00,M1,6848.5,1,342425,-62.5,-0.02,0,50,0.01,-162.5,-0.05,-825,0.00,1 +147,Exit long,2025-12-03 10:00,M1,6852,1,342487.5,112.5,0.03,0,300,0.09,-50,-0.01,-712.5,0.00,1 +147,Entry long,2025-12-03 09:00,M1,6849.75,1,342487.5,112.5,0.03,0,300,0.09,-50,-0.01,-712.5,0.00,1 +148,Exit short,2025-12-03 11:00,M1,6855.75,1,342600,-187.5,-0.05,0,112.5,0.03,-250,-0.07,-900,0.00,1 +148,Entry short,2025-12-03 10:00,M1,6852,1,342600,-187.5,-0.05,0,112.5,0.03,-250,-0.07,-900,0.00,1 +149,Exit long,2025-12-03 12:00,M1,6854.5,1,342787.5,-62.5,-0.02,0,50,0.01,-87.5,-0.03,-962.5,0.00,1 +149,Entry long,2025-12-03 11:00,M1,6855.75,1,342787.5,-62.5,-0.02,0,50,0.01,-87.5,-0.03,-962.5,0.00,1 +150,Exit short,2025-12-03 13:00,M1,6853.5,1,342725,50,0.01,0,87.5,0.03,-162.5,-0.05,-912.5,0.00,1 +150,Entry short,2025-12-03 12:00,M1,6854.5,1,342725,50,0.01,0,87.5,0.03,-162.5,-0.05,-912.5,0.00,1 +151,Exit long,2025-12-03 14:00,M1,6853.25,1,342675,-12.5,0.00,0,137.5,0.04,-212.5,-0.06,-925,0.00,1 +151,Entry long,2025-12-03 13:00,M1,6853.5,1,342675,-12.5,0.00,0,137.5,0.04,-212.5,-0.06,-925,0.00,1 +152,Exit short,2025-12-03 15:00,M1,6852.5,1,342662.5,37.5,0.01,0,150,0.04,-187.5,-0.05,-887.5,0.00,1 +152,Entry short,2025-12-03 14:00,M1,6853.25,1,342662.5,37.5,0.01,0,150,0.04,-187.5,-0.05,-887.5,0.00,1 +153,Exit long,2025-12-03 16:00,M1,6855.25,1,342625,137.5,0.04,0,175,0.05,-337.5,-0.10,-750,0.00,1 +153,Entry long,2025-12-03 15:00,M1,6852.5,1,342625,137.5,0.04,0,175,0.05,-337.5,-0.10,-750,0.00,1 +154,Exit short,2025-12-03 17:00,M1,6852,1,342762.5,162.5,0.05,0,412.5,0.12,-12.5,0.00,-587.5,0.00,1 +154,Entry short,2025-12-03 16:00,M1,6855.25,1,342762.5,162.5,0.05,0,412.5,0.12,-12.5,0.00,-587.5,0.00,1 +155,Exit long,2025-12-03 18:00,M1,6849.75,1,342600,-112.5,-0.03,0,125,0.04,-275,-0.08,-700,0.00,1 +155,Entry long,2025-12-03 17:00,M1,6852,1,342600,-112.5,-0.03,0,125,0.04,-275,-0.08,-700,0.00,1 +156,Exit short,2025-12-03 19:00,M1,6850.5,1,342487.5,-37.5,-0.01,0,137.5,0.04,-200,-0.06,-737.5,0.00,1 +156,Entry short,2025-12-03 18:00,M1,6849.75,1,342487.5,-37.5,-0.01,0,137.5,0.04,-200,-0.06,-737.5,0.00,1 +157,Exit long,2025-12-03 20:00,M1,6858,1,342525,375,0.11,0,500,0.15,-200,-0.06,-362.5,0.00,1 +157,Entry long,2025-12-03 19:00,M1,6850.5,1,342525,375,0.11,0,500,0.15,-200,-0.06,-362.5,0.00,1 +158,Exit short,2025-12-03 21:00,M1,6855.5,1,342900,125,0.04,0,200,0.06,-212.5,-0.06,-237.5,0.00,1 +158,Entry short,2025-12-03 20:00,M1,6858,1,342900,125,0.04,0,200,0.06,-212.5,-0.06,-237.5,0.00,1 +159,Exit long,2025-12-03 22:00,M1,6832.75,1,342775,-1137.5,-0.33,0,50,0.01,-1900,-0.55,-1375,0.00,1 +159,Entry long,2025-12-03 21:00,M1,6855.5,1,342775,-1137.5,-0.33,0,50,0.01,-1900,-0.55,-1375,0.00,1 +160,Exit short,2025-12-03 23:00,M1,6847,1,341637.5,-712.5,-0.21,0,87.5,0.03,-1037.5,-0.30,-2087.5,0.00,1 +160,Entry short,2025-12-03 22:00,M1,6832.75,1,341637.5,-712.5,-0.21,0,87.5,0.03,-1037.5,-0.30,-2087.5,0.00,1 +161,Exit long,2025-12-04 00:00,M1,6845.75,1,342350,-62.5,-0.02,0,625,0.18,-350,-0.10,-2150,0.00,1 +161,Entry long,2025-12-03 23:00,M1,6847,1,342350,-62.5,-0.02,0,625,0.18,-350,-0.10,-2150,0.00,1 +162,Exit short,2025-12-04 01:00,M1,6863,1,342287.5,-862.5,-0.25,0,50,0.01,-962.5,-0.28,-3012.5,0.00,1 +162,Entry short,2025-12-04 00:00,M1,6845.75,1,342287.5,-862.5,-0.25,0,50,0.01,-962.5,-0.28,-3012.5,0.00,1 +163,Exit long,2025-12-04 02:00,M1,6863,1,343150,0,0.00,0,312.5,0.09,-212.5,-0.06,-3012.5,0.00,1 +163,Entry long,2025-12-04 01:00,M1,6863,1,343150,0,0.00,0,312.5,0.09,-212.5,-0.06,-3012.5,0.00,1 +164,Exit short,2025-12-04 03:00,M1,6870.25,1,343150,-362.5,-0.11,0,37.5,0.01,-437.5,-0.13,-3375,0.00,1 +164,Entry short,2025-12-04 02:00,M1,6863,1,343150,-362.5,-0.11,0,37.5,0.01,-437.5,-0.13,-3375,0.00,1 +165,Exit long,2025-12-04 04:00,M1,6862.5,1,343512.5,-387.5,-0.11,0,150,0.04,-550,-0.16,-3762.5,0.00,1 +165,Entry long,2025-12-04 03:00,M1,6870.25,1,343512.5,-387.5,-0.11,0,150,0.04,-550,-0.16,-3762.5,0.00,1 +166,Exit short,2025-12-04 05:00,M1,6864.75,1,343125,-112.5,-0.03,0,50,0.01,-137.5,-0.04,-3875,0.00,1 +166,Entry short,2025-12-04 04:00,M1,6862.5,1,343125,-112.5,-0.03,0,50,0.01,-137.5,-0.04,-3875,0.00,1 +167,Exit long,2025-12-04 07:00,M1,6863.5,1,343237.5,-62.5,-0.02,0,62.5,0.02,-187.5,-0.05,-3937.5,0.00,1 +167,Entry long,2025-12-04 05:00,M1,6864.75,1,343237.5,-62.5,-0.02,0,62.5,0.02,-187.5,-0.05,-3937.5,0.00,1 +168,Exit short,2025-12-04 08:00,M1,6859.5,1,343175,200,0.06,0,287.5,0.08,-162.5,-0.05,-3737.5,0.00,1 +168,Entry short,2025-12-04 07:00,M1,6863.5,1,343175,200,0.06,0,287.5,0.08,-162.5,-0.05,-3737.5,0.00,1 +169,Exit long,2025-12-04 09:00,M1,6861.25,1,342975,87.5,0.03,0,275,0.08,-37.5,-0.01,-3650,0.00,1 +169,Entry long,2025-12-04 08:00,M1,6859.5,1,342975,87.5,0.03,0,275,0.08,-37.5,-0.01,-3650,0.00,1 +170,Exit short,2025-12-04 10:00,M1,6863.25,1,343062.5,-100,-0.03,0,187.5,0.05,-162.5,-0.05,-3750,0.00,1 +170,Entry short,2025-12-04 09:00,M1,6861.25,1,343062.5,-100,-0.03,0,187.5,0.05,-162.5,-0.05,-3750,0.00,1 +171,Exit long,2025-12-04 11:00,M1,6860.5,1,343162.5,-137.5,-0.04,0,0,0.00,-312.5,-0.09,-3887.5,0.00,1 +171,Entry long,2025-12-04 10:00,M1,6863.25,1,343162.5,-137.5,-0.04,0,0,0.00,-312.5,-0.09,-3887.5,0.00,1 +172,Exit short,2025-12-04 12:00,M1,6862.75,1,343025,-112.5,-0.03,0,100,0.03,-150,-0.04,-4000,0.00,1 +172,Entry short,2025-12-04 11:00,M1,6860.5,1,343025,-112.5,-0.03,0,100,0.03,-150,-0.04,-4000,0.00,1 +173,Exit long,2025-12-04 13:00,M1,6862.75,1,343137.5,0,0.00,0,87.5,0.03,-100,-0.03,-4000,0.00,1 +173,Entry long,2025-12-04 12:00,M1,6862.75,1,343137.5,0,0.00,0,87.5,0.03,-100,-0.03,-4000,0.00,1 +174,Exit short,2025-12-04 14:00,M1,6865.25,1,343137.5,-125,-0.04,0,50,0.01,-175,-0.05,-4125,0.00,1 +174,Entry short,2025-12-04 13:00,M1,6862.75,1,343137.5,-125,-0.04,0,50,0.01,-175,-0.05,-4125,0.00,1 +175,Exit long,2025-12-04 15:00,M1,6865.5,1,343262.5,12.5,0.00,0,162.5,0.05,-37.5,-0.01,-4112.5,0.00,1 +175,Entry long,2025-12-04 14:00,M1,6865.25,1,343262.5,12.5,0.00,0,162.5,0.05,-37.5,-0.01,-4112.5,0.00,1 +176,Exit short,2025-12-04 16:00,M1,6858.75,1,343275,337.5,0.10,0,562.5,0.16,0,0.00,-3775,0.00,1 +176,Entry short,2025-12-04 15:00,M1,6865.5,1,343275,337.5,0.10,0,562.5,0.16,0,0.00,-3775,0.00,1 +177,Exit long,2025-12-04 17:00,M1,6868.25,1,342937.5,475,0.14,0,562.5,0.16,-150,-0.04,-3300,0.00,1 +177,Entry long,2025-12-04 16:00,M1,6858.75,1,342937.5,475,0.14,0,562.5,0.16,-150,-0.04,-3300,0.00,1 +178,Exit short,2025-12-04 18:00,M1,6862.5,1,343412.5,287.5,0.08,0,337.5,0.10,-50,-0.01,-3012.5,0.00,1 +178,Entry short,2025-12-04 17:00,M1,6868.25,1,343412.5,287.5,0.08,0,337.5,0.10,-50,-0.01,-3012.5,0.00,1 +179,Exit long,2025-12-04 19:00,M1,6865.25,1,343125,137.5,0.04,0,162.5,0.05,-125,-0.04,-2875,0.00,1 +179,Entry long,2025-12-04 18:00,M1,6862.5,1,343125,137.5,0.04,0,162.5,0.05,-125,-0.04,-2875,0.00,1 +180,Exit short,2025-12-04 20:00,M1,6868.25,1,343262.5,-150,-0.04,0,37.5,0.01,-300,-0.09,-3025,0.00,1 +180,Entry short,2025-12-04 19:00,M1,6865.25,1,343262.5,-150,-0.04,0,37.5,0.01,-300,-0.09,-3025,0.00,1 +181,Exit long,2025-12-04 21:00,M1,6864.25,1,343412.5,-200,-0.06,0,137.5,0.04,-312.5,-0.09,-3225,0.00,1 +181,Entry long,2025-12-04 20:00,M1,6868.25,1,343412.5,-200,-0.06,0,137.5,0.04,-312.5,-0.09,-3225,0.00,1 +182,Exit short,2025-12-04 22:00,M1,6853.5,1,343212.5,537.5,0.16,0,700,0.20,-825,-0.24,-2687.5,0.00,1 +182,Entry short,2025-12-04 21:00,M1,6864.25,1,343212.5,537.5,0.16,0,700,0.20,-825,-0.24,-2687.5,0.00,1 +183,Exit long,2025-12-04 23:00,M1,6855.75,1,342675,112.5,0.03,0,762.5,0.22,-450,-0.13,-2575,0.00,1 +183,Entry long,2025-12-04 22:00,M1,6853.5,1,342675,112.5,0.03,0,762.5,0.22,-450,-0.13,-2575,0.00,1 +184,Exit short,2025-12-05 00:00,M1,6867.25,1,342787.5,-575,-0.17,0,537.5,0.16,-650,-0.19,-3150,0.00,1 +184,Entry short,2025-12-04 23:00,M1,6855.75,1,342787.5,-575,-0.17,0,537.5,0.16,-650,-0.19,-3150,0.00,1 +185,Exit long,2025-12-05 01:00,M1,6866,1,343362.5,-62.5,-0.02,0,75,0.02,-537.5,-0.16,-3212.5,0.00,1 +185,Entry long,2025-12-05 00:00,M1,6867.25,1,343362.5,-62.5,-0.02,0,75,0.02,-537.5,-0.16,-3212.5,0.00,1 +186,Exit short,2025-12-05 02:00,M1,6864.25,1,343300,87.5,0.03,0,412.5,0.12,-250,-0.07,-3125,0.00,1 +186,Entry short,2025-12-05 01:00,M1,6866,1,343300,87.5,0.03,0,412.5,0.12,-250,-0.07,-3125,0.00,1 +187,Exit long,2025-12-05 03:00,M1,6855.5,1,343212.5,-437.5,-0.13,0,62.5,0.02,-1400,-0.41,-3562.5,0.00,1 +187,Entry long,2025-12-05 02:00,M1,6864.25,1,343212.5,-437.5,-0.13,0,62.5,0.02,-1400,-0.41,-3562.5,0.00,1 +188,Exit short,2025-12-05 04:00,M1,6867.5,1,342775,-600,-0.18,0,475,0.14,-675,-0.20,-4162.5,0.00,1 +188,Entry short,2025-12-05 03:00,M1,6855.5,1,342775,-600,-0.18,0,475,0.14,-675,-0.20,-4162.5,0.00,1 +189,Exit long,2025-12-05 05:00,M1,6865.75,1,343375,-87.5,-0.03,0,50,0.01,-175,-0.05,-4250,0.00,1 +189,Entry long,2025-12-05 04:00,M1,6867.5,1,343375,-87.5,-0.03,0,50,0.01,-175,-0.05,-4250,0.00,1 +190,Exit short,2025-12-05 07:00,M1,6863.75,1,343287.5,100,0.03,0,225,0.07,-187.5,-0.05,-4150,0.00,1 +190,Entry short,2025-12-05 05:00,M1,6865.75,1,343287.5,100,0.03,0,225,0.07,-187.5,-0.05,-4150,0.00,1 +191,Exit long,2025-12-05 08:00,M1,6870.75,1,343187.5,350,0.10,0,387.5,0.11,-87.5,-0.03,-3800,0.00,1 +191,Entry long,2025-12-05 07:00,M1,6863.75,1,343187.5,350,0.10,0,387.5,0.11,-87.5,-0.03,-3800,0.00,1 +192,Exit short,2025-12-05 08:00,,6870.75,1,343537.5,0,0.00,0,0,0.00,0,0.00,-3800,0.00,0 +192,Entry short,2025-12-05 08:00,M1,6870.75,1,343537.5,0,0.00,0,0,0.00,0,0.00,-3800,0.00,0 \ No newline at end of file diff --git a/tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/meta.json b/tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/meta.json new file mode 100644 index 00000000..63ce42a9 --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/meta.json @@ -0,0 +1,16 @@ +{ + "title": "HM G2-36 session.ismarket", + "slug": "hm-g236-eth-60-24x7", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:01:29.897Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "60", + "from": "2025-03-07", + "to": "2025-03-11" + }, + "pine_sha256": "7c5b7e36130c230e6ad26f5525ab527486a008e2d6eaa1a5e41da41cfa256a1d", + "tv_trades_sha256": "96b7e8890b41a9e8aa60698a43cdffd5f42c55bb8d2d0223f7985c13b5040b74", + "tv_trades_rows": 194 +} diff --git a/tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/metrics.json b/tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/metrics.json new file mode 100644 index 00000000..4a29fa2d --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-g236-eth-60-24x7", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "60", + "from": "2025-03-07", + "to": "2025-03-11", + "deepBacktesting": true, + "trades": 97, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "7c5b7e36130c230e6ad26f5525ab527486a008e2d6eaa1a5e41da41cfa256a1d", + "tvTradesCsvHash": "96b7e8890b41a9e8aa60698a43cdffd5f42c55bb8d2d0223f7985c13b5040b74", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74408", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "60", + "requestedRange": { + "from": 1741305600000, + "to": 1741651200000 + }, + "returnedRange": { + "from": 1741305600000, + "to": 1741651200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "be345b3b0f658b1ef1221394f5acdc606234f41da5b1c382bb79c05388e23504", + "decodedReportSha256": "634ba2e1cd78dd4cb8e9f655d8736d62b6af4ff7b6d0e461b9941844d84edd29", + "closedTrades": 97, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:01:29.896Z" +} diff --git a/tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/strategy.pine b/tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/strategy.pine new file mode 100644 index 00000000..182c911e --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/strategy.pine @@ -0,0 +1,17 @@ +// H-MEASURE (R5 wave H) row G2-36: which chart bars does TradingView flag +// session.ismarket on? process_orders_on_close=true: every bar reverses the +// position at its own close, so every trade's ENTRY is one chart bar (TradingView +// dates a process_orders_on_close fill at the bar's open) and the entry's Signal +// (its comment) is the flag TradingView evaluated on that bar: "M1" when +// session.ismarket is true, "M0" when it is false. +//@version=6 +strategy("HM G2-36 session.ismarket", overlay=true, initial_capital=1000000000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=true, margin_long=100, margin_short=100) + +flag = session.ismarket ? "M1" : "M0" +if bar_index % 2 == 0 + strategy.entry("L" + flag, strategy.long, comment=flag) +else + strategy.entry("S" + flag, strategy.short, comment=flag) diff --git a/tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/tv_trades.csv b/tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/tv_trades.csv new file mode 100644 index 00000000..0c0b702e --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-eth-60-24x7/tv_trades.csv @@ -0,0 +1,195 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-03-07 09:00,M1,2174.07,1,2116.38,57.69,2.73,0,76.64,3.62,-9.12,-0.43,57.69,0.00,1 +1,Entry long,2025-03-07 08:00,M1,2116.38,1,2116.38,57.69,2.73,0,76.64,3.62,-9.12,-0.43,57.69,0.00,1 +2,Exit short,2025-03-07 10:00,M1,2154.5,1,2174.07,19.57,0.90,0,20.27,0.93,-11.2,-0.52,77.26,0.00,1 +2,Entry short,2025-03-07 09:00,M1,2174.07,1,2174.07,19.57,0.90,0,20.27,0.93,-11.2,-0.52,77.26,0.00,1 +3,Exit long,2025-03-07 11:00,M1,2147.21,1,2154.5,-7.29,-0.34,0,7.84,0.36,-9.15,-0.42,69.97,0.00,1 +3,Entry long,2025-03-07 10:00,M1,2154.5,1,2154.5,-7.29,-0.34,0,7.84,0.36,-9.15,-0.42,69.97,0.00,1 +4,Exit short,2025-03-07 12:00,M1,2154.32,1,2147.21,-7.11,-0.33,0,3.8,0.18,-16.79,-0.78,62.86,0.00,1 +4,Entry short,2025-03-07 11:00,M1,2147.21,1,2147.21,-7.11,-0.33,0,3.8,0.18,-16.79,-0.78,62.86,0.00,1 +5,Exit long,2025-03-07 13:00,M1,2173.73,1,2154.32,19.41,0.90,0,28.86,1.34,-1,-0.05,82.27,0.00,1 +5,Entry long,2025-03-07 12:00,M1,2154.32,1,2154.32,19.41,0.90,0,28.86,1.34,-1,-0.05,82.27,0.00,1 +6,Exit short,2025-03-07 14:00,M1,2181.32,1,2173.73,-7.59,-0.35,0,9.42,0.43,-12.53,-0.58,74.68,0.00,1 +6,Entry short,2025-03-07 13:00,M1,2173.73,1,2173.73,-7.59,-0.35,0,9.42,0.43,-12.53,-0.58,74.68,0.00,1 +7,Exit long,2025-03-07 15:00,M1,2191.52,1,2181.32,10.2,0.47,0,11.52,0.53,-4.5,-0.21,84.88,0.00,1 +7,Entry long,2025-03-07 14:00,M1,2181.32,1,2181.32,10.2,0.47,0,11.52,0.53,-4.5,-0.21,84.88,0.00,1 +8,Exit short,2025-03-07 16:00,M1,2184.4,1,2191.52,7.12,0.32,0,15.47,0.71,-4.36,-0.20,92,0.00,1 +8,Entry short,2025-03-07 15:00,M1,2191.52,1,2191.52,7.12,0.32,0,15.47,0.71,-4.36,-0.20,92,0.00,1 +9,Exit long,2025-03-07 17:00,M1,2192.7,1,2184.4,8.3,0.38,0,25.54,1.17,-0.42,-0.02,100.3,0.00,1 +9,Entry long,2025-03-07 16:00,M1,2184.4,1,2184.4,8.3,0.38,0,25.54,1.17,-0.42,-0.02,100.3,0.00,1 +10,Exit short,2025-03-07 18:00,M1,2195.72,1,2192.7,-3.02,-0.14,0,8.21,0.37,-4.57,-0.21,97.28,0.00,1 +10,Entry short,2025-03-07 17:00,M1,2192.7,1,2192.7,-3.02,-0.14,0,8.21,0.37,-4.57,-0.21,97.28,0.00,1 +11,Exit long,2025-03-07 19:00,M1,2196.69,1,2195.72,0.97,0.04,0,11.28,0.51,-6.44,-0.29,98.25,0.00,1 +11,Entry long,2025-03-07 18:00,M1,2195.72,1,2195.72,0.97,0.04,0,11.28,0.51,-6.44,-0.29,98.25,0.00,1 +12,Exit short,2025-03-07 20:00,M1,2177.26,1,2196.69,19.43,0.88,0,32.32,1.47,-1.64,-0.07,117.68,0.00,1 +12,Entry short,2025-03-07 19:00,M1,2196.69,1,2196.69,19.43,0.88,0,32.32,1.47,-1.64,-0.07,117.68,0.00,1 +13,Exit long,2025-03-07 21:00,M1,2187.56,1,2177.26,10.3,0.47,0,51.74,2.38,-0.74,-0.03,127.98,0.00,1 +13,Entry long,2025-03-07 20:00,M1,2177.26,1,2177.26,10.3,0.47,0,51.74,2.38,-0.74,-0.03,127.98,0.00,1 +14,Exit short,2025-03-07 22:00,M1,2220.8,1,2187.56,-33.24,-1.52,0,11.92,0.54,-71.78,-3.28,94.74,0.00,1 +14,Entry short,2025-03-07 21:00,M1,2187.56,1,2187.56,-33.24,-1.52,0,11.92,0.54,-71.78,-3.28,94.74,0.00,1 +15,Exit long,2025-03-07 23:00,M1,2208.94,1,2220.8,-11.86,-0.53,0,17.86,0.80,-41.12,-1.85,82.88,0.00,1 +15,Entry long,2025-03-07 22:00,M1,2220.8,1,2220.8,-11.86,-0.53,0,17.86,0.80,-41.12,-1.85,82.88,0.00,1 +16,Exit short,2025-03-08 00:00,M1,2157.53,1,2208.94,51.41,2.33,0,66.57,3.01,-5.91,-0.27,134.29,0.00,1 +16,Entry short,2025-03-07 23:00,M1,2208.94,1,2208.94,51.41,2.33,0,66.57,3.01,-5.91,-0.27,134.29,0.00,1 +17,Exit long,2025-03-08 01:00,M1,2161.23,1,2157.53,3.7,0.17,0,26.33,1.22,-17.7,-0.82,137.99,0.00,1 +17,Entry long,2025-03-08 00:00,M1,2157.53,1,2157.53,3.7,0.17,0,26.33,1.22,-17.7,-0.82,137.99,0.00,1 +18,Exit short,2025-03-08 02:00,M1,2193.99,1,2161.23,-32.76,-1.52,0,5.02,0.23,-38.76,-1.79,105.23,0.00,1 +18,Entry short,2025-03-08 01:00,M1,2161.23,1,2161.23,-32.76,-1.52,0,5.02,0.23,-38.76,-1.79,105.23,0.00,1 +19,Exit long,2025-03-08 03:00,M1,2177.47,1,2193.99,-16.52,-0.75,0,7.86,0.36,-33.69,-1.54,88.71,0.00,1 +19,Entry long,2025-03-08 02:00,M1,2193.99,1,2193.99,-16.52,-0.75,0,7.86,0.36,-33.69,-1.54,88.71,0.00,1 +20,Exit short,2025-03-08 04:00,M1,2156.37,1,2177.47,21.1,0.97,0,32.77,1.50,-8.74,-0.40,109.81,0.00,1 +20,Entry short,2025-03-08 03:00,M1,2177.47,1,2177.47,21.1,0.97,0,32.77,1.50,-8.74,-0.40,109.81,0.00,1 +21,Exit long,2025-03-08 05:00,M1,2138.36,1,2156.37,-18.01,-0.84,0,7.1,0.33,-43.27,-2.01,91.8,0.00,1 +21,Entry long,2025-03-08 04:00,M1,2156.37,1,2156.37,-18.01,-0.84,0,7.1,0.33,-43.27,-2.01,91.8,0.00,1 +22,Exit short,2025-03-08 06:00,M1,2143.46,1,2138.36,-5.1,-0.24,0,23.2,1.08,-12.46,-0.58,86.7,0.00,1 +22,Entry short,2025-03-08 05:00,M1,2138.36,1,2138.36,-5.1,-0.24,0,23.2,1.08,-12.46,-0.58,86.7,0.00,1 +23,Exit long,2025-03-08 07:00,M1,2140.72,1,2143.46,-2.74,-0.13,0,0.32,0.01,-29.53,-1.38,83.96,0.00,1 +23,Entry long,2025-03-08 06:00,M1,2143.46,1,2143.46,-2.74,-0.13,0,0.32,0.01,-29.53,-1.38,83.96,0.00,1 +24,Exit short,2025-03-08 08:00,M1,2151.25,1,2140.72,-10.53,-0.49,0,11.41,0.53,-11.5,-0.54,73.43,0.00,1 +24,Entry short,2025-03-08 07:00,M1,2140.72,1,2140.72,-10.53,-0.49,0,11.41,0.53,-11.5,-0.54,73.43,0.00,1 +25,Exit long,2025-03-08 09:00,M1,2131.96,1,2151.25,-19.29,-0.90,0,4.12,0.19,-48.18,-2.24,54.14,0.00,1 +25,Entry long,2025-03-08 08:00,M1,2151.25,1,2151.25,-19.29,-0.90,0,4.12,0.19,-48.18,-2.24,54.14,0.00,1 +26,Exit short,2025-03-08 10:00,M1,2142.97,1,2131.96,-11.01,-0.52,0,1.88,0.09,-12.99,-0.61,43.13,0.00,1 +26,Entry short,2025-03-08 09:00,M1,2131.96,1,2131.96,-11.01,-0.52,0,1.88,0.09,-12.99,-0.61,43.13,0.00,1 +27,Exit long,2025-03-08 11:00,M1,2137.07,1,2142.97,-5.9,-0.28,0,0.01,0.00,-19.45,-0.91,37.23,0.00,1 +27,Entry long,2025-03-08 10:00,M1,2142.97,1,2142.97,-5.9,-0.28,0,0.01,0.00,-19.45,-0.91,37.23,0.00,1 +28,Exit short,2025-03-08 12:00,M1,2149.12,1,2137.07,-12.05,-0.56,0,6.65,0.31,-14.96,-0.70,25.18,0.00,1 +28,Entry short,2025-03-08 11:00,M1,2137.07,1,2137.07,-12.05,-0.56,0,6.65,0.31,-14.96,-0.70,25.18,0.00,1 +29,Exit long,2025-03-08 13:00,M1,2139.46,1,2149.12,-9.66,-0.45,0,0.54,0.03,-16.77,-0.78,15.52,0.00,1 +29,Entry long,2025-03-08 12:00,M1,2149.12,1,2149.12,-9.66,-0.45,0,0.54,0.03,-16.77,-0.78,15.52,0.00,1 +30,Exit short,2025-03-08 14:00,M1,2138.26,1,2139.46,1.2,0.06,0,7.46,0.35,-5.13,-0.24,16.72,0.00,1 +30,Entry short,2025-03-08 13:00,M1,2139.46,1,2139.46,1.2,0.06,0,7.46,0.35,-5.13,-0.24,16.72,0.00,1 +31,Exit long,2025-03-08 15:00,M1,2136.65,1,2138.26,-1.61,-0.08,0,8.74,0.41,-5.11,-0.24,15.11,0.00,1 +31,Entry long,2025-03-08 14:00,M1,2138.26,1,2138.26,-1.61,-0.08,0,8.74,0.41,-5.11,-0.24,15.11,0.00,1 +32,Exit short,2025-03-08 16:00,M1,2127.2,1,2136.65,9.45,0.44,0,11.1,0.52,-3.47,-0.16,24.56,0.00,1 +32,Entry short,2025-03-08 15:00,M1,2136.65,1,2136.65,9.45,0.44,0,11.1,0.52,-3.47,-0.16,24.56,0.00,1 +33,Exit long,2025-03-08 17:00,M1,2128.64,1,2127.2,1.44,0.07,0,15.3,0.72,-1.8,-0.08,26,0.00,1 +33,Entry long,2025-03-08 16:00,M1,2127.2,1,2127.2,1.44,0.07,0,15.3,0.72,-1.8,-0.08,26,0.00,1 +34,Exit short,2025-03-08 18:00,M1,2138.77,1,2128.64,-10.13,-0.48,0,1.98,0.09,-11.52,-0.54,15.87,0.00,1 +34,Entry short,2025-03-08 17:00,M1,2128.64,1,2128.64,-10.13,-0.48,0,1.98,0.09,-11.52,-0.54,15.87,0.00,1 +35,Exit long,2025-03-08 19:00,M1,2140.89,1,2138.77,2.12,0.10,0,2.73,0.13,-12.6,-0.59,17.99,0.00,1 +35,Entry long,2025-03-08 18:00,M1,2138.77,1,2138.77,2.12,0.10,0,2.73,0.13,-12.6,-0.59,17.99,0.00,1 +36,Exit short,2025-03-08 20:00,M1,2167.48,1,2140.89,-26.59,-1.24,0,4.05,0.19,-31.96,-1.49,-8.6,0.00,1 +36,Entry short,2025-03-08 19:00,M1,2140.89,1,2140.89,-26.59,-1.24,0,4.05,0.19,-31.96,-1.49,-8.6,0.00,1 +37,Exit long,2025-03-08 21:00,M1,2189.25,1,2167.48,21.77,1.00,0,30.52,1.41,-2.34,-0.11,13.17,0.00,1 +37,Entry long,2025-03-08 20:00,M1,2167.48,1,2167.48,21.77,1.00,0,30.52,1.41,-2.34,-0.11,13.17,0.00,1 +38,Exit short,2025-03-08 22:00,M1,2178,1,2189.25,11.25,0.51,0,15.76,0.72,-5.57,-0.25,24.42,0.00,1 +38,Entry short,2025-03-08 21:00,M1,2189.25,1,2189.25,11.25,0.51,0,15.76,0.72,-5.57,-0.25,24.42,0.00,1 +39,Exit long,2025-03-08 23:00,M1,2180.41,1,2178,2.41,0.11,0,13.36,0.61,-11,-0.51,26.83,0.00,1 +39,Entry long,2025-03-08 22:00,M1,2178,1,2178,2.41,0.11,0,13.36,0.61,-11,-0.51,26.83,0.00,1 +40,Exit short,2025-03-09 00:00,M1,2189.17,1,2180.41,-8.76,-0.40,0,20.21,0.93,-17.28,-0.79,18.07,0.00,1 +40,Entry short,2025-03-08 23:00,M1,2180.41,1,2180.41,-8.76,-0.40,0,20.21,0.93,-17.28,-0.79,18.07,0.00,1 +41,Exit long,2025-03-09 01:00,M1,2198.1,1,2189.17,8.93,0.41,0,31.27,1.43,-10.17,-0.46,27,0.00,1 +41,Entry long,2025-03-09 00:00,M1,2189.17,1,2189.17,8.93,0.41,0,31.27,1.43,-10.17,-0.46,27,0.00,1 +42,Exit short,2025-03-09 02:00,M1,2207.76,1,2198.1,-9.66,-0.44,0,4.86,0.22,-15.5,-0.71,17.34,0.00,1 +42,Entry short,2025-03-09 01:00,M1,2198.1,1,2198.1,-9.66,-0.44,0,4.86,0.22,-15.5,-0.71,17.34,0.00,1 +43,Exit long,2025-03-09 03:00,M1,2223.73,1,2207.76,15.97,0.72,0,26.64,1.21,-8.12,-0.37,33.31,0.00,1 +43,Entry long,2025-03-09 02:00,M1,2207.76,1,2207.76,15.97,0.72,0,26.64,1.21,-8.12,-0.37,33.31,0.00,1 +44,Exit short,2025-03-09 04:00,M1,2212.01,1,2223.73,11.72,0.53,0,13.23,0.59,-1.42,-0.06,45.03,0.00,1 +44,Entry short,2025-03-09 03:00,M1,2223.73,1,2223.73,11.72,0.53,0,13.23,0.59,-1.42,-0.06,45.03,0.00,1 +45,Exit long,2025-03-09 05:00,M1,2215.28,1,2212.01,3.27,0.15,0,12.01,0.54,-0.28,-0.01,48.3,0.00,1 +45,Entry long,2025-03-09 04:00,M1,2212.01,1,2212.01,3.27,0.15,0,12.01,0.54,-0.28,-0.01,48.3,0.00,1 +46,Exit short,2025-03-09 06:00,M1,2206.66,1,2215.28,8.62,0.39,0,18.41,0.83,-1.71,-0.08,56.92,0.00,1 +46,Entry short,2025-03-09 05:00,M1,2215.28,1,2215.28,8.62,0.39,0,18.41,0.83,-1.71,-0.08,56.92,0.00,1 +47,Exit long,2025-03-09 07:00,M1,2202.84,1,2206.66,-3.82,-0.17,0,3.14,0.14,-14.25,-0.65,53.1,0.00,1 +47,Entry long,2025-03-09 06:00,M1,2206.66,1,2206.66,-3.82,-0.17,0,3.14,0.14,-14.25,-0.65,53.1,0.00,1 +48,Exit short,2025-03-09 08:00,M1,2205.34,1,2202.84,-2.5,-0.11,0,4.04,0.18,-5.96,-0.27,50.6,0.00,1 +48,Entry short,2025-03-09 07:00,M1,2202.84,1,2202.84,-2.5,-0.11,0,4.04,0.18,-5.96,-0.27,50.6,0.00,1 +49,Exit long,2025-03-09 09:00,M1,2190.11,1,2205.34,-15.23,-0.69,0,5.93,0.27,-19.1,-0.87,35.37,0.00,1 +49,Entry long,2025-03-09 08:00,M1,2205.34,1,2205.34,-15.23,-0.69,0,5.93,0.27,-19.1,-0.87,35.37,0.00,1 +50,Exit short,2025-03-09 10:00,M1,2180.89,1,2190.11,9.22,0.42,0,11.03,0.50,-3.58,-0.16,44.59,0.00,1 +50,Entry short,2025-03-09 09:00,M1,2190.11,1,2190.11,9.22,0.42,0,11.03,0.50,-3.58,-0.16,44.59,0.00,1 +51,Exit long,2025-03-09 11:00,M1,2184.82,1,2180.89,3.93,0.18,0,8.11,0.37,-5.88,-0.27,48.52,0.00,1 +51,Entry long,2025-03-09 10:00,M1,2180.89,1,2180.89,3.93,0.18,0,8.11,0.37,-5.88,-0.27,48.52,0.00,1 +52,Exit short,2025-03-09 12:00,M1,2188.6,1,2184.82,-3.78,-0.17,0,2.27,0.10,-5.9,-0.27,44.74,0.00,1 +52,Entry short,2025-03-09 11:00,M1,2184.82,1,2184.82,-3.78,-0.17,0,2.27,0.10,-5.9,-0.27,44.74,0.00,1 +53,Exit long,2025-03-09 13:00,M1,2182.09,1,2188.6,-6.51,-0.30,0,7.68,0.35,-9.2,-0.42,38.23,0.00,1 +53,Entry long,2025-03-09 12:00,M1,2188.6,1,2188.6,-6.51,-0.30,0,7.68,0.35,-9.2,-0.42,38.23,0.00,1 +54,Exit short,2025-03-09 14:00,M1,2178.79,1,2182.09,3.3,0.15,0,11.26,0.52,-1.89,-0.09,41.53,0.00,1 +54,Entry short,2025-03-09 13:00,M1,2182.09,1,2182.09,3.3,0.15,0,11.26,0.52,-1.89,-0.09,41.53,0.00,1 +55,Exit long,2025-03-09 15:00,M1,2184.07,1,2178.79,5.28,0.24,0,9.78,0.45,-1.69,-0.08,46.81,0.00,1 +55,Entry long,2025-03-09 14:00,M1,2178.79,1,2178.79,5.28,0.24,0,9.78,0.45,-1.69,-0.08,46.81,0.00,1 +56,Exit short,2025-03-09 16:00,M1,2175.29,1,2184.07,8.78,0.40,0,9.66,0.44,-2.93,-0.13,55.59,0.00,1 +56,Entry short,2025-03-09 15:00,M1,2184.07,1,2184.07,8.78,0.40,0,9.66,0.44,-2.93,-0.13,55.59,0.00,1 +57,Exit long,2025-03-09 17:00,M1,2164.81,1,2175.29,-10.48,-0.48,0,4.45,0.20,-15.49,-0.71,45.11,0.00,1 +57,Entry long,2025-03-09 16:00,M1,2175.29,1,2175.29,-10.48,-0.48,0,4.45,0.20,-15.49,-0.71,45.11,0.00,1 +58,Exit short,2025-03-09 18:00,M1,2132.84,1,2164.81,31.97,1.48,0,37.79,1.75,-4.57,-0.21,77.08,0.00,1 +58,Entry short,2025-03-09 17:00,M1,2164.81,1,2164.81,31.97,1.48,0,37.79,1.75,-4.57,-0.21,77.08,0.00,1 +59,Exit long,2025-03-09 19:00,M1,2145.63,1,2132.84,12.79,0.60,0,14.8,0.69,-1.55,-0.07,89.87,0.00,1 +59,Entry long,2025-03-09 18:00,M1,2132.84,1,2132.84,12.79,0.60,0,14.8,0.69,-1.55,-0.07,89.87,0.00,1 +60,Exit short,2025-03-09 20:00,M1,2140.9,1,2145.63,4.73,0.22,0,15.22,0.71,-0.43,-0.02,94.6,0.00,1 +60,Entry short,2025-03-09 19:00,M1,2145.63,1,2145.63,4.73,0.22,0,15.22,0.71,-0.43,-0.02,94.6,0.00,1 +61,Exit long,2025-03-09 21:00,M1,2103.2,1,2140.9,-37.7,-1.76,0,2.1,0.10,-40.74,-1.90,56.9,0.00,1 +61,Entry long,2025-03-09 20:00,M1,2140.9,1,2140.9,-37.7,-1.76,0,2.1,0.10,-40.74,-1.90,56.9,0.00,1 +62,Exit short,2025-03-09 22:00,M1,2116.32,1,2103.2,-13.12,-0.62,0,8.51,0.40,-20.5,-0.97,43.78,0.00,1 +62,Entry short,2025-03-09 21:00,M1,2103.2,1,2103.2,-13.12,-0.62,0,8.51,0.40,-20.5,-0.97,43.78,0.00,1 +63,Exit long,2025-03-09 23:00,M1,2095.86,1,2116.32,-20.46,-0.97,0,5.27,0.25,-34.32,-1.62,23.32,0.00,1 +63,Entry long,2025-03-09 22:00,M1,2116.32,1,2116.32,-20.46,-0.97,0,5.27,0.25,-34.32,-1.62,23.32,0.00,1 +64,Exit short,2025-03-10 00:00,M1,2048,1,2095.86,47.86,2.28,0,55.86,2.67,-12.44,-0.59,71.18,0.00,1 +64,Entry short,2025-03-09 23:00,M1,2095.86,1,2095.86,47.86,2.28,0,55.86,2.67,-12.44,-0.59,71.18,0.00,1 +65,Exit long,2025-03-10 01:00,M1,2022.82,1,2048,-25.18,-1.23,0,8.19,0.40,-35.5,-1.73,46,0.00,1 +65,Entry long,2025-03-10 00:00,M1,2048,1,2048,-25.18,-1.23,0,8.19,0.40,-35.5,-1.73,46,0.00,1 +66,Exit short,2025-03-10 02:00,M1,2019.99,1,2022.82,2.83,0.14,0,26.99,1.33,-15.41,-0.76,48.83,0.00,1 +66,Entry short,2025-03-10 01:00,M1,2022.82,1,2022.82,2.83,0.14,0,26.99,1.33,-15.41,-0.76,48.83,0.00,1 +67,Exit long,2025-03-10 03:00,M1,2039.34,1,2019.99,19.35,0.96,0,28.11,1.39,-4.59,-0.23,68.18,0.00,1 +67,Entry long,2025-03-10 02:00,M1,2019.99,1,2019.99,19.35,0.96,0,28.11,1.39,-4.59,-0.23,68.18,0.00,1 +68,Exit short,2025-03-10 04:00,M1,2046,1,2039.34,-6.66,-0.33,0,3.95,0.19,-16.3,-0.80,61.52,0.00,1 +68,Entry short,2025-03-10 03:00,M1,2039.34,1,2039.34,-6.66,-0.33,0,3.95,0.19,-16.3,-0.80,61.52,0.00,1 +69,Exit long,2025-03-10 05:00,M1,2023.51,1,2046,-22.49,-1.10,0,3.63,0.18,-33.95,-1.66,39.03,0.00,1 +69,Entry long,2025-03-10 04:00,M1,2046,1,2046,-22.49,-1.10,0,3.63,0.18,-33.95,-1.66,39.03,0.00,1 +70,Exit short,2025-03-10 06:00,M1,2009.28,1,2023.51,14.23,0.70,0,18.74,0.93,-5.81,-0.29,53.26,0.00,1 +70,Entry short,2025-03-10 05:00,M1,2023.51,1,2023.51,14.23,0.70,0,18.74,0.93,-5.81,-0.29,53.26,0.00,1 +71,Exit long,2025-03-10 07:00,M1,2019.2,1,2009.28,9.92,0.49,0,22.38,1.11,-22.48,-1.12,63.18,0.00,1 +71,Entry long,2025-03-10 06:00,M1,2009.28,1,2009.28,9.92,0.49,0,22.38,1.11,-22.48,-1.12,63.18,0.00,1 +72,Exit short,2025-03-10 08:00,M1,2035.1,1,2019.2,-15.9,-0.79,0,25.66,1.27,-23.5,-1.16,47.28,0.00,1 +72,Entry short,2025-03-10 07:00,M1,2019.2,1,2019.2,-15.9,-0.79,0,25.66,1.27,-23.5,-1.16,47.28,0.00,1 +73,Exit long,2025-03-10 09:00,M1,2042.16,1,2035.1,7.06,0.35,0,17.9,0.88,-9.08,-0.45,54.34,0.00,1 +73,Entry long,2025-03-10 08:00,M1,2035.1,1,2035.1,7.06,0.35,0,17.9,0.88,-9.08,-0.45,54.34,0.00,1 +74,Exit short,2025-03-10 10:00,M1,2056.75,1,2042.16,-14.59,-0.71,0,6.55,0.32,-27.65,-1.35,39.75,0.00,1 +74,Entry short,2025-03-10 09:00,M1,2042.16,1,2042.16,-14.59,-0.71,0,6.55,0.32,-27.65,-1.35,39.75,0.00,1 +75,Exit long,2025-03-10 11:00,M1,2056.82,1,2056.75,0.07,0.00,0,7.72,0.38,-13.68,-0.67,39.82,0.00,1 +75,Entry long,2025-03-10 10:00,M1,2056.75,1,2056.75,0.07,0.00,0,7.72,0.38,-13.68,-0.67,39.82,0.00,1 +76,Exit short,2025-03-10 12:00,M1,2071.75,1,2056.82,-14.93,-0.73,0,1.96,0.10,-19.36,-0.94,24.89,0.00,1 +76,Entry short,2025-03-10 11:00,M1,2056.82,1,2056.82,-14.93,-0.73,0,1.96,0.10,-19.36,-0.94,24.89,0.00,1 +77,Exit long,2025-03-10 13:00,M1,2063.12,1,2071.75,-8.63,-0.42,0,3.25,0.16,-13.75,-0.66,16.26,0.00,1 +77,Entry long,2025-03-10 12:00,M1,2071.75,1,2071.75,-8.63,-0.42,0,3.25,0.16,-13.75,-0.66,16.26,0.00,1 +78,Exit short,2025-03-10 14:00,M1,2066,1,2063.12,-2.88,-0.14,0,9.94,0.48,-3.84,-0.19,13.38,0.00,1 +78,Entry short,2025-03-10 13:00,M1,2063.12,1,2063.12,-2.88,-0.14,0,9.94,0.48,-3.84,-0.19,13.38,0.00,1 +79,Exit long,2025-03-10 15:00,M1,2074.2,1,2066,8.2,0.40,0,12.55,0.61,-4,-0.19,21.58,0.00,1 +79,Entry long,2025-03-10 14:00,M1,2066,1,2066,8.2,0.40,0,12.55,0.61,-4,-0.19,21.58,0.00,1 +80,Exit short,2025-03-10 16:00,M1,2061.1,1,2074.2,13.1,0.63,0,27.45,1.32,-2.14,-0.10,34.68,0.00,1 +80,Entry short,2025-03-10 15:00,M1,2074.2,1,2074.2,13.1,0.63,0,27.45,1.32,-2.14,-0.10,34.68,0.00,1 +81,Exit long,2025-03-10 17:00,M1,2100.98,1,2061.1,39.88,1.93,0,91.89,4.46,-0.21,-0.01,74.56,0.00,1 +81,Entry long,2025-03-10 16:00,M1,2061.1,1,2061.1,39.88,1.93,0,91.89,4.46,-0.21,-0.01,74.56,0.00,1 +82,Exit short,2025-03-10 18:00,M1,2098.92,1,2100.98,2.06,0.10,0,25.64,1.22,-4.7,-0.22,76.62,0.00,1 +82,Entry short,2025-03-10 17:00,M1,2100.98,1,2100.98,2.06,0.10,0,25.64,1.22,-4.7,-0.22,76.62,0.00,1 +83,Exit long,2025-03-10 19:00,M1,2127.13,1,2098.92,28.21,1.34,0,39.44,1.88,-1.44,-0.07,104.83,0.00,1 +83,Entry long,2025-03-10 18:00,M1,2098.92,1,2098.92,28.21,1.34,0,39.44,1.88,-1.44,-0.07,104.83,0.00,1 +84,Exit short,2025-03-10 20:00,M1,2124.79,1,2127.13,2.34,0.11,0,23.1,1.09,-7.8,-0.37,107.17,0.00,1 +84,Entry short,2025-03-10 19:00,M1,2127.13,1,2127.13,2.34,0.11,0,23.1,1.09,-7.8,-0.37,107.17,0.00,1 +85,Exit long,2025-03-10 21:00,M1,2082.79,1,2124.79,-42,-1.98,0,4.46,0.21,-47.67,-2.24,65.17,0.00,1 +85,Entry long,2025-03-10 20:00,M1,2124.79,1,2124.79,-42,-1.98,0,4.46,0.21,-47.67,-2.24,65.17,0.00,1 +86,Exit short,2025-03-10 22:00,M1,2018.12,1,2082.79,64.67,3.10,0,87.32,4.19,-0.49,-0.02,129.84,0.00,1 +86,Entry short,2025-03-10 21:00,M1,2082.79,1,2082.79,64.67,3.10,0,87.32,4.19,-0.49,-0.02,129.84,0.00,1 +87,Exit long,2025-03-10 23:00,M1,2016.99,1,2018.12,-1.13,-0.06,0,24.73,1.23,-8.55,-0.42,128.71,0.00,1 +87,Entry long,2025-03-10 22:00,M1,2018.12,1,2018.12,-1.13,-0.06,0,24.73,1.23,-8.55,-0.42,128.71,0.00,1 +88,Exit short,2025-03-11 00:00,M1,2004.6,1,2016.99,12.39,0.61,0,18.52,0.92,-7.3,-0.36,141.1,0.00,1 +88,Entry short,2025-03-10 23:00,M1,2016.99,1,2016.99,12.39,0.61,0,18.52,0.92,-7.3,-0.36,141.1,0.00,1 +89,Exit long,2025-03-11 01:00,M1,1916.65,1,2004.6,-87.95,-4.39,0,3.86,0.19,-99.2,-4.95,53.15,0.00,1 +89,Entry long,2025-03-11 00:00,M1,2004.6,1,2004.6,-87.95,-4.39,0,3.86,0.19,-99.2,-4.95,53.15,0.00,1 +90,Exit short,2025-03-11 02:00,M1,1823.99,1,1916.65,92.66,4.83,0,105.6,5.51,-21.73,-1.13,145.81,0.00,1 +90,Entry short,2025-03-11 01:00,M1,1916.65,1,1916.65,92.66,4.83,0,105.6,5.51,-21.73,-1.13,145.81,0.00,1 +91,Exit long,2025-03-11 03:00,M1,1868.66,1,1823.99,44.67,2.45,0,59.01,3.24,-16.24,-0.89,190.48,0.00,1 +91,Entry long,2025-03-11 02:00,M1,1823.99,1,1823.99,44.67,2.45,0,59.01,3.24,-16.24,-0.89,190.48,0.00,1 +92,Exit short,2025-03-11 04:00,M1,1868.1,1,1868.66,0.56,0.03,0,26.45,1.42,-12.81,-0.69,191.04,0.00,1 +92,Entry short,2025-03-11 03:00,M1,1868.66,1,1868.66,0.56,0.03,0,26.45,1.42,-12.81,-0.69,191.04,0.00,1 +93,Exit long,2025-03-11 05:00,M1,1886.38,1,1868.1,18.28,0.98,0,34.9,1.87,-7.84,-0.42,209.32,0.00,1 +93,Entry long,2025-03-11 04:00,M1,1868.1,1,1868.1,18.28,0.98,0,34.9,1.87,-7.84,-0.42,209.32,0.00,1 +94,Exit short,2025-03-11 06:00,M1,1878.91,1,1886.38,7.47,0.40,0,21.58,1.14,-3.03,-0.16,216.79,0.00,1 +94,Entry short,2025-03-11 05:00,M1,1886.38,1,1886.38,7.47,0.40,0,21.58,1.14,-3.03,-0.16,216.79,0.00,1 +95,Exit long,2025-03-11 07:00,M1,1864.51,1,1878.91,-14.4,-0.77,0,15.66,0.83,-22.42,-1.19,202.39,0.00,1 +95,Entry long,2025-03-11 06:00,M1,1878.91,1,1878.91,-14.4,-0.77,0,15.66,0.83,-22.42,-1.19,202.39,0.00,1 +96,Exit short,2025-03-11 08:00,M1,1813.08,1,1864.51,51.43,2.76,0,112.01,6.01,-24.98,-1.34,253.82,0.00,1 +96,Entry short,2025-03-11 07:00,M1,1864.51,1,1864.51,51.43,2.76,0,112.01,6.01,-24.98,-1.34,253.82,0.00,1 +97,Exit long,2025-03-11 08:00,,1813.08,1,1813.08,0,0.00,0,0,0.00,0,0.00,253.82,0.00,0 +97,Entry long,2025-03-11 08:00,M1,1813.08,1,1813.08,0,0.00,0,0,0.00,0,0.00,253.82,0.00,0 \ No newline at end of file diff --git a/tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/meta.json b/tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/meta.json new file mode 100644 index 00000000..afe043fe --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/meta.json @@ -0,0 +1,16 @@ +{ + "title": "HM G2-36 session.ismarket", + "slug": "hm-g236-eurusd-60-dst-mar", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:01:34.199Z", + "chart": { + "symbol": "OANDA:EURUSD", + "interval": "60", + "from": "2025-03-02", + "to": "2025-03-14" + }, + "pine_sha256": "7c5b7e36130c230e6ad26f5525ab527486a008e2d6eaa1a5e41da41cfa256a1d", + "tv_trades_sha256": "60713d2c22442ffe14070fddb1c5cd7fbf6fc6054c0b532b4c4275de305decf1", + "tv_trades_rows": 440 +} diff --git a/tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/metrics.json b/tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/metrics.json new file mode 100644 index 00000000..70704a51 --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-g236-eurusd-60-dst-mar", + "symbol": "OANDA:EURUSD", + "interval": "60", + "from": "2025-03-02", + "to": "2025-03-14", + "deepBacktesting": true, + "trades": 220, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "7c5b7e36130c230e6ad26f5525ab527486a008e2d6eaa1a5e41da41cfa256a1d", + "tvTradesCsvHash": "60713d2c22442ffe14070fddb1c5cd7fbf6fc6054c0b532b4c4275de305decf1", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74408", + "symbol": "OANDA:EURUSD", + "interval": "60", + "requestedRange": { + "from": 1740873600000, + "to": 1741910400000 + }, + "returnedRange": { + "from": 1740952800000, + "to": 1741910400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "052d53c7ab003e6cf49edb3813ebfcb6513d113a3e8ec5b6ddb0ea22e774094e", + "decodedReportSha256": "762d7e2720ea4ac0f4973bf2a760feedef762c31661c61c04f6e5ad549ce113f", + "closedTrades": 220, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:01:34.198Z" +} diff --git a/tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/strategy.pine b/tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/strategy.pine new file mode 100644 index 00000000..182c911e --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/strategy.pine @@ -0,0 +1,17 @@ +// H-MEASURE (R5 wave H) row G2-36: which chart bars does TradingView flag +// session.ismarket on? process_orders_on_close=true: every bar reverses the +// position at its own close, so every trade's ENTRY is one chart bar (TradingView +// dates a process_orders_on_close fill at the bar's open) and the entry's Signal +// (its comment) is the flag TradingView evaluated on that bar: "M1" when +// session.ismarket is true, "M0" when it is false. +//@version=6 +strategy("HM G2-36 session.ismarket", overlay=true, initial_capital=1000000000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=true, margin_long=100, margin_short=100) + +flag = session.ismarket ? "M1" : "M0" +if bar_index % 2 == 0 + strategy.entry("L" + flag, strategy.long, comment=flag) +else + strategy.entry("S" + flag, strategy.short, comment=flag) diff --git a/tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/tv_trades.csv b/tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/tv_trades.csv new file mode 100644 index 00000000..20a2101e --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-eurusd-60-dst-mar/tv_trades.csv @@ -0,0 +1,441 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-03-03 07:00,M1,1.04122,1,1.04038,0.00084,0.08,0,0.00174,0.17,-0.00038,-0.04,0.00084,0.00,1 +1,Entry long,2025-03-03 06:00,M1,1.04038,1,1.04038,0.00084,0.08,0,0.00174,0.17,-0.00038,-0.04,0.00084,0.00,1 +2,Exit short,2025-03-03 08:00,M1,1.04095,1,1.04122,0.00027,0.03,0,0.00098,0.09,-0.00078,-0.07,0.00111,0.00,1 +2,Entry short,2025-03-03 07:00,M1,1.04122,1,1.04122,0.00027,0.03,0,0.00098,0.09,-0.00078,-0.07,0.00111,0.00,1 +3,Exit long,2025-03-03 09:00,M1,1.04199,1,1.04095,0.00104,0.10,0,0.00138,0.13,-0.00019,-0.02,0.00215,0.00,1 +3,Entry long,2025-03-03 08:00,M1,1.04095,1,1.04095,0.00104,0.10,0,0.00138,0.13,-0.00019,-0.02,0.00215,0.00,1 +4,Exit short,2025-03-03 10:00,M1,1.042,1,1.04199,-0.00001,0.00,0,0.00049,0.05,-0.00037,-0.04,0.00214,0.00,1 +4,Entry short,2025-03-03 09:00,M1,1.04199,1,1.04199,-0.00001,0.00,0,0.00049,0.05,-0.00037,-0.04,0.00214,0.00,1 +5,Exit long,2025-03-03 11:00,M1,1.0417,1,1.042,-0.0003,-0.03,0,0.00002,0.00,-0.00099,-0.10,0.00184,0.00,1 +5,Entry long,2025-03-03 10:00,M1,1.042,1,1.042,-0.0003,-0.03,0,0.00002,0.00,-0.00099,-0.10,0.00184,0.00,1 +6,Exit short,2025-03-03 12:00,M1,1.0418,1,1.0417,-0.0001,-0.01,0,0.00003,0.00,-0.00035,-0.03,0.00174,0.00,1 +6,Entry short,2025-03-03 11:00,M1,1.0417,1,1.0417,-0.0001,-0.01,0,0.00003,0.00,-0.00035,-0.03,0.00174,0.00,1 +7,Exit long,2025-03-03 13:00,M1,1.04144,1,1.0418,-0.00036,-0.03,0,0.00016,0.02,-0.00054,-0.05,0.00138,0.00,1 +7,Entry long,2025-03-03 12:00,M1,1.0418,1,1.0418,-0.00036,-0.03,0,0.00016,0.02,-0.00054,-0.05,0.00138,0.00,1 +8,Exit short,2025-03-03 14:00,M1,1.04084,1,1.04144,0.0006,0.06,0,0.00076,0.07,-0.00021,-0.02,0.00198,0.00,1 +8,Entry short,2025-03-03 13:00,M1,1.04144,1,1.04144,0.0006,0.06,0,0.00076,0.07,-0.00021,-0.02,0.00198,0.00,1 +9,Exit long,2025-03-03 15:00,M1,1.04057,1,1.04084,-0.00027,-0.03,0,0.00019,0.02,-0.00065,-0.06,0.00171,0.00,1 +9,Entry long,2025-03-03 14:00,M1,1.04084,1,1.04084,-0.00027,-0.03,0,0.00019,0.02,-0.00065,-0.06,0.00171,0.00,1 +10,Exit short,2025-03-03 16:00,M1,1.04012,1,1.04057,0.00045,0.04,0,0.00171,0.16,-0.00011,-0.01,0.00216,0.00,1 +10,Entry short,2025-03-03 15:00,M1,1.04057,1,1.04057,0.00045,0.04,0,0.00171,0.16,-0.00011,-0.01,0.00216,0.00,1 +11,Exit long,2025-03-03 17:00,M1,1.04272,1,1.04012,0.0026,0.25,0,0.00278,0.27,0,0.00,0.00476,0.00,1 +11,Entry long,2025-03-03 16:00,M1,1.04012,1,1.04012,0.0026,0.25,0,0.00278,0.27,0,0.00,0.00476,0.00,1 +12,Exit short,2025-03-03 18:00,M1,1.04422,1,1.04272,-0.0015,-0.14,0,0,0.00,-0.00176,-0.17,0.00326,0.00,1 +12,Entry short,2025-03-03 17:00,M1,1.04272,1,1.04272,-0.0015,-0.14,0,0,0.00,-0.00176,-0.17,0.00326,0.00,1 +13,Exit long,2025-03-03 19:00,M1,1.04526,1,1.04422,0.00104,0.10,0,0.00158,0.15,-0.00016,-0.02,0.0043,0.00,1 +13,Entry long,2025-03-03 18:00,M1,1.04422,1,1.04422,0.00104,0.10,0,0.00158,0.15,-0.00016,-0.02,0.0043,0.00,1 +14,Exit short,2025-03-03 20:00,M1,1.04705,1,1.04526,-0.00179,-0.17,0,0,0.00,-0.00237,-0.23,0.00251,0.00,1 +14,Entry short,2025-03-03 19:00,M1,1.04526,1,1.04526,-0.00179,-0.17,0,0,0.00,-0.00237,-0.23,0.00251,0.00,1 +15,Exit long,2025-03-03 21:00,M1,1.04719,1,1.04705,0.00014,0.01,0,0.00059,0.06,-0.0011,-0.11,0.00265,0.00,1 +15,Entry long,2025-03-03 20:00,M1,1.04705,1,1.04705,0.00014,0.01,0,0.00059,0.06,-0.0011,-0.11,0.00265,0.00,1 +16,Exit short,2025-03-03 22:00,M1,1.04824,1,1.04719,-0.00105,-0.10,0,0.00031,0.03,-0.00191,-0.18,0.0016,0.00,1 +16,Entry short,2025-03-03 21:00,M1,1.04719,1,1.04719,-0.00105,-0.10,0,0.00031,0.03,-0.00191,-0.18,0.0016,0.00,1 +17,Exit long,2025-03-03 23:00,M1,1.04846,1,1.04824,0.00022,0.02,0,0.00214,0.20,-0.00051,-0.05,0.00182,0.00,1 +17,Entry long,2025-03-03 22:00,M1,1.04824,1,1.04824,0.00022,0.02,0,0.00214,0.20,-0.00051,-0.05,0.00182,0.00,1 +18,Exit short,2025-03-04 00:00,M1,1.04962,1,1.04846,-0.00116,-0.11,0,0.00006,0.01,-0.00169,-0.16,0.00066,0.00,1 +18,Entry short,2025-03-03 23:00,M1,1.04846,1,1.04846,-0.00116,-0.11,0,0.00006,0.01,-0.00169,-0.16,0.00066,0.00,1 +19,Exit long,2025-03-04 01:00,M1,1.04904,1,1.04962,-0.00058,-0.06,0,0.00007,0.01,-0.00096,-0.09,0.00008,0.00,1 +19,Entry long,2025-03-04 00:00,M1,1.04962,1,1.04962,-0.00058,-0.06,0,0.00007,0.01,-0.00096,-0.09,0.00008,0.00,1 +20,Exit short,2025-03-04 02:00,M1,1.04872,1,1.04904,0.00032,0.03,0,0.00084,0.08,-0.00136,-0.13,0.0004,0.00,1 +20,Entry short,2025-03-04 01:00,M1,1.04904,1,1.04904,0.00032,0.03,0,0.00084,0.08,-0.00136,-0.13,0.0004,0.00,1 +21,Exit long,2025-03-04 03:00,M1,1.0473,1,1.04872,-0.00142,-0.14,0,0.00066,0.06,-0.00148,-0.14,-0.00102,0.00,1 +21,Entry long,2025-03-04 02:00,M1,1.04872,1,1.04872,-0.00142,-0.14,0,0.00066,0.06,-0.00148,-0.14,-0.00102,0.00,1 +22,Exit short,2025-03-04 04:00,M1,1.04823,1,1.0473,-0.00093,-0.09,0,0.00066,0.06,-0.00123,-0.12,-0.00195,0.00,1 +22,Entry short,2025-03-04 03:00,M1,1.0473,1,1.0473,-0.00093,-0.09,0,0.00066,0.06,-0.00123,-0.12,-0.00195,0.00,1 +23,Exit long,2025-03-04 05:00,M1,1.04882,1,1.04823,0.00059,0.06,0,0.00105,0.10,-0.00019,-0.02,-0.00136,0.00,1 +23,Entry long,2025-03-04 04:00,M1,1.04823,1,1.04823,0.00059,0.06,0,0.00105,0.10,-0.00019,-0.02,-0.00136,0.00,1 +24,Exit short,2025-03-04 06:00,M1,1.0488,1,1.04882,0.00002,0.00,0,0.00037,0.04,-0.00006,-0.01,-0.00134,0.00,1 +24,Entry short,2025-03-04 05:00,M1,1.04882,1,1.04882,0.00002,0.00,0,0.00037,0.04,-0.00006,-0.01,-0.00134,0.00,1 +25,Exit long,2025-03-04 07:00,M1,1.04856,1,1.0488,-0.00024,-0.02,0,0.00045,0.04,-0.00114,-0.11,-0.00158,0.00,1 +25,Entry long,2025-03-04 06:00,M1,1.0488,1,1.0488,-0.00024,-0.02,0,0.00045,0.04,-0.00114,-0.11,-0.00158,0.00,1 +26,Exit short,2025-03-04 08:00,M1,1.04882,1,1.04856,-0.00026,-0.02,0,0.00089,0.08,-0.00032,-0.03,-0.00184,0.00,1 +26,Entry short,2025-03-04 07:00,M1,1.04856,1,1.04856,-0.00026,-0.02,0,0.00089,0.08,-0.00032,-0.03,-0.00184,0.00,1 +27,Exit long,2025-03-04 09:00,M1,1.04884,1,1.04882,0.00002,0.00,0,0.0008,0.08,-0.00058,-0.06,-0.00182,0.00,1 +27,Entry long,2025-03-04 08:00,M1,1.04882,1,1.04882,0.00002,0.00,0,0.0008,0.08,-0.00058,-0.06,-0.00182,0.00,1 +28,Exit short,2025-03-04 10:00,M1,1.04808,1,1.04884,0.00076,0.07,0,0.0008,0.08,-0.00016,-0.02,-0.00106,0.00,1 +28,Entry short,2025-03-04 09:00,M1,1.04884,1,1.04884,0.00076,0.07,0,0.0008,0.08,-0.00016,-0.02,-0.00106,0.00,1 +29,Exit long,2025-03-04 11:00,M1,1.04772,1,1.04808,-0.00036,-0.03,0,0.0003,0.03,-0.00059,-0.06,-0.00142,0.00,1 +29,Entry long,2025-03-04 10:00,M1,1.04808,1,1.04808,-0.00036,-0.03,0,0.0003,0.03,-0.00059,-0.06,-0.00142,0.00,1 +30,Exit short,2025-03-04 12:00,M1,1.04782,1,1.04772,-0.0001,-0.01,0,0.00059,0.06,-0.00034,-0.03,-0.00152,0.00,1 +30,Entry short,2025-03-04 11:00,M1,1.04772,1,1.04772,-0.0001,-0.01,0,0.00059,0.06,-0.00034,-0.03,-0.00152,0.00,1 +31,Exit long,2025-03-04 13:00,M1,1.04837,1,1.04782,0.00055,0.05,0,0.00083,0.08,-0.00072,-0.07,-0.00097,0.00,1 +31,Entry long,2025-03-04 12:00,M1,1.04782,1,1.04782,0.00055,0.05,0,0.00083,0.08,-0.00072,-0.07,-0.00097,0.00,1 +32,Exit short,2025-03-04 14:00,M1,1.04861,1,1.04837,-0.00024,-0.02,0,0.00046,0.04,-0.00061,-0.06,-0.00121,0.00,1 +32,Entry short,2025-03-04 13:00,M1,1.04837,1,1.04837,-0.00024,-0.02,0,0.00046,0.04,-0.00061,-0.06,-0.00121,0.00,1 +33,Exit long,2025-03-04 15:00,M1,1.0493,1,1.04861,0.00069,0.07,0,0.00147,0.14,-0.00051,-0.05,-0.00052,0.00,1 +33,Entry long,2025-03-04 14:00,M1,1.04861,1,1.04861,0.00069,0.07,0,0.00147,0.14,-0.00051,-0.05,-0.00052,0.00,1 +34,Exit short,2025-03-04 16:00,M1,1.05188,1,1.0493,-0.00258,-0.25,0,0.00067,0.06,-0.0031,-0.30,-0.0031,0.00,1 +34,Entry short,2025-03-04 15:00,M1,1.0493,1,1.0493,-0.00258,-0.25,0,0.00067,0.06,-0.0031,-0.30,-0.0031,0.00,1 +35,Exit long,2025-03-04 17:00,M1,1.05122,1,1.05188,-0.00066,-0.06,0,0.00087,0.08,-0.00094,-0.09,-0.00376,0.00,1 +35,Entry long,2025-03-04 16:00,M1,1.05188,1,1.05188,-0.00066,-0.06,0,0.00087,0.08,-0.00094,-0.09,-0.00376,0.00,1 +36,Exit short,2025-03-04 18:00,M1,1.05114,1,1.05122,0.00008,0.01,0,0.00096,0.09,-0.00074,-0.07,-0.00368,0.00,1 +36,Entry short,2025-03-04 17:00,M1,1.05122,1,1.05122,0.00008,0.01,0,0.00096,0.09,-0.00074,-0.07,-0.00368,0.00,1 +37,Exit long,2025-03-04 19:00,M1,1.05442,1,1.05114,0.00328,0.31,0,0.00333,0.32,-0.00018,-0.02,-0.0004,0.00,1 +37,Entry long,2025-03-04 18:00,M1,1.05114,1,1.05114,0.00328,0.31,0,0.00333,0.32,-0.00018,-0.02,-0.0004,0.00,1 +38,Exit short,2025-03-04 20:00,M1,1.05532,1,1.05442,-0.0009,-0.09,0,0.00081,0.08,-0.00129,-0.12,-0.0013,0.00,1 +38,Entry short,2025-03-04 19:00,M1,1.05442,1,1.05442,-0.0009,-0.09,0,0.00081,0.08,-0.00129,-0.12,-0.0013,0.00,1 +39,Exit long,2025-03-04 21:00,M1,1.05391,1,1.05532,-0.00141,-0.13,0,0.00063,0.06,-0.00224,-0.21,-0.00271,0.00,1 +39,Entry long,2025-03-04 20:00,M1,1.05532,1,1.05532,-0.00141,-0.13,0,0.00063,0.06,-0.00224,-0.21,-0.00271,0.00,1 +40,Exit short,2025-03-04 22:00,M1,1.05214,1,1.05391,0.00177,0.17,0,0.00185,0.18,-0.00095,-0.09,-0.00094,0.00,1 +40,Entry short,2025-03-04 21:00,M1,1.05391,1,1.05391,0.00177,0.17,0,0.00185,0.18,-0.00095,-0.09,-0.00094,0.00,1 +41,Exit long,2025-03-04 23:00,M1,1.05266,1,1.05214,0.00052,0.05,0,0.00067,0.06,-0.0025,-0.24,-0.00042,0.00,1 +41,Entry long,2025-03-04 22:00,M1,1.05214,1,1.05214,0.00052,0.05,0,0.00067,0.06,-0.0025,-0.24,-0.00042,0.00,1 +42,Exit short,2025-03-05 00:00,M1,1.05338,1,1.05266,-0.00072,-0.07,0,0.00112,0.11,-0.00132,-0.13,-0.00114,0.00,1 +42,Entry short,2025-03-04 23:00,M1,1.05266,1,1.05266,-0.00072,-0.07,0,0.00112,0.11,-0.00132,-0.13,-0.00114,0.00,1 +43,Exit long,2025-03-05 01:00,M1,1.05418,1,1.05338,0.0008,0.08,0,0.00102,0.10,-0.00032,-0.03,-0.00034,0.00,1 +43,Entry long,2025-03-05 00:00,M1,1.05338,1,1.05338,0.0008,0.08,0,0.00102,0.10,-0.00032,-0.03,-0.00034,0.00,1 +44,Exit short,2025-03-05 02:00,M1,1.05907,1,1.05418,-0.00489,-0.46,0,0.0004,0.04,-0.00494,-0.47,-0.00523,0.00,1 +44,Entry short,2025-03-05 01:00,M1,1.05418,1,1.05418,-0.00489,-0.46,0,0.0004,0.04,-0.00494,-0.47,-0.00523,0.00,1 +45,Exit long,2025-03-05 03:00,M1,1.06008,1,1.05907,0.00101,0.10,0,0.00114,0.11,-0.00042,-0.04,-0.00422,0.00,1 +45,Entry long,2025-03-05 02:00,M1,1.05907,1,1.05907,0.00101,0.10,0,0.00114,0.11,-0.00042,-0.04,-0.00422,0.00,1 +46,Exit short,2025-03-05 04:00,M1,1.06085,1,1.06008,-0.00077,-0.07,0,0.00001,0.00,-0.00233,-0.22,-0.00499,0.00,1 +46,Entry short,2025-03-05 03:00,M1,1.06008,1,1.06008,-0.00077,-0.07,0,0.00001,0.00,-0.00233,-0.22,-0.00499,0.00,1 +47,Exit long,2025-03-05 05:00,M1,1.06258,1,1.06085,0.00173,0.16,0,0.00193,0.18,-0.00003,0.00,-0.00326,0.00,1 +47,Entry long,2025-03-05 04:00,M1,1.06085,1,1.06085,0.00173,0.16,0,0.00193,0.18,-0.00003,0.00,-0.00326,0.00,1 +48,Exit short,2025-03-05 06:00,M1,1.06202,1,1.06258,0.00056,0.05,0,0.00079,0.07,-0.00004,0.00,-0.0027,0.00,1 +48,Entry short,2025-03-05 05:00,M1,1.06258,1,1.06258,0.00056,0.05,0,0.00079,0.07,-0.00004,0.00,-0.0027,0.00,1 +49,Exit long,2025-03-05 07:00,M1,1.06232,1,1.06202,0.0003,0.03,0,0.0017,0.16,-0.00006,-0.01,-0.0024,0.00,1 +49,Entry long,2025-03-05 06:00,M1,1.06202,1,1.06202,0.0003,0.03,0,0.0017,0.16,-0.00006,-0.01,-0.0024,0.00,1 +50,Exit short,2025-03-05 08:00,M1,1.0621,1,1.06232,0.00022,0.02,0,0.00124,0.12,-0.00043,-0.04,-0.00218,0.00,1 +50,Entry short,2025-03-05 07:00,M1,1.06232,1,1.06232,0.00022,0.02,0,0.00124,0.12,-0.00043,-0.04,-0.00218,0.00,1 +51,Exit long,2025-03-05 09:00,M1,1.06127,1,1.0621,-0.00083,-0.08,0,0.00067,0.06,-0.00125,-0.12,-0.00301,0.00,1 +51,Entry long,2025-03-05 08:00,M1,1.0621,1,1.0621,-0.00083,-0.08,0,0.00067,0.06,-0.00125,-0.12,-0.00301,0.00,1 +52,Exit short,2025-03-05 10:00,M1,1.0604,1,1.06127,0.00087,0.08,0,0.00087,0.08,-0.00031,-0.03,-0.00214,0.00,1 +52,Entry short,2025-03-05 09:00,M1,1.06127,1,1.06127,0.00087,0.08,0,0.00087,0.08,-0.00031,-0.03,-0.00214,0.00,1 +53,Exit long,2025-03-05 11:00,M1,1.06182,1,1.0604,0.00142,0.13,0,0.00168,0.16,-0.0002,-0.02,-0.00072,0.00,1 +53,Entry long,2025-03-05 10:00,M1,1.0604,1,1.0604,0.00142,0.13,0,0.00168,0.16,-0.0002,-0.02,-0.00072,0.00,1 +54,Exit short,2025-03-05 12:00,M1,1.0619,1,1.06182,-0.00008,-0.01,0,0.00028,0.03,-0.0008,-0.08,-0.0008,0.00,1 +54,Entry short,2025-03-05 11:00,M1,1.06182,1,1.06182,-0.00008,-0.01,0,0.00028,0.03,-0.0008,-0.08,-0.0008,0.00,1 +55,Exit long,2025-03-05 13:00,M1,1.06316,1,1.0619,0.00126,0.12,0,0.00136,0.13,-0.00001,0.00,0.00046,0.00,1 +55,Entry long,2025-03-05 12:00,M1,1.0619,1,1.0619,0.00126,0.12,0,0.00136,0.13,-0.00001,0.00,0.00046,0.00,1 +56,Exit short,2025-03-05 14:00,M1,1.06324,1,1.06316,-0.00008,-0.01,0,0.00073,0.07,-0.00076,-0.07,0.00038,0.00,1 +56,Entry short,2025-03-05 13:00,M1,1.06316,1,1.06316,-0.00008,-0.01,0,0.00073,0.07,-0.00076,-0.07,0.00038,0.00,1 +57,Exit long,2025-03-05 15:00,M1,1.06744,1,1.06324,0.0042,0.40,0,0.00442,0.42,-0.00026,-0.02,0.00458,0.00,1 +57,Entry long,2025-03-05 14:00,M1,1.06324,1,1.06324,0.0042,0.40,0,0.00442,0.42,-0.00026,-0.02,0.00458,0.00,1 +58,Exit short,2025-03-05 16:00,M1,1.06981,1,1.06744,-0.00237,-0.22,0,0.00158,0.15,-0.00254,-0.24,0.00221,0.00,1 +58,Entry short,2025-03-05 15:00,M1,1.06744,1,1.06744,-0.00237,-0.22,0,0.00158,0.15,-0.00254,-0.24,0.00221,0.00,1 +59,Exit long,2025-03-05 17:00,M1,1.0712,1,1.06981,0.00139,0.13,0,0.00242,0.23,-0.00025,-0.02,0.0036,0.00,1 +59,Entry long,2025-03-05 16:00,M1,1.06981,1,1.06981,0.00139,0.13,0,0.00242,0.23,-0.00025,-0.02,0.0036,0.00,1 +60,Exit short,2025-03-05 18:00,M1,1.06972,1,1.0712,0.00148,0.14,0,0.00284,0.27,-0.00032,-0.03,0.00508,0.00,1 +60,Entry short,2025-03-05 17:00,M1,1.0712,1,1.0712,0.00148,0.14,0,0.00284,0.27,-0.00032,-0.03,0.00508,0.00,1 +61,Exit long,2025-03-05 19:00,M1,1.06789,1,1.06972,-0.00183,-0.17,0,0.00016,0.01,-0.00208,-0.19,0.00325,0.00,1 +61,Entry long,2025-03-05 18:00,M1,1.06972,1,1.06972,-0.00183,-0.17,0,0.00016,0.01,-0.00208,-0.19,0.00325,0.00,1 +62,Exit short,2025-03-05 20:00,M1,1.06958,1,1.06789,-0.00169,-0.16,0,0.00001,0.00,-0.00351,-0.33,0.00156,0.00,1 +62,Entry short,2025-03-05 19:00,M1,1.06789,1,1.06789,-0.00169,-0.16,0,0.00001,0.00,-0.00351,-0.33,0.00156,0.00,1 +63,Exit long,2025-03-05 21:00,M1,1.07158,1,1.06958,0.002,0.19,0,0.00272,0.25,-0.00089,-0.08,0.00356,0.00,1 +63,Entry long,2025-03-05 20:00,M1,1.06958,1,1.06958,0.002,0.19,0,0.00272,0.25,-0.00089,-0.08,0.00356,0.00,1 +64,Exit short,2025-03-05 22:00,M1,1.077,1,1.07158,-0.00542,-0.51,0,0.0002,0.02,-0.00612,-0.57,-0.00186,0.00,1 +64,Entry short,2025-03-05 21:00,M1,1.07158,1,1.07158,-0.00542,-0.51,0,0.0002,0.02,-0.00612,-0.57,-0.00186,0.00,1 +65,Exit long,2025-03-05 23:00,M1,1.07652,1,1.077,-0.00048,-0.04,0,0.00012,0.01,-0.00289,-0.27,-0.00234,0.00,1 +65,Entry long,2025-03-05 22:00,M1,1.077,1,1.077,-0.00048,-0.04,0,0.00012,0.01,-0.00289,-0.27,-0.00234,0.00,1 +66,Exit short,2025-03-06 00:00,M1,1.07671,1,1.07652,-0.00019,-0.02,0,0.0004,0.04,-0.00218,-0.20,-0.00253,0.00,1 +66,Entry short,2025-03-05 23:00,M1,1.07652,1,1.07652,-0.00019,-0.02,0,0.0004,0.04,-0.00218,-0.20,-0.00253,0.00,1 +67,Exit long,2025-03-06 01:00,M1,1.07851,1,1.07671,0.0018,0.17,0,0.00209,0.19,-0.00047,-0.04,-0.00073,0.00,1 +67,Entry long,2025-03-06 00:00,M1,1.07671,1,1.07671,0.0018,0.17,0,0.00209,0.19,-0.00047,-0.04,-0.00073,0.00,1 +68,Exit short,2025-03-06 02:00,M1,1.07737,1,1.07851,0.00114,0.11,0,0.00213,0.20,-0.00039,-0.04,0.00041,0.00,1 +68,Entry short,2025-03-06 01:00,M1,1.07851,1,1.07851,0.00114,0.11,0,0.00213,0.20,-0.00039,-0.04,0.00041,0.00,1 +69,Exit long,2025-03-06 03:00,M1,1.07902,1,1.07737,0.00165,0.15,0,0.00173,0.16,-0.00037,-0.03,0.00206,0.00,1 +69,Entry long,2025-03-06 02:00,M1,1.07737,1,1.07737,0.00165,0.15,0,0.00173,0.16,-0.00037,-0.03,0.00206,0.00,1 +70,Exit short,2025-03-06 04:00,M1,1.07922,1,1.07902,-0.0002,-0.02,0,0.00025,0.02,-0.00066,-0.06,0.00186,0.00,1 +70,Entry short,2025-03-06 03:00,M1,1.07902,1,1.07902,-0.0002,-0.02,0,0.00025,0.02,-0.00066,-0.06,0.00186,0.00,1 +71,Exit long,2025-03-06 05:00,M1,1.07896,1,1.07922,-0.00026,-0.02,0,0.00006,0.01,-0.00067,-0.06,0.0016,0.00,1 +71,Entry long,2025-03-06 04:00,M1,1.07922,1,1.07922,-0.00026,-0.02,0,0.00006,0.01,-0.00067,-0.06,0.0016,0.00,1 +72,Exit short,2025-03-06 06:00,M1,1.07908,1,1.07896,-0.00012,-0.01,0,0,0.00,-0.00036,-0.03,0.00148,0.00,1 +72,Entry short,2025-03-06 05:00,M1,1.07896,1,1.07896,-0.00012,-0.01,0,0,0.00,-0.00036,-0.03,0.00148,0.00,1 +73,Exit long,2025-03-06 07:00,M1,1.07942,1,1.07908,0.00034,0.03,0,0.00044,0.04,-0.00035,-0.03,0.00182,0.00,1 +73,Entry long,2025-03-06 06:00,M1,1.07908,1,1.07908,0.00034,0.03,0,0.00044,0.04,-0.00035,-0.03,0.00182,0.00,1 +74,Exit short,2025-03-06 08:00,M1,1.07944,1,1.07942,-0.00002,0.00,0,0.00068,0.06,-0.00088,-0.08,0.0018,0.00,1 +74,Entry short,2025-03-06 07:00,M1,1.07942,1,1.07942,-0.00002,0.00,0,0.00068,0.06,-0.00088,-0.08,0.0018,0.00,1 +75,Exit long,2025-03-06 09:00,M1,1.08152,1,1.07944,0.00208,0.19,0,0.0026,0.24,-0.00048,-0.04,0.00388,0.00,1 +75,Entry long,2025-03-06 08:00,M1,1.07944,1,1.07944,0.00208,0.19,0,0.0026,0.24,-0.00048,-0.04,0.00388,0.00,1 +76,Exit short,2025-03-06 10:00,M1,1.08052,1,1.08152,0.001,0.09,0,0.00173,0.16,-0.00042,-0.04,0.00488,0.00,1 +76,Entry short,2025-03-06 09:00,M1,1.08152,1,1.08152,0.001,0.09,0,0.00173,0.16,-0.00042,-0.04,0.00488,0.00,1 +77,Exit long,2025-03-06 11:00,M1,1.07896,1,1.08052,-0.00156,-0.14,0,0.00002,0.00,-0.0017,-0.16,0.00332,0.00,1 +77,Entry long,2025-03-06 10:00,M1,1.08052,1,1.08052,-0.00156,-0.14,0,0.00002,0.00,-0.0017,-0.16,0.00332,0.00,1 +78,Exit short,2025-03-06 12:00,M1,1.08036,1,1.07896,-0.0014,-0.13,0,0.00054,0.05,-0.00148,-0.14,0.00192,0.00,1 +78,Entry short,2025-03-06 11:00,M1,1.07896,1,1.07896,-0.0014,-0.13,0,0.00054,0.05,-0.00148,-0.14,0.00192,0.00,1 +79,Exit long,2025-03-06 13:00,M1,1.08104,1,1.08036,0.00068,0.06,0,0.00082,0.08,-0.00034,-0.03,0.0026,0.00,1 +79,Entry long,2025-03-06 12:00,M1,1.08036,1,1.08036,0.00068,0.06,0,0.00082,0.08,-0.00034,-0.03,0.0026,0.00,1 +80,Exit short,2025-03-06 14:00,M1,1.08028,1,1.08104,0.00076,0.07,0,0.00112,0.10,-0.00097,-0.09,0.00336,0.00,1 +80,Entry short,2025-03-06 13:00,M1,1.08104,1,1.08104,0.00076,0.07,0,0.00112,0.10,-0.00097,-0.09,0.00336,0.00,1 +81,Exit long,2025-03-06 15:00,M1,1.07992,1,1.08028,-0.00036,-0.03,0,0.00194,0.18,-0.00108,-0.10,0.003,0.00,1 +81,Entry long,2025-03-06 14:00,M1,1.08028,1,1.08028,-0.00036,-0.03,0,0.00194,0.18,-0.00108,-0.10,0.003,0.00,1 +82,Exit short,2025-03-06 16:00,M1,1.08012,1,1.07992,-0.0002,-0.02,0,0.00062,0.06,-0.00178,-0.16,0.0028,0.00,1 +82,Entry short,2025-03-06 15:00,M1,1.07992,1,1.07992,-0.0002,-0.02,0,0.00062,0.06,-0.00178,-0.16,0.0028,0.00,1 +83,Exit long,2025-03-06 17:00,M1,1.07999,1,1.08012,-0.00013,-0.01,0,0.00056,0.05,-0.00208,-0.19,0.00267,0.00,1 +83,Entry long,2025-03-06 16:00,M1,1.08012,1,1.08012,-0.00013,-0.01,0,0.00056,0.05,-0.00208,-0.19,0.00267,0.00,1 +84,Exit short,2025-03-06 18:00,M1,1.07919,1,1.07999,0.0008,0.07,0,0.00171,0.16,-0.00039,-0.04,0.00347,0.00,1 +84,Entry short,2025-03-06 17:00,M1,1.07999,1,1.07999,0.0008,0.07,0,0.00171,0.16,-0.00039,-0.04,0.00347,0.00,1 +85,Exit long,2025-03-06 19:00,M1,1.07993,1,1.07919,0.00074,0.07,0,0.00093,0.09,-0.00087,-0.08,0.00421,0.00,1 +85,Entry long,2025-03-06 18:00,M1,1.07919,1,1.07919,0.00074,0.07,0,0.00093,0.09,-0.00087,-0.08,0.00421,0.00,1 +86,Exit short,2025-03-06 20:00,M1,1.08002,1,1.07993,-0.00009,-0.01,0,0.00111,0.10,-0.0008,-0.07,0.00412,0.00,1 +86,Entry short,2025-03-06 19:00,M1,1.07993,1,1.07993,-0.00009,-0.01,0,0.00111,0.10,-0.0008,-0.07,0.00412,0.00,1 +87,Exit long,2025-03-06 21:00,M1,1.08262,1,1.08002,0.0026,0.24,0,0.00451,0.42,-0.0009,-0.08,0.00672,0.00,1 +87,Entry long,2025-03-06 20:00,M1,1.08002,1,1.08002,0.0026,0.24,0,0.00451,0.42,-0.0009,-0.08,0.00672,0.00,1 +88,Exit short,2025-03-06 22:00,M1,1.08372,1,1.08262,-0.0011,-0.10,0,0.0015,0.14,-0.00152,-0.14,0.00562,0.00,1 +88,Entry short,2025-03-06 21:00,M1,1.08262,1,1.08262,-0.0011,-0.10,0,0.0015,0.14,-0.00152,-0.14,0.00562,0.00,1 +89,Exit long,2025-03-06 23:00,M1,1.08172,1,1.08372,-0.002,-0.18,0,0.00164,0.15,-0.00258,-0.24,0.00362,0.00,1 +89,Entry long,2025-03-06 22:00,M1,1.08372,1,1.08372,-0.002,-0.18,0,0.00164,0.15,-0.00258,-0.24,0.00362,0.00,1 +90,Exit short,2025-03-07 00:00,M1,1.08175,1,1.08172,-0.00003,0.00,0,0.00018,0.02,-0.00203,-0.19,0.00359,0.00,1 +90,Entry short,2025-03-06 23:00,M1,1.08172,1,1.08172,-0.00003,0.00,0,0.00018,0.02,-0.00203,-0.19,0.00359,0.00,1 +91,Exit long,2025-03-07 01:00,M1,1.07971,1,1.08175,-0.00204,-0.19,0,0.00049,0.05,-0.00261,-0.24,0.00155,0.00,1 +91,Entry long,2025-03-07 00:00,M1,1.08175,1,1.08175,-0.00204,-0.19,0,0.00049,0.05,-0.00261,-0.24,0.00155,0.00,1 +92,Exit short,2025-03-07 02:00,M1,1.07838,1,1.07971,0.00133,0.12,0,0.00313,0.29,-0.00048,-0.04,0.00288,0.00,1 +92,Entry short,2025-03-07 01:00,M1,1.07971,1,1.07971,0.00133,0.12,0,0.00313,0.29,-0.00048,-0.04,0.00288,0.00,1 +93,Exit long,2025-03-07 03:00,M1,1.08018,1,1.07838,0.0018,0.17,0,0.00278,0.26,-0.0001,-0.01,0.00468,0.00,1 +93,Entry long,2025-03-07 02:00,M1,1.07838,1,1.07838,0.0018,0.17,0,0.00278,0.26,-0.0001,-0.01,0.00468,0.00,1 +94,Exit short,2025-03-07 04:00,M1,1.07868,1,1.08018,0.0015,0.14,0,0.00163,0.15,-0.00016,-0.01,0.00618,0.00,1 +94,Entry short,2025-03-07 03:00,M1,1.08018,1,1.08018,0.0015,0.14,0,0.00163,0.15,-0.00016,-0.01,0.00618,0.00,1 +95,Exit long,2025-03-07 05:00,M1,1.07854,1,1.07868,-0.00014,-0.01,0,0.00038,0.04,-0.00117,-0.11,0.00604,0.00,1 +95,Entry long,2025-03-07 04:00,M1,1.07868,1,1.07868,-0.00014,-0.01,0,0.00038,0.04,-0.00117,-0.11,0.00604,0.00,1 +96,Exit short,2025-03-07 06:00,M1,1.07917,1,1.07854,-0.00063,-0.06,0,0.00026,0.02,-0.00079,-0.07,0.00541,0.00,1 +96,Entry short,2025-03-07 05:00,M1,1.07854,1,1.07854,-0.00063,-0.06,0,0.00026,0.02,-0.00079,-0.07,0.00541,0.00,1 +97,Exit long,2025-03-07 07:00,M1,1.07875,1,1.07917,-0.00042,-0.04,0,0.00047,0.04,-0.00071,-0.07,0.00499,0.00,1 +97,Entry long,2025-03-07 06:00,M1,1.07917,1,1.07917,-0.00042,-0.04,0,0.00047,0.04,-0.00071,-0.07,0.00499,0.00,1 +98,Exit short,2025-03-07 08:00,M1,1.07876,1,1.07875,-0.00001,0.00,0,0.00006,0.01,-0.00081,-0.08,0.00498,0.00,1 +98,Entry short,2025-03-07 07:00,M1,1.07875,1,1.07875,-0.00001,0.00,0,0.00006,0.01,-0.00081,-0.08,0.00498,0.00,1 +99,Exit long,2025-03-07 09:00,M1,1.079,1,1.07876,0.00024,0.02,0,0.00053,0.05,-0.00062,-0.06,0.00522,0.00,1 +99,Entry long,2025-03-07 08:00,M1,1.07876,1,1.07876,0.00024,0.02,0,0.00053,0.05,-0.00062,-0.06,0.00522,0.00,1 +100,Exit short,2025-03-07 10:00,M1,1.07994,1,1.079,-0.00094,-0.09,0,0.00053,0.05,-0.00095,-0.09,0.00428,0.00,1 +100,Entry short,2025-03-07 09:00,M1,1.079,1,1.079,-0.00094,-0.09,0,0.00053,0.05,-0.00095,-0.09,0.00428,0.00,1 +101,Exit long,2025-03-07 11:00,M1,1.08058,1,1.07994,0.00064,0.06,0,0.00154,0.14,-0.00031,-0.03,0.00492,0.00,1 +101,Entry long,2025-03-07 10:00,M1,1.07994,1,1.07994,0.00064,0.06,0,0.00154,0.14,-0.00031,-0.03,0.00492,0.00,1 +102,Exit short,2025-03-07 12:00,M1,1.0813,1,1.08058,-0.00072,-0.07,0,0.00028,0.03,-0.00075,-0.07,0.0042,0.00,1 +102,Entry short,2025-03-07 11:00,M1,1.08058,1,1.08058,-0.00072,-0.07,0,0.00028,0.03,-0.00075,-0.07,0.0042,0.00,1 +103,Exit long,2025-03-07 13:00,M1,1.08134,1,1.0813,0.00004,0.00,0,0.00048,0.04,-0.00053,-0.05,0.00424,0.00,1 +103,Entry long,2025-03-07 12:00,M1,1.0813,1,1.0813,0.00004,0.00,0,0.00048,0.04,-0.00053,-0.05,0.00424,0.00,1 +104,Exit short,2025-03-07 14:00,M1,1.08237,1,1.08134,-0.00103,-0.10,0,0.00053,0.05,-0.00118,-0.11,0.00321,0.00,1 +104,Entry short,2025-03-07 13:00,M1,1.08134,1,1.08134,-0.00103,-0.10,0,0.00053,0.05,-0.00118,-0.11,0.00321,0.00,1 +105,Exit long,2025-03-07 15:00,M1,1.08299,1,1.08237,0.00062,0.06,0,0.00171,0.16,-0.00036,-0.03,0.00383,0.00,1 +105,Entry long,2025-03-07 14:00,M1,1.08237,1,1.08237,0.00062,0.06,0,0.00171,0.16,-0.00036,-0.03,0.00383,0.00,1 +106,Exit short,2025-03-07 16:00,M1,1.08508,1,1.08299,-0.00209,-0.19,0,0.00037,0.03,-0.00254,-0.23,0.00174,0.00,1 +106,Entry short,2025-03-07 15:00,M1,1.08299,1,1.08299,-0.00209,-0.19,0,0.00037,0.03,-0.00254,-0.23,0.00174,0.00,1 +107,Exit long,2025-03-07 17:00,M1,1.08614,1,1.08508,0.00106,0.10,0,0.00206,0.19,-0.00062,-0.06,0.0028,0.00,1 +107,Entry long,2025-03-07 16:00,M1,1.08508,1,1.08508,0.00106,0.10,0,0.00206,0.19,-0.00062,-0.06,0.0028,0.00,1 +108,Exit short,2025-03-07 18:00,M1,1.08607,1,1.08614,0.00007,0.01,0,0.00039,0.04,-0.00057,-0.05,0.00287,0.00,1 +108,Entry short,2025-03-07 17:00,M1,1.08614,1,1.08614,0.00007,0.01,0,0.00039,0.04,-0.00057,-0.05,0.00287,0.00,1 +109,Exit long,2025-03-07 19:00,M1,1.08495,1,1.08607,-0.00112,-0.10,0,0.00001,0.00,-0.00162,-0.15,0.00175,0.00,1 +109,Entry long,2025-03-07 18:00,M1,1.08607,1,1.08607,-0.00112,-0.10,0,0.00001,0.00,-0.00162,-0.15,0.00175,0.00,1 +110,Exit short,2025-03-07 20:00,M1,1.08546,1,1.08495,-0.00051,-0.05,0,0.00083,0.08,-0.00077,-0.07,0.00124,0.00,1 +110,Entry short,2025-03-07 19:00,M1,1.08495,1,1.08495,-0.00051,-0.05,0,0.00083,0.08,-0.00077,-0.07,0.00124,0.00,1 +111,Exit long,2025-03-07 21:00,M1,1.08445,1,1.08546,-0.00101,-0.09,0,0.00344,0.32,-0.00116,-0.11,0.00023,0.00,1 +111,Entry long,2025-03-07 20:00,M1,1.08546,1,1.08546,-0.00101,-0.09,0,0.00344,0.32,-0.00116,-0.11,0.00023,0.00,1 +112,Exit short,2025-03-07 22:00,M1,1.08537,1,1.08445,-0.00092,-0.08,0,0.00003,0.00,-0.00376,-0.35,-0.00069,0.00,1 +112,Entry short,2025-03-07 21:00,M1,1.08445,1,1.08445,-0.00092,-0.08,0,0.00003,0.00,-0.00376,-0.35,-0.00069,0.00,1 +113,Exit long,2025-03-07 23:00,M1,1.087,1,1.08537,0.00163,0.15,0,0.00335,0.31,-0.00181,-0.17,0.00094,0.00,1 +113,Entry long,2025-03-07 22:00,M1,1.08537,1,1.08537,0.00163,0.15,0,0.00335,0.31,-0.00181,-0.17,0.00094,0.00,1 +114,Exit short,2025-03-08 00:00,M1,1.08588,1,1.087,0.00112,0.10,0,0.00326,0.30,-0.00028,-0.03,0.00206,0.00,1 +114,Entry short,2025-03-07 23:00,M1,1.087,1,1.087,0.00112,0.10,0,0.00326,0.30,-0.00028,-0.03,0.00206,0.00,1 +115,Exit long,2025-03-08 01:00,M1,1.08434,1,1.08588,-0.00154,-0.14,0,0.00055,0.05,-0.00268,-0.25,0.00052,0.00,1 +115,Entry long,2025-03-08 00:00,M1,1.08588,1,1.08588,-0.00154,-0.14,0,0.00055,0.05,-0.00268,-0.25,0.00052,0.00,1 +116,Exit short,2025-03-08 02:00,M1,1.084,1,1.08434,0.00034,0.03,0,0.00177,0.16,-0.00012,-0.01,0.00086,0.00,1 +116,Entry short,2025-03-08 01:00,M1,1.08434,1,1.08434,0.00034,0.03,0,0.00177,0.16,-0.00012,-0.01,0.00086,0.00,1 +117,Exit long,2025-03-08 03:00,M1,1.08426,1,1.084,0.00026,0.02,0,0.00043,0.04,-0.00066,-0.06,0.00112,0.00,1 +117,Entry long,2025-03-08 02:00,M1,1.084,1,1.084,0.00026,0.02,0,0.00043,0.04,-0.00066,-0.06,0.00112,0.00,1 +118,Exit short,2025-03-08 04:00,M1,1.08474,1,1.08426,-0.00048,-0.04,0,0.00026,0.02,-0.00066,-0.06,0.00064,0.00,1 +118,Entry short,2025-03-08 03:00,M1,1.08426,1,1.08426,-0.00048,-0.04,0,0.00026,0.02,-0.00066,-0.06,0.00064,0.00,1 +119,Exit long,2025-03-08 05:00,M1,1.08345,1,1.08474,-0.00129,-0.12,0,0.0004,0.04,-0.00154,-0.14,-0.00065,0.00,1 +119,Entry long,2025-03-08 04:00,M1,1.08474,1,1.08474,-0.00129,-0.12,0,0.0004,0.04,-0.00154,-0.14,-0.00065,0.00,1 +120,Exit short,2025-03-10 05:00,M1,1.08404,1,1.08345,-0.00059,-0.05,0,0.00043,0.04,-0.00059,-0.05,-0.00124,0.00,1 +120,Entry short,2025-03-08 05:00,M1,1.08345,1,1.08345,-0.00059,-0.05,0,0.00043,0.04,-0.00059,-0.05,-0.00124,0.00,1 +121,Exit long,2025-03-10 06:00,M1,1.08496,1,1.08404,0.00092,0.08,0,0.00194,0.18,-0.00004,0.00,-0.00032,0.00,1 +121,Entry long,2025-03-10 05:00,M1,1.08404,1,1.08404,0.00092,0.08,0,0.00194,0.18,-0.00004,0.00,-0.00032,0.00,1 +122,Exit short,2025-03-10 07:00,M1,1.08602,1,1.08496,-0.00106,-0.10,0,0.00012,0.01,-0.00159,-0.15,-0.00138,0.00,1 +122,Entry short,2025-03-10 06:00,M1,1.08496,1,1.08496,-0.00106,-0.10,0,0.00012,0.01,-0.00159,-0.15,-0.00138,0.00,1 +123,Exit long,2025-03-10 08:00,M1,1.08642,1,1.08602,0.0004,0.04,0,0.00072,0.07,-0.00074,-0.07,-0.00098,0.00,1 +123,Entry long,2025-03-10 07:00,M1,1.08602,1,1.08602,0.0004,0.04,0,0.00072,0.07,-0.00074,-0.07,-0.00098,0.00,1 +124,Exit short,2025-03-10 09:00,M1,1.0853,1,1.08642,0.00112,0.10,0,0.00115,0.11,-0.00072,-0.07,0.00014,0.00,1 +124,Entry short,2025-03-10 08:00,M1,1.08642,1,1.08642,0.00112,0.10,0,0.00115,0.11,-0.00072,-0.07,0.00014,0.00,1 +125,Exit long,2025-03-10 10:00,M1,1.0837,1,1.0853,-0.0016,-0.15,0,0.00029,0.03,-0.00171,-0.16,-0.00146,0.00,1 +125,Entry long,2025-03-10 09:00,M1,1.0853,1,1.0853,-0.0016,-0.15,0,0.00029,0.03,-0.00171,-0.16,-0.00146,0.00,1 +126,Exit short,2025-03-10 11:00,M1,1.08317,1,1.0837,0.00053,0.05,0,0.00068,0.06,-0.00016,-0.01,-0.00093,0.00,1 +126,Entry short,2025-03-10 10:00,M1,1.0837,1,1.0837,0.00053,0.05,0,0.00068,0.06,-0.00016,-0.01,-0.00093,0.00,1 +127,Exit long,2025-03-10 12:00,M1,1.08306,1,1.08317,-0.00011,-0.01,0,0.00061,0.06,-0.00013,-0.01,-0.00104,0.00,1 +127,Entry long,2025-03-10 11:00,M1,1.08317,1,1.08317,-0.00011,-0.01,0,0.00061,0.06,-0.00013,-0.01,-0.00104,0.00,1 +128,Exit short,2025-03-10 13:00,M1,1.08351,1,1.08306,-0.00045,-0.04,0,0.00026,0.02,-0.00052,-0.05,-0.00149,0.00,1 +128,Entry short,2025-03-10 12:00,M1,1.08306,1,1.08306,-0.00045,-0.04,0,0.00026,0.02,-0.00052,-0.05,-0.00149,0.00,1 +129,Exit long,2025-03-10 14:00,M1,1.08374,1,1.08351,0.00023,0.02,0,0.00098,0.09,-0.00005,0.00,-0.00126,0.00,1 +129,Entry long,2025-03-10 13:00,M1,1.08351,1,1.08351,0.00023,0.02,0,0.00098,0.09,-0.00005,0.00,-0.00126,0.00,1 +130,Exit short,2025-03-10 15:00,M1,1.08228,1,1.08374,0.00146,0.13,0,0.00264,0.24,-0.00069,-0.06,0.0002,0.00,1 +130,Entry short,2025-03-10 14:00,M1,1.08374,1,1.08374,0.00146,0.13,0,0.00264,0.24,-0.00069,-0.06,0.0002,0.00,1 +131,Exit long,2025-03-10 16:00,M1,1.081,1,1.08228,-0.00128,-0.12,0,0.00018,0.02,-0.00178,-0.16,-0.00108,0.00,1 +131,Entry long,2025-03-10 15:00,M1,1.08228,1,1.08228,-0.00128,-0.12,0,0.00018,0.02,-0.00178,-0.16,-0.00108,0.00,1 +132,Exit short,2025-03-10 17:00,M1,1.08512,1,1.081,-0.00412,-0.38,0,0.00012,0.01,-0.00468,-0.43,-0.0052,0.00,1 +132,Entry short,2025-03-10 16:00,M1,1.081,1,1.081,-0.00412,-0.38,0,0.00012,0.01,-0.00468,-0.43,-0.0052,0.00,1 +133,Exit long,2025-03-10 18:00,M1,1.08611,1,1.08512,0.00099,0.09,0,0.00235,0.22,-0.00006,-0.01,-0.00421,0.00,1 +133,Entry long,2025-03-10 17:00,M1,1.08512,1,1.08512,0.00099,0.09,0,0.00235,0.22,-0.00006,-0.01,-0.00421,0.00,1 +134,Exit short,2025-03-10 19:00,M1,1.08507,1,1.08611,0.00104,0.10,0,0.00486,0.45,-0.00035,-0.03,-0.00317,0.00,1 +134,Entry short,2025-03-10 18:00,M1,1.08611,1,1.08611,0.00104,0.10,0,0.00486,0.45,-0.00035,-0.03,-0.00317,0.00,1 +135,Exit long,2025-03-10 20:00,M1,1.08439,1,1.08507,-0.00068,-0.06,0,0.00095,0.09,-0.00086,-0.08,-0.00385,0.00,1 +135,Entry long,2025-03-10 19:00,M1,1.08507,1,1.08507,-0.00068,-0.06,0,0.00095,0.09,-0.00086,-0.08,-0.00385,0.00,1 +136,Exit short,2025-03-10 21:00,M1,1.08352,1,1.08439,0.00087,0.08,0,0.00107,0.10,-0.00069,-0.06,-0.00298,0.00,1 +136,Entry short,2025-03-10 20:00,M1,1.08439,1,1.08439,0.00087,0.08,0,0.00107,0.10,-0.00069,-0.06,-0.00298,0.00,1 +137,Exit long,2025-03-10 22:00,M1,1.08452,1,1.08352,0.001,0.09,0,0.00116,0.11,-0.00036,-0.03,-0.00198,0.00,1 +137,Entry long,2025-03-10 21:00,M1,1.08352,1,1.08352,0.001,0.09,0,0.00116,0.11,-0.00036,-0.03,-0.00198,0.00,1 +138,Exit short,2025-03-10 23:00,M1,1.0837,1,1.08452,0.00082,0.08,0,0.00244,0.22,-0.00029,-0.03,-0.00116,0.00,1 +138,Entry short,2025-03-10 22:00,M1,1.08452,1,1.08452,0.00082,0.08,0,0.00244,0.22,-0.00029,-0.03,-0.00116,0.00,1 +139,Exit long,2025-03-11 00:00,M1,1.08304,1,1.0837,-0.00066,-0.06,0,0.0005,0.05,-0.00091,-0.08,-0.00182,0.00,1 +139,Entry long,2025-03-10 23:00,M1,1.0837,1,1.0837,-0.00066,-0.06,0,0.0005,0.05,-0.00091,-0.08,-0.00182,0.00,1 +140,Exit short,2025-03-11 01:00,M1,1.08298,1,1.08304,0.00006,0.01,0,0.00047,0.04,-0.00154,-0.14,-0.00176,0.00,1 +140,Entry short,2025-03-11 00:00,M1,1.08304,1,1.08304,0.00006,0.01,0,0.00047,0.04,-0.00154,-0.14,-0.00176,0.00,1 +141,Exit long,2025-03-11 02:00,M1,1.08297,1,1.08298,-0.00001,0.00,0,0.00056,0.05,-0.0012,-0.11,-0.00177,0.00,1 +141,Entry long,2025-03-11 01:00,M1,1.08298,1,1.08298,-0.00001,0.00,0,0.00056,0.05,-0.0012,-0.11,-0.00177,0.00,1 +142,Exit short,2025-03-11 03:00,M1,1.08318,1,1.08297,-0.00021,-0.02,0,0.00121,0.11,-0.00031,-0.03,-0.00198,0.00,1 +142,Entry short,2025-03-11 02:00,M1,1.08297,1,1.08297,-0.00021,-0.02,0,0.00121,0.11,-0.00031,-0.03,-0.00198,0.00,1 +143,Exit long,2025-03-11 04:00,M1,1.08362,1,1.08318,0.00044,0.04,0,0.00048,0.04,-0.0006,-0.06,-0.00154,0.00,1 +143,Entry long,2025-03-11 03:00,M1,1.08318,1,1.08318,0.00044,0.04,0,0.00048,0.04,-0.0006,-0.06,-0.00154,0.00,1 +144,Exit short,2025-03-11 05:00,M1,1.08353,1,1.08362,0.00009,0.01,0,0.00038,0.04,-0.00002,0.00,-0.00145,0.00,1 +144,Entry short,2025-03-11 04:00,M1,1.08362,1,1.08362,0.00009,0.01,0,0.00038,0.04,-0.00002,0.00,-0.00145,0.00,1 +145,Exit long,2025-03-11 06:00,M1,1.08348,1,1.08353,-0.00005,0.00,0,0.00064,0.06,-0.00021,-0.02,-0.0015,0.00,1 +145,Entry long,2025-03-11 05:00,M1,1.08353,1,1.08353,-0.00005,0.00,0,0.00064,0.06,-0.00021,-0.02,-0.0015,0.00,1 +146,Exit short,2025-03-11 07:00,M1,1.08394,1,1.08348,-0.00046,-0.04,0,0,0.00,-0.00088,-0.08,-0.00196,0.00,1 +146,Entry short,2025-03-11 06:00,M1,1.08348,1,1.08348,-0.00046,-0.04,0,0,0.00,-0.00088,-0.08,-0.00196,0.00,1 +147,Exit long,2025-03-11 08:00,M1,1.08515,1,1.08394,0.00121,0.11,0,0.00143,0.13,-0.00002,0.00,-0.00075,0.00,1 +147,Entry long,2025-03-11 07:00,M1,1.08394,1,1.08394,0.00121,0.11,0,0.00143,0.13,-0.00002,0.00,-0.00075,0.00,1 +148,Exit short,2025-03-11 09:00,M1,1.08582,1,1.08515,-0.00067,-0.06,0,0.00079,0.07,-0.00073,-0.07,-0.00142,0.00,1 +148,Entry short,2025-03-11 08:00,M1,1.08515,1,1.08515,-0.00067,-0.06,0,0.00079,0.07,-0.00073,-0.07,-0.00142,0.00,1 +149,Exit long,2025-03-11 10:00,M1,1.08559,1,1.08582,-0.00023,-0.02,0,0.00024,0.02,-0.00081,-0.07,-0.00165,0.00,1 +149,Entry long,2025-03-11 09:00,M1,1.08582,1,1.08582,-0.00023,-0.02,0,0.00024,0.02,-0.00081,-0.07,-0.00165,0.00,1 +150,Exit short,2025-03-11 11:00,M1,1.08544,1,1.08559,0.00015,0.01,0,0.00069,0.06,-0.0004,-0.04,-0.0015,0.00,1 +150,Entry short,2025-03-11 10:00,M1,1.08559,1,1.08559,0.00015,0.01,0,0.00069,0.06,-0.0004,-0.04,-0.0015,0.00,1 +151,Exit long,2025-03-11 12:00,M1,1.08545,1,1.08544,0.00001,0.00,0,0.00073,0.07,-0.00038,-0.04,-0.00149,0.00,1 +151,Entry long,2025-03-11 11:00,M1,1.08544,1,1.08544,0.00001,0.00,0,0.00073,0.07,-0.00038,-0.04,-0.00149,0.00,1 +152,Exit short,2025-03-11 13:00,M1,1.08384,1,1.08545,0.00161,0.15,0,0.00189,0.17,-0.00013,-0.01,0.00012,0.00,1 +152,Entry short,2025-03-11 12:00,M1,1.08545,1,1.08545,0.00161,0.15,0,0.00189,0.17,-0.00013,-0.01,0.00012,0.00,1 +153,Exit long,2025-03-11 14:00,M1,1.08566,1,1.08384,0.00182,0.17,0,0.00376,0.35,-0.00018,-0.02,0.00194,0.00,1 +153,Entry long,2025-03-11 13:00,M1,1.08384,1,1.08384,0.00182,0.17,0,0.00376,0.35,-0.00018,-0.02,0.00194,0.00,1 +154,Exit short,2025-03-11 15:00,M1,1.08907,1,1.08566,-0.00341,-0.31,0,0.00012,0.01,-0.00448,-0.41,-0.00147,0.00,1 +154,Entry short,2025-03-11 14:00,M1,1.08566,1,1.08566,-0.00341,-0.31,0,0.00012,0.01,-0.00448,-0.41,-0.00147,0.00,1 +155,Exit long,2025-03-11 16:00,M1,1.09172,1,1.08907,0.00265,0.24,0,0.00285,0.26,-0.00059,-0.05,0.00118,0.00,1 +155,Entry long,2025-03-11 15:00,M1,1.08907,1,1.08907,0.00265,0.24,0,0.00285,0.26,-0.00059,-0.05,0.00118,0.00,1 +156,Exit short,2025-03-11 17:00,M1,1.09085,1,1.09172,0.00087,0.08,0,0.00211,0.19,-0.00038,-0.03,0.00205,0.00,1 +156,Entry short,2025-03-11 16:00,M1,1.09172,1,1.09172,0.00087,0.08,0,0.00211,0.19,-0.00038,-0.03,0.00205,0.00,1 +157,Exit long,2025-03-11 18:00,M1,1.08906,1,1.09085,-0.00179,-0.16,0,0,0.00,-0.00261,-0.24,0.00026,0.00,1 +157,Entry long,2025-03-11 17:00,M1,1.09085,1,1.09085,-0.00179,-0.16,0,0,0.00,-0.00261,-0.24,0.00026,0.00,1 +158,Exit short,2025-03-11 19:00,M1,1.09046,1,1.08906,-0.0014,-0.13,0,0.00094,0.09,-0.00142,-0.13,-0.00114,0.00,1 +158,Entry short,2025-03-11 18:00,M1,1.08906,1,1.08906,-0.0014,-0.13,0,0.00094,0.09,-0.00142,-0.13,-0.00114,0.00,1 +159,Exit long,2025-03-11 20:00,M1,1.0918,1,1.09046,0.00134,0.12,0,0.0016,0.15,-0.00024,-0.02,0.0002,0.00,1 +159,Entry long,2025-03-11 19:00,M1,1.09046,1,1.09046,0.00134,0.12,0,0.0016,0.15,-0.00024,-0.02,0.0002,0.00,1 +160,Exit short,2025-03-11 21:00,M1,1.09201,1,1.0918,-0.00021,-0.02,0,0.00064,0.06,-0.00128,-0.12,-0.00001,0.00,1 +160,Entry short,2025-03-11 20:00,M1,1.0918,1,1.0918,-0.00021,-0.02,0,0.00064,0.06,-0.00128,-0.12,-0.00001,0.00,1 +161,Exit long,2025-03-11 22:00,M1,1.09194,1,1.09201,-0.00007,-0.01,0,0.00003,0.00,-0.00251,-0.23,-0.00008,0.00,1 +161,Entry long,2025-03-11 21:00,M1,1.09201,1,1.09201,-0.00007,-0.01,0,0.00003,0.00,-0.00251,-0.23,-0.00008,0.00,1 +162,Exit short,2025-03-11 23:00,M1,1.09272,1,1.09194,-0.00078,-0.07,0,0.00071,0.07,-0.00094,-0.09,-0.00086,0.00,1 +162,Entry short,2025-03-11 22:00,M1,1.09194,1,1.09194,-0.00078,-0.07,0,0.00071,0.07,-0.00094,-0.09,-0.00086,0.00,1 +163,Exit long,2025-03-12 00:00,M1,1.09252,1,1.09272,-0.0002,-0.02,0,0.00025,0.02,-0.00092,-0.08,-0.00106,0.00,1 +163,Entry long,2025-03-11 23:00,M1,1.09272,1,1.09272,-0.0002,-0.02,0,0.00025,0.02,-0.00092,-0.08,-0.00106,0.00,1 +164,Exit short,2025-03-12 01:00,M1,1.09342,1,1.09252,-0.0009,-0.08,0,0.00012,0.01,-0.00098,-0.09,-0.00196,0.00,1 +164,Entry short,2025-03-12 00:00,M1,1.09252,1,1.09252,-0.0009,-0.08,0,0.00012,0.01,-0.00098,-0.09,-0.00196,0.00,1 +165,Exit long,2025-03-12 02:00,M1,1.0931,1,1.09342,-0.00032,-0.03,0,0.0013,0.12,-0.0009,-0.08,-0.00228,0.00,1 +165,Entry long,2025-03-12 01:00,M1,1.09342,1,1.09342,-0.00032,-0.03,0,0.0013,0.12,-0.0009,-0.08,-0.00228,0.00,1 +166,Exit short,2025-03-12 03:00,M1,1.092,1,1.0931,0.0011,0.10,0,0.00114,0.10,-0.00031,-0.03,-0.00118,0.00,1 +166,Entry short,2025-03-12 02:00,M1,1.0931,1,1.0931,0.0011,0.10,0,0.00114,0.10,-0.00031,-0.03,-0.00118,0.00,1 +167,Exit long,2025-03-12 04:00,M1,1.09194,1,1.092,-0.00006,-0.01,0,0.0001,0.01,-0.00098,-0.09,-0.00124,0.00,1 +167,Entry long,2025-03-12 03:00,M1,1.092,1,1.092,-0.00006,-0.01,0,0.0001,0.01,-0.00098,-0.09,-0.00124,0.00,1 +168,Exit short,2025-03-12 05:00,M1,1.09116,1,1.09194,0.00078,0.07,0,0.00088,0.08,-0.00014,-0.01,-0.00046,0.00,1 +168,Entry short,2025-03-12 04:00,M1,1.09194,1,1.09194,0.00078,0.07,0,0.00088,0.08,-0.00014,-0.01,-0.00046,0.00,1 +169,Exit long,2025-03-12 06:00,M1,1.09158,1,1.09116,0.00042,0.04,0,0.00074,0.07,-0.00006,-0.01,-0.00004,0.00,1 +169,Entry long,2025-03-12 05:00,M1,1.09116,1,1.09116,0.00042,0.04,0,0.00074,0.07,-0.00006,-0.01,-0.00004,0.00,1 +170,Exit short,2025-03-12 07:00,M1,1.09142,1,1.09158,0.00016,0.01,0,0.00026,0.02,-0.0002,-0.02,0.00012,0.00,1 +170,Entry short,2025-03-12 06:00,M1,1.09158,1,1.09158,0.00016,0.01,0,0.00026,0.02,-0.0002,-0.02,0.00012,0.00,1 +171,Exit long,2025-03-12 08:00,M1,1.09088,1,1.09142,-0.00054,-0.05,0,0.00016,0.01,-0.00085,-0.08,-0.00042,0.00,1 +171,Entry long,2025-03-12 07:00,M1,1.09142,1,1.09142,-0.00054,-0.05,0,0.00016,0.01,-0.00085,-0.08,-0.00042,0.00,1 +172,Exit short,2025-03-12 09:00,M1,1.09066,1,1.09088,0.00022,0.02,0,0.00027,0.02,-0.00066,-0.06,-0.0002,0.00,1 +172,Entry short,2025-03-12 08:00,M1,1.09088,1,1.09088,0.00022,0.02,0,0.00027,0.02,-0.00066,-0.06,-0.0002,0.00,1 +173,Exit long,2025-03-12 10:00,M1,1.09038,1,1.09066,-0.00028,-0.03,0,0.0005,0.05,-0.00034,-0.03,-0.00048,0.00,1 +173,Entry long,2025-03-12 09:00,M1,1.09066,1,1.09066,-0.00028,-0.03,0,0.0005,0.05,-0.00034,-0.03,-0.00048,0.00,1 +174,Exit short,2025-03-12 11:00,M1,1.08974,1,1.09038,0.00064,0.06,0,0.00076,0.07,-0.00011,-0.01,0.00016,0.00,1 +174,Entry short,2025-03-12 10:00,M1,1.09038,1,1.09038,0.00064,0.06,0,0.00076,0.07,-0.00011,-0.01,0.00016,0.00,1 +175,Exit long,2025-03-12 12:00,M1,1.08988,1,1.08974,0.00014,0.01,0,0.00053,0.05,-0.00016,-0.01,0.0003,0.00,1 +175,Entry long,2025-03-12 11:00,M1,1.08974,1,1.08974,0.00014,0.01,0,0.00053,0.05,-0.00016,-0.01,0.0003,0.00,1 +176,Exit short,2025-03-12 13:00,M1,1.09036,1,1.08988,-0.00048,-0.04,0,0.00036,0.03,-0.00056,-0.05,-0.00018,0.00,1 +176,Entry short,2025-03-12 12:00,M1,1.08988,1,1.08988,-0.00048,-0.04,0,0.00036,0.03,-0.00056,-0.05,-0.00018,0.00,1 +177,Exit long,2025-03-12 14:00,M1,1.08922,1,1.09036,-0.00114,-0.10,0,0.00054,0.05,-0.00116,-0.11,-0.00132,0.00,1 +177,Entry long,2025-03-12 13:00,M1,1.09036,1,1.09036,-0.00114,-0.10,0,0.00054,0.05,-0.00116,-0.11,-0.00132,0.00,1 +178,Exit short,2025-03-12 15:00,M1,1.09017,1,1.08922,-0.00095,-0.09,0,0.00048,0.04,-0.0011,-0.10,-0.00227,0.00,1 +178,Entry short,2025-03-12 14:00,M1,1.08922,1,1.08922,-0.00095,-0.09,0,0.00048,0.04,-0.0011,-0.10,-0.00227,0.00,1 +179,Exit long,2025-03-12 16:00,M1,1.09216,1,1.09017,0.00199,0.18,0,0.00201,0.18,-0.00039,-0.04,-0.00028,0.00,1 +179,Entry long,2025-03-12 15:00,M1,1.09017,1,1.09017,0.00199,0.18,0,0.00201,0.18,-0.00039,-0.04,-0.00028,0.00,1 +180,Exit short,2025-03-12 17:00,M1,1.09145,1,1.09216,0.00071,0.07,0,0.00082,0.08,-0.0005,-0.05,0.00043,0.00,1 +180,Entry short,2025-03-12 16:00,M1,1.09216,1,1.09216,0.00071,0.07,0,0.00082,0.08,-0.0005,-0.05,0.00043,0.00,1 +181,Exit long,2025-03-12 18:00,M1,1.09064,1,1.09145,-0.00081,-0.07,0,0.00065,0.06,-0.00099,-0.09,-0.00038,0.00,1 +181,Entry long,2025-03-12 17:00,M1,1.09145,1,1.09145,-0.00081,-0.07,0,0.00065,0.06,-0.00099,-0.09,-0.00038,0.00,1 +182,Exit short,2025-03-12 19:00,M1,1.0901,1,1.09064,0.00054,0.05,0,0.0007,0.06,-0.00065,-0.06,0.00016,0.00,1 +182,Entry short,2025-03-12 18:00,M1,1.09064,1,1.09064,0.00054,0.05,0,0.0007,0.06,-0.00065,-0.06,0.00016,0.00,1 +183,Exit long,2025-03-12 20:00,M1,1.0878,1,1.0901,-0.0023,-0.21,0,0.00312,0.29,-0.00248,-0.23,-0.00214,0.00,1 +183,Entry long,2025-03-12 19:00,M1,1.0901,1,1.0901,-0.0023,-0.21,0,0.00312,0.29,-0.00248,-0.23,-0.00214,0.00,1 +184,Exit short,2025-03-12 21:00,M1,1.08824,1,1.0878,-0.00044,-0.04,0,0.00023,0.02,-0.00184,-0.17,-0.00258,0.00,1 +184,Entry short,2025-03-12 20:00,M1,1.0878,1,1.0878,-0.00044,-0.04,0,0.00023,0.02,-0.00184,-0.17,-0.00258,0.00,1 +185,Exit long,2025-03-12 22:00,M1,1.0913,1,1.08824,0.00306,0.28,0,0.00313,0.29,-0.00012,-0.01,0.00048,0.00,1 +185,Entry long,2025-03-12 21:00,M1,1.08824,1,1.08824,0.00306,0.28,0,0.00313,0.29,-0.00012,-0.01,0.00048,0.00,1 +186,Exit short,2025-03-12 23:00,M1,1.09244,1,1.0913,-0.00114,-0.10,0,0.0003,0.03,-0.00114,-0.10,-0.00066,0.00,1 +186,Entry short,2025-03-12 22:00,M1,1.0913,1,1.0913,-0.00114,-0.10,0,0.0003,0.03,-0.00114,-0.10,-0.00066,0.00,1 +187,Exit long,2025-03-13 00:00,M1,1.09058,1,1.09244,-0.00186,-0.17,0,0.00036,0.03,-0.00187,-0.17,-0.00252,0.00,1 +187,Entry long,2025-03-12 23:00,M1,1.09244,1,1.09244,-0.00186,-0.17,0,0.00036,0.03,-0.00187,-0.17,-0.00252,0.00,1 +188,Exit short,2025-03-13 01:00,M1,1.0907,1,1.09058,-0.00012,-0.01,0,0.00024,0.02,-0.00085,-0.08,-0.00264,0.00,1 +188,Entry short,2025-03-13 00:00,M1,1.09058,1,1.09058,-0.00012,-0.01,0,0.00024,0.02,-0.00085,-0.08,-0.00264,0.00,1 +189,Exit long,2025-03-13 02:00,M1,1.08846,1,1.0907,-0.00224,-0.21,0,0.00026,0.02,-0.00272,-0.25,-0.00488,0.00,1 +189,Entry long,2025-03-13 01:00,M1,1.0907,1,1.0907,-0.00224,-0.21,0,0.00026,0.02,-0.00272,-0.25,-0.00488,0.00,1 +190,Exit short,2025-03-13 03:00,M1,1.08902,1,1.08846,-0.00056,-0.05,0,0.00014,0.01,-0.00133,-0.12,-0.00544,0.00,1 +190,Entry short,2025-03-13 02:00,M1,1.08846,1,1.08846,-0.00056,-0.05,0,0.00014,0.01,-0.00133,-0.12,-0.00544,0.00,1 +191,Exit long,2025-03-13 04:00,M1,1.08875,1,1.08902,-0.00027,-0.02,0,0.00034,0.03,-0.00036,-0.03,-0.00571,0.00,1 +191,Entry long,2025-03-13 03:00,M1,1.08902,1,1.08902,-0.00027,-0.02,0,0.00034,0.03,-0.00036,-0.03,-0.00571,0.00,1 +192,Exit short,2025-03-13 05:00,M1,1.0887,1,1.08875,0.00005,0.00,0,0.00007,0.01,-0.00031,-0.03,-0.00566,0.00,1 +192,Entry short,2025-03-13 04:00,M1,1.08875,1,1.08875,0.00005,0.00,0,0.00007,0.01,-0.00031,-0.03,-0.00566,0.00,1 +193,Exit long,2025-03-13 06:00,M1,1.08892,1,1.0887,0.00022,0.02,0,0.0003,0.03,0,0.00,-0.00544,0.00,1 +193,Entry long,2025-03-13 05:00,M1,1.0887,1,1.0887,0.00022,0.02,0,0.0003,0.03,0,0.00,-0.00544,0.00,1 +194,Exit short,2025-03-13 07:00,M1,1.08854,1,1.08892,0.00038,0.03,0,0.00058,0.05,-0.00008,-0.01,-0.00506,0.00,1 +194,Entry short,2025-03-13 06:00,M1,1.08892,1,1.08892,0.00038,0.03,0,0.00058,0.05,-0.00008,-0.01,-0.00506,0.00,1 +195,Exit long,2025-03-13 08:00,M1,1.08916,1,1.08854,0.00062,0.06,0,0.00065,0.06,-0.00028,-0.03,-0.00444,0.00,1 +195,Entry long,2025-03-13 07:00,M1,1.08854,1,1.08854,0.00062,0.06,0,0.00065,0.06,-0.00028,-0.03,-0.00444,0.00,1 +196,Exit short,2025-03-13 09:00,M1,1.08892,1,1.08916,0.00024,0.02,0,0.00052,0.05,-0.00018,-0.02,-0.0042,0.00,1 +196,Entry short,2025-03-13 08:00,M1,1.08916,1,1.08916,0.00024,0.02,0,0.00052,0.05,-0.00018,-0.02,-0.0042,0.00,1 +197,Exit long,2025-03-13 10:00,M1,1.08956,1,1.08892,0.00064,0.06,0,0.00081,0.07,-0.00002,0.00,-0.00356,0.00,1 +197,Entry long,2025-03-13 09:00,M1,1.08892,1,1.08892,0.00064,0.06,0,0.00081,0.07,-0.00002,0.00,-0.00356,0.00,1 +198,Exit short,2025-03-13 11:00,M1,1.08851,1,1.08956,0.00105,0.10,0,0.00105,0.10,-0.00001,0.00,-0.00251,0.00,1 +198,Entry short,2025-03-13 10:00,M1,1.08956,1,1.08956,0.00105,0.10,0,0.00105,0.10,-0.00001,0.00,-0.00251,0.00,1 +199,Exit long,2025-03-13 12:00,M1,1.08935,1,1.08851,0.00084,0.08,0,0.00085,0.08,-0.00033,-0.03,-0.00167,0.00,1 +199,Entry long,2025-03-13 11:00,M1,1.08851,1,1.08851,0.00084,0.08,0,0.00085,0.08,-0.00033,-0.03,-0.00167,0.00,1 +200,Exit short,2025-03-13 13:00,M1,1.08852,1,1.08935,0.00083,0.08,0,0.00141,0.13,-0.00001,0.00,-0.00084,0.00,1 +200,Entry short,2025-03-13 12:00,M1,1.08935,1,1.08935,0.00083,0.08,0,0.00141,0.13,-0.00001,0.00,-0.00084,0.00,1 +201,Exit long,2025-03-13 14:00,M1,1.08746,1,1.08852,-0.00106,-0.10,0,0.00004,0.00,-0.0016,-0.15,-0.0019,0.00,1 +201,Entry long,2025-03-13 13:00,M1,1.08852,1,1.08852,-0.00106,-0.10,0,0.00004,0.00,-0.0016,-0.15,-0.0019,0.00,1 +202,Exit short,2025-03-13 15:00,M1,1.08654,1,1.08746,0.00092,0.08,0,0.00148,0.14,-0.00106,-0.10,-0.00098,0.00,1 +202,Entry short,2025-03-13 14:00,M1,1.08746,1,1.08746,0.00092,0.08,0,0.00148,0.14,-0.00106,-0.10,-0.00098,0.00,1 +203,Exit long,2025-03-13 16:00,M1,1.08732,1,1.08654,0.00078,0.07,0,0.00188,0.17,-0.00062,-0.06,-0.0002,0.00,1 +203,Entry long,2025-03-13 15:00,M1,1.08654,1,1.08654,0.00078,0.07,0,0.00188,0.17,-0.00062,-0.06,-0.0002,0.00,1 +204,Exit short,2025-03-13 17:00,M1,1.08794,1,1.08732,-0.00062,-0.06,0,0.00044,0.04,-0.00122,-0.11,-0.00082,0.00,1 +204,Entry short,2025-03-13 16:00,M1,1.08732,1,1.08732,-0.00062,-0.06,0,0.00044,0.04,-0.00122,-0.11,-0.00082,0.00,1 +205,Exit long,2025-03-13 18:00,M1,1.08702,1,1.08794,-0.00092,-0.08,0,0.00036,0.03,-0.00169,-0.16,-0.00174,0.00,1 +205,Entry long,2025-03-13 17:00,M1,1.08794,1,1.08794,-0.00092,-0.08,0,0.00036,0.03,-0.00169,-0.16,-0.00174,0.00,1 +206,Exit short,2025-03-13 19:00,M1,1.08564,1,1.08702,0.00138,0.13,0,0.00258,0.24,-0.00048,-0.04,-0.00036,0.00,1 +206,Entry short,2025-03-13 18:00,M1,1.08702,1,1.08702,0.00138,0.13,0,0.00258,0.24,-0.00048,-0.04,-0.00036,0.00,1 +207,Exit long,2025-03-13 20:00,M1,1.08349,1,1.08564,-0.00215,-0.20,0,0.00222,0.20,-0.00302,-0.28,-0.00251,0.00,1 +207,Entry long,2025-03-13 19:00,M1,1.08564,1,1.08564,-0.00215,-0.20,0,0.00222,0.20,-0.00302,-0.28,-0.00251,0.00,1 +208,Exit short,2025-03-13 21:00,M1,1.08474,1,1.08349,-0.00125,-0.12,0,0.00123,0.11,-0.00183,-0.17,-0.00376,0.00,1 +208,Entry short,2025-03-13 20:00,M1,1.08349,1,1.08349,-0.00125,-0.12,0,0.00123,0.11,-0.00183,-0.17,-0.00376,0.00,1 +209,Exit long,2025-03-13 22:00,M1,1.08652,1,1.08474,0.00178,0.16,0,0.00224,0.21,-0.00012,-0.01,-0.00198,0.00,1 +209,Entry long,2025-03-13 21:00,M1,1.08474,1,1.08474,0.00178,0.16,0,0.00224,0.21,-0.00012,-0.01,-0.00198,0.00,1 +210,Exit short,2025-03-13 23:00,M1,1.08585,1,1.08652,0.00067,0.06,0,0.00112,0.10,-0.00068,-0.06,-0.00131,0.00,1 +210,Entry short,2025-03-13 22:00,M1,1.08652,1,1.08652,0.00067,0.06,0,0.00112,0.10,-0.00068,-0.06,-0.00131,0.00,1 +211,Exit long,2025-03-14 00:00,M1,1.08726,1,1.08585,0.00141,0.13,0,0.00197,0.18,-0.00033,-0.03,0.0001,0.00,1 +211,Entry long,2025-03-13 23:00,M1,1.08585,1,1.08585,0.00141,0.13,0,0.00197,0.18,-0.00033,-0.03,0.0001,0.00,1 +212,Exit short,2025-03-14 01:00,M1,1.08546,1,1.08726,0.0018,0.17,0,0.00182,0.17,-0.00009,-0.01,0.0019,0.00,1 +212,Entry short,2025-03-14 00:00,M1,1.08726,1,1.08726,0.0018,0.17,0,0.00182,0.17,-0.00009,-0.01,0.0019,0.00,1 +213,Exit long,2025-03-14 02:00,M1,1.08536,1,1.08546,-0.0001,-0.01,0,0.00035,0.03,-0.00025,-0.02,0.0018,0.00,1 +213,Entry long,2025-03-14 01:00,M1,1.08546,1,1.08546,-0.0001,-0.01,0,0.00035,0.03,-0.00025,-0.02,0.0018,0.00,1 +214,Exit short,2025-03-14 03:00,M1,1.08484,1,1.08536,0.00052,0.05,0,0.00058,0.05,-0.00034,-0.03,0.00232,0.00,1 +214,Entry short,2025-03-14 02:00,M1,1.08536,1,1.08536,0.00052,0.05,0,0.00058,0.05,-0.00034,-0.03,0.00232,0.00,1 +215,Exit long,2025-03-14 04:00,M1,1.08524,1,1.08484,0.0004,0.04,0,0.00076,0.07,-0.0002,-0.02,0.00272,0.00,1 +215,Entry long,2025-03-14 03:00,M1,1.08484,1,1.08484,0.0004,0.04,0,0.00076,0.07,-0.0002,-0.02,0.00272,0.00,1 +216,Exit short,2025-03-14 05:00,M1,1.08528,1,1.08524,-0.00004,0.00,0,0.00035,0.03,-0.00006,-0.01,0.00268,0.00,1 +216,Entry short,2025-03-14 04:00,M1,1.08524,1,1.08524,-0.00004,0.00,0,0.00035,0.03,-0.00006,-0.01,0.00268,0.00,1 +217,Exit long,2025-03-14 06:00,M1,1.08526,1,1.08528,-0.00002,0.00,0,0.0003,0.03,-0.00015,-0.01,0.00266,0.00,1 +217,Entry long,2025-03-14 05:00,M1,1.08528,1,1.08528,-0.00002,0.00,0,0.0003,0.03,-0.00015,-0.01,0.00266,0.00,1 +218,Exit short,2025-03-14 07:00,M1,1.0855,1,1.08526,-0.00024,-0.02,0,0.00024,0.02,-0.00025,-0.02,0.00242,0.00,1 +218,Entry short,2025-03-14 06:00,M1,1.08526,1,1.08526,-0.00024,-0.02,0,0.00024,0.02,-0.00025,-0.02,0.00242,0.00,1 +219,Exit long,2025-03-14 08:00,M1,1.08492,1,1.0855,-0.00058,-0.05,0,0.00036,0.03,-0.00066,-0.06,0.00184,0.00,1 +219,Entry long,2025-03-14 07:00,M1,1.0855,1,1.0855,-0.00058,-0.05,0,0.00036,0.03,-0.00066,-0.06,0.00184,0.00,1 +220,Exit short,2025-03-14 08:00,,1.08492,1,1.08492,0,0.00,0,0,0.00,0,0.00,0.00184,0.00,0 +220,Entry short,2025-03-14 08:00,M1,1.08492,1,1.08492,0,0.00,0,0,0.00,0,0.00,0.00184,0.00,0 \ No newline at end of file diff --git a/tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/meta.json b/tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/meta.json new file mode 100644 index 00000000..975d8d47 --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/meta.json @@ -0,0 +1,16 @@ +{ + "title": "HM G2-36 session.ismarket", + "slug": "hm-g236-xauusd-60-dst-mar", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:01:37.880Z", + "chart": { + "symbol": "OANDA:XAUUSD", + "interval": "60", + "from": "2025-03-02", + "to": "2025-03-14" + }, + "pine_sha256": "7c5b7e36130c230e6ad26f5525ab527486a008e2d6eaa1a5e41da41cfa256a1d", + "tv_trades_sha256": "ebc226aea010125fb66b7cda3a86563846a243463723816ee529f8f044c54cf6", + "tv_trades_rows": 420 +} diff --git a/tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/metrics.json b/tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/metrics.json new file mode 100644 index 00000000..b0900169 --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-g236-xauusd-60-dst-mar", + "symbol": "OANDA:XAUUSD", + "interval": "60", + "from": "2025-03-02", + "to": "2025-03-14", + "deepBacktesting": true, + "trades": 210, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "7c5b7e36130c230e6ad26f5525ab527486a008e2d6eaa1a5e41da41cfa256a1d", + "tvTradesCsvHash": "ebc226aea010125fb66b7cda3a86563846a243463723816ee529f8f044c54cf6", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74408", + "symbol": "OANDA:XAUUSD", + "interval": "60", + "requestedRange": { + "from": 1740873600000, + "to": 1741910400000 + }, + "returnedRange": { + "from": 1740956400000, + "to": 1741910400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "4bfa1862871ea73feba76e6cd040ee46bf04ae44f59bc0c98471da3d0e74b360", + "decodedReportSha256": "74c6c1b3fff68fef5fb443924afb03fdca0a3216cafdbcab7b4b7df469df2913", + "closedTrades": 210, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:01:37.879Z" +} diff --git a/tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/strategy.pine b/tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/strategy.pine new file mode 100644 index 00000000..182c911e --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/strategy.pine @@ -0,0 +1,17 @@ +// H-MEASURE (R5 wave H) row G2-36: which chart bars does TradingView flag +// session.ismarket on? process_orders_on_close=true: every bar reverses the +// position at its own close, so every trade's ENTRY is one chart bar (TradingView +// dates a process_orders_on_close fill at the bar's open) and the entry's Signal +// (its comment) is the flag TradingView evaluated on that bar: "M1" when +// session.ismarket is true, "M0" when it is false. +//@version=6 +strategy("HM G2-36 session.ismarket", overlay=true, initial_capital=1000000000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=true, margin_long=100, margin_short=100) + +flag = session.ismarket ? "M1" : "M0" +if bar_index % 2 == 0 + strategy.entry("L" + flag, strategy.long, comment=flag) +else + strategy.entry("S" + flag, strategy.short, comment=flag) diff --git a/tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/tv_trades.csv b/tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/tv_trades.csv new file mode 100644 index 00000000..cdf98ca5 --- /dev/null +++ b/tests/fixtures/session_ismarket/hm-g236-xauusd-60-dst-mar/tv_trades.csv @@ -0,0 +1,421 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-03-03 08:00,M1,2865.95,1,2871.125,-5.175,-0.18,0,0.4,0.01,-5.79,-0.20,-5.175,0.00,1 +1,Entry long,2025-03-03 07:00,M1,2871.125,1,2871.125,-5.175,-0.18,0,0.4,0.01,-5.79,-0.20,-5.175,0.00,1 +2,Exit short,2025-03-03 09:00,M1,2870.97,1,2865.95,-5.02,-0.18,0,1.69,0.06,-6.2,-0.22,-10.195,0.00,1 +2,Entry short,2025-03-03 08:00,M1,2865.95,1,2865.95,-5.02,-0.18,0,1.69,0.06,-6.2,-0.22,-10.195,0.00,1 +3,Exit long,2025-03-03 10:00,M1,2868.15,1,2870.97,-2.82,-0.10,0,1.025,0.04,-3.22,-0.11,-13.015,0.00,1 +3,Entry long,2025-03-03 09:00,M1,2870.97,1,2870.97,-2.82,-0.10,0,1.025,0.04,-3.22,-0.11,-13.015,0.00,1 +4,Exit short,2025-03-03 11:00,M1,2864.87,1,2868.15,3.28,0.11,0,8.68,0.30,-0.02,0.00,-9.735,0.00,1 +4,Entry short,2025-03-03 10:00,M1,2868.15,1,2868.15,3.28,0.11,0,8.68,0.30,-0.02,0.00,-9.735,0.00,1 +5,Exit long,2025-03-03 12:00,M1,2867.42,1,2864.87,2.55,0.09,0,3.645,0.13,-1.675,-0.06,-7.185,0.00,1 +5,Entry long,2025-03-03 11:00,M1,2864.87,1,2864.87,2.55,0.09,0,3.645,0.13,-1.675,-0.06,-7.185,0.00,1 +6,Exit short,2025-03-03 13:00,M1,2863.995,1,2867.42,3.425,0.12,0,6.09,0.21,-0.03,0.00,-3.76,0.00,1 +6,Entry short,2025-03-03 12:00,M1,2867.42,1,2867.42,3.425,0.12,0,6.09,0.21,-0.03,0.00,-3.76,0.00,1 +7,Exit long,2025-03-03 14:00,M1,2865.595,1,2863.995,1.6,0.06,0,2.105,0.07,-2.18,-0.08,-2.16,0.00,1 +7,Entry long,2025-03-03 13:00,M1,2863.995,1,2863.995,1.6,0.06,0,2.105,0.07,-2.18,-0.08,-2.16,0.00,1 +8,Exit short,2025-03-03 15:00,M1,2863.325,1,2865.595,2.27,0.08,0,5.31,0.19,-0.815,-0.03,0.11,0.00,1 +8,Entry short,2025-03-03 14:00,M1,2865.595,1,2865.595,2.27,0.08,0,5.31,0.19,-0.815,-0.03,0.11,0.00,1 +9,Exit long,2025-03-03 16:00,M1,2868.69,1,2863.325,5.365,0.19,0,5.605,0.20,-4.325,-0.15,5.475,0.00,1 +9,Entry long,2025-03-03 15:00,M1,2863.325,1,2863.325,5.365,0.19,0,5.605,0.20,-4.325,-0.15,5.475,0.00,1 +10,Exit short,2025-03-03 17:00,M1,2867.495,1,2868.69,1.195,0.04,0,4.315,0.15,-2.875,-0.10,6.67,0.00,1 +10,Entry short,2025-03-03 16:00,M1,2868.69,1,2868.69,1.195,0.04,0,4.315,0.15,-2.875,-0.10,6.67,0.00,1 +11,Exit long,2025-03-03 18:00,M1,2871.935,1,2867.495,4.44,0.15,0,6.21,0.22,-0.22,-0.01,11.11,0.00,1 +11,Entry long,2025-03-03 17:00,M1,2867.495,1,2867.495,4.44,0.15,0,6.21,0.22,-0.22,-0.01,11.11,0.00,1 +12,Exit short,2025-03-03 19:00,M1,2874.49,1,2871.935,-2.555,-0.09,0,0.545,0.02,-3.64,-0.13,8.555,0.00,1 +12,Entry short,2025-03-03 18:00,M1,2871.935,1,2871.935,-2.555,-0.09,0,0.545,0.02,-3.64,-0.13,8.555,0.00,1 +13,Exit long,2025-03-03 20:00,M1,2875.685,1,2874.49,1.195,0.04,0,3.285,0.11,-0.39,-0.01,9.75,0.00,1 +13,Entry long,2025-03-03 19:00,M1,2874.49,1,2874.49,1.195,0.04,0,3.285,0.11,-0.39,-0.01,9.75,0.00,1 +14,Exit short,2025-03-03 21:00,M1,2872.72,1,2875.685,2.965,0.10,0,6.735,0.23,-0.64,-0.02,12.715,0.00,1 +14,Entry short,2025-03-03 20:00,M1,2875.685,1,2875.685,2.965,0.10,0,6.735,0.23,-0.64,-0.02,12.715,0.00,1 +15,Exit long,2025-03-03 22:00,M1,2877.725,1,2872.72,5.005,0.17,0,10.33,0.36,-1.355,-0.05,17.72,0.00,1 +15,Entry long,2025-03-03 21:00,M1,2872.72,1,2872.72,5.005,0.17,0,10.33,0.36,-1.355,-0.05,17.72,0.00,1 +16,Exit short,2025-03-03 23:00,M1,2887.925,1,2877.725,-10.2,-0.35,0,3.725,0.13,-12.805,-0.44,7.52,0.00,1 +16,Entry short,2025-03-03 22:00,M1,2877.725,1,2877.725,-10.2,-0.35,0,3.725,0.13,-12.805,-0.44,7.52,0.00,1 +17,Exit long,2025-03-04 00:00,M1,2891.64,1,2887.925,3.715,0.13,0,5.275,0.18,-1.81,-0.06,11.235,0.00,1 +17,Entry long,2025-03-03 23:00,M1,2887.925,1,2887.925,3.715,0.13,0,5.275,0.18,-1.81,-0.06,11.235,0.00,1 +18,Exit short,2025-03-04 01:00,M1,2890.91,1,2891.64,0.73,0.03,0,4.25,0.15,-1.375,-0.05,11.965,0.00,1 +18,Entry short,2025-03-04 00:00,M1,2891.64,1,2891.64,0.73,0.03,0,4.25,0.15,-1.375,-0.05,11.965,0.00,1 +19,Exit long,2025-03-04 02:00,M1,2890,1,2890.91,-0.91,-0.03,0,1.54,0.05,-4.25,-0.15,11.055,0.00,1 +19,Entry long,2025-03-04 01:00,M1,2890.91,1,2890.91,-0.91,-0.03,0,1.54,0.05,-4.25,-0.15,11.055,0.00,1 +20,Exit short,2025-03-04 03:00,M1,2881.67,1,2890,8.33,0.29,0,10.17,0.35,-1.28,-0.04,19.385,0.00,1 +20,Entry short,2025-03-04 02:00,M1,2890,1,2890,8.33,0.29,0,10.17,0.35,-1.28,-0.04,19.385,0.00,1 +21,Exit long,2025-03-04 04:00,M1,2890.675,1,2881.67,9.005,0.31,0,9.91,0.34,-3.235,-0.11,28.39,0.00,1 +21,Entry long,2025-03-04 03:00,M1,2881.67,1,2881.67,9.005,0.31,0,9.91,0.34,-3.235,-0.11,28.39,0.00,1 +22,Exit short,2025-03-04 05:00,M1,2892.985,1,2890.675,-2.31,-0.08,0,0.465,0.02,-4.535,-0.16,26.08,0.00,1 +22,Entry short,2025-03-04 04:00,M1,2890.675,1,2890.675,-2.31,-0.08,0,0.465,0.02,-4.535,-0.16,26.08,0.00,1 +23,Exit long,2025-03-04 07:00,M1,2890.895,1,2892.985,-2.09,-0.07,0,0.32,0.01,-2.35,-0.08,23.99,0.00,1 +23,Entry long,2025-03-04 05:00,M1,2892.985,1,2892.985,-2.09,-0.07,0,0.32,0.01,-2.35,-0.08,23.99,0.00,1 +24,Exit short,2025-03-04 08:00,M1,2891.945,1,2890.895,-1.05,-0.04,0,0.76,0.03,-2.955,-0.10,22.94,0.00,1 +24,Entry short,2025-03-04 07:00,M1,2890.895,1,2890.895,-1.05,-0.04,0,0.76,0.03,-2.955,-0.10,22.94,0.00,1 +25,Exit long,2025-03-04 09:00,M1,2888.41,1,2891.945,-3.535,-0.12,0,1.125,0.04,-9.965,-0.34,19.405,0.00,1 +25,Entry long,2025-03-04 08:00,M1,2891.945,1,2891.945,-3.535,-0.12,0,1.125,0.04,-9.965,-0.34,19.405,0.00,1 +26,Exit short,2025-03-04 10:00,M1,2885.535,1,2888.41,2.875,0.10,0,3.535,0.12,-0.96,-0.03,22.28,0.00,1 +26,Entry short,2025-03-04 09:00,M1,2888.41,1,2888.41,2.875,0.10,0,3.535,0.12,-0.96,-0.03,22.28,0.00,1 +27,Exit long,2025-03-04 11:00,M1,2888.655,1,2885.535,3.12,0.11,0,7.615,0.26,-0.475,-0.02,25.4,0.00,1 +27,Entry long,2025-03-04 10:00,M1,2885.535,1,2885.535,3.12,0.11,0,7.615,0.26,-0.475,-0.02,25.4,0.00,1 +28,Exit short,2025-03-04 12:00,M1,2891.37,1,2888.655,-2.715,-0.09,0,1.92,0.07,-4.045,-0.14,22.685,0.00,1 +28,Entry short,2025-03-04 11:00,M1,2888.655,1,2888.655,-2.715,-0.09,0,1.92,0.07,-4.045,-0.14,22.685,0.00,1 +29,Exit long,2025-03-04 13:00,M1,2889.76,1,2891.37,-1.61,-0.06,0,3.045,0.11,-2.855,-0.10,21.075,0.00,1 +29,Entry long,2025-03-04 12:00,M1,2891.37,1,2891.37,-1.61,-0.06,0,3.045,0.11,-2.855,-0.10,21.075,0.00,1 +30,Exit short,2025-03-04 14:00,M1,2887.445,1,2889.76,2.315,0.08,0,5.235,0.18,-0.11,0.00,23.39,0.00,1 +30,Entry short,2025-03-04 13:00,M1,2889.76,1,2889.76,2.315,0.08,0,5.235,0.18,-0.11,0.00,23.39,0.00,1 +31,Exit long,2025-03-04 15:00,M1,2900.03,1,2887.445,12.585,0.44,0,14.205,0.49,-0.33,-0.01,35.975,0.00,1 +31,Entry long,2025-03-04 14:00,M1,2887.445,1,2887.445,12.585,0.44,0,14.205,0.49,-0.33,-0.01,35.975,0.00,1 +32,Exit short,2025-03-04 16:00,M1,2914.22,1,2900.03,-14.19,-0.49,0,3.245,0.11,-18.705,-0.64,21.785,0.00,1 +32,Entry short,2025-03-04 15:00,M1,2900.03,1,2900.03,-14.19,-0.49,0,3.245,0.11,-18.705,-0.64,21.785,0.00,1 +33,Exit long,2025-03-04 17:00,M1,2918.105,1,2914.22,3.885,0.13,0,7.005,0.24,-1.59,-0.05,25.67,0.00,1 +33,Entry long,2025-03-04 16:00,M1,2914.22,1,2914.22,3.885,0.13,0,7.005,0.24,-1.59,-0.05,25.67,0.00,1 +34,Exit short,2025-03-04 18:00,M1,2914.725,1,2918.105,3.38,0.12,0,6.025,0.21,-0.07,0.00,29.05,0.00,1 +34,Entry short,2025-03-04 17:00,M1,2918.105,1,2918.105,3.38,0.12,0,6.025,0.21,-0.07,0.00,29.05,0.00,1 +35,Exit long,2025-03-04 19:00,M1,2920.08,1,2914.725,5.355,0.18,0,7.085,0.24,-1.705,-0.06,34.405,0.00,1 +35,Entry long,2025-03-04 18:00,M1,2914.725,1,2914.725,5.355,0.18,0,7.085,0.24,-1.705,-0.06,34.405,0.00,1 +36,Exit short,2025-03-04 20:00,M1,2924.935,1,2920.08,-4.855,-0.17,0,1.37,0.05,-5.545,-0.19,29.55,0.00,1 +36,Entry short,2025-03-04 19:00,M1,2920.08,1,2920.08,-4.855,-0.17,0,1.37,0.05,-5.545,-0.19,29.55,0.00,1 +37,Exit long,2025-03-04 21:00,M1,2922.615,1,2924.935,-2.32,-0.08,0,2.975,0.10,-5.125,-0.18,27.23,0.00,1 +37,Entry long,2025-03-04 20:00,M1,2924.935,1,2924.935,-2.32,-0.08,0,2.975,0.10,-5.125,-0.18,27.23,0.00,1 +38,Exit short,2025-03-04 22:00,M1,2903.365,1,2922.615,19.25,0.66,0,19.27,0.66,-0.465,-0.02,46.48,0.00,1 +38,Entry short,2025-03-04 21:00,M1,2922.615,1,2922.615,19.25,0.66,0,19.27,0.66,-0.465,-0.02,46.48,0.00,1 +39,Exit long,2025-03-04 23:00,M1,2906.145,1,2903.365,2.78,0.10,0,9.04,0.31,-2.975,-0.10,49.26,0.00,1 +39,Entry long,2025-03-04 22:00,M1,2903.365,1,2903.365,2.78,0.10,0,9.04,0.31,-2.975,-0.10,49.26,0.00,1 +40,Exit short,2025-03-05 00:00,M1,2915.18,1,2906.145,-9.035,-0.31,0,2.38,0.08,-9.965,-0.34,40.225,0.00,1 +40,Entry short,2025-03-04 23:00,M1,2906.145,1,2906.145,-9.035,-0.31,0,2.38,0.08,-9.965,-0.34,40.225,0.00,1 +41,Exit long,2025-03-05 01:00,M1,2911.67,1,2915.18,-3.51,-0.12,0,1.28,0.04,-4.075,-0.14,36.715,0.00,1 +41,Entry long,2025-03-05 00:00,M1,2915.18,1,2915.18,-3.51,-0.12,0,1.28,0.04,-4.075,-0.14,36.715,0.00,1 +42,Exit short,2025-03-05 02:00,M1,2911.375,1,2911.67,0.295,0.01,0,3.365,0.12,-3.605,-0.12,37.01,0.00,1 +42,Entry short,2025-03-05 01:00,M1,2911.67,1,2911.67,0.295,0.01,0,3.365,0.12,-3.605,-0.12,37.01,0.00,1 +43,Exit long,2025-03-05 03:00,M1,2916.25,1,2911.375,4.875,0.17,0,5.15,0.18,-0.115,0.00,41.885,0.00,1 +43,Entry long,2025-03-05 02:00,M1,2911.375,1,2911.375,4.875,0.17,0,5.15,0.18,-0.115,0.00,41.885,0.00,1 +44,Exit short,2025-03-05 04:00,M1,2917.285,1,2916.25,-1.035,-0.04,0,1.8,0.06,-4.36,-0.15,40.85,0.00,1 +44,Entry short,2025-03-05 03:00,M1,2916.25,1,2916.25,-1.035,-0.04,0,1.8,0.06,-4.36,-0.15,40.85,0.00,1 +45,Exit long,2025-03-05 05:00,M1,2917.85,1,2917.285,0.565,0.02,0,1.195,0.04,-3.85,-0.13,41.415,0.00,1 +45,Entry long,2025-03-05 04:00,M1,2917.285,1,2917.285,0.565,0.02,0,1.195,0.04,-3.85,-0.13,41.415,0.00,1 +46,Exit short,2025-03-05 07:00,M1,2914.39,1,2917.85,3.46,0.12,0,3.555,0.12,-0.035,0.00,44.875,0.00,1 +46,Entry short,2025-03-05 05:00,M1,2917.85,1,2917.85,3.46,0.12,0,3.555,0.12,-0.035,0.00,44.875,0.00,1 +47,Exit long,2025-03-05 08:00,M1,2914.08,1,2914.39,-0.31,-0.01,0,3.02,0.10,-2.12,-0.07,44.565,0.00,1 +47,Entry long,2025-03-05 07:00,M1,2914.39,1,2914.39,-0.31,-0.01,0,3.02,0.10,-2.12,-0.07,44.565,0.00,1 +48,Exit short,2025-03-05 09:00,M1,2907.24,1,2914.08,6.84,0.23,0,10.555,0.36,-1.15,-0.04,51.405,0.00,1 +48,Entry short,2025-03-05 08:00,M1,2914.08,1,2914.08,6.84,0.23,0,10.555,0.36,-1.15,-0.04,51.405,0.00,1 +49,Exit long,2025-03-05 10:00,M1,2904.51,1,2907.24,-2.73,-0.09,0,7.365,0.25,-5.2,-0.18,48.675,0.00,1 +49,Entry long,2025-03-05 09:00,M1,2907.24,1,2907.24,-2.73,-0.09,0,7.365,0.25,-5.2,-0.18,48.675,0.00,1 +50,Exit short,2025-03-05 11:00,M1,2908.76,1,2904.51,-4.25,-0.15,0,1.04,0.04,-9.09,-0.31,44.425,0.00,1 +50,Entry short,2025-03-05 10:00,M1,2904.51,1,2904.51,-4.25,-0.15,0,1.04,0.04,-9.09,-0.31,44.425,0.00,1 +51,Exit long,2025-03-05 12:00,M1,2910.605,1,2908.76,1.845,0.06,0,3.935,0.14,-0.14,0.00,46.27,0.00,1 +51,Entry long,2025-03-05 11:00,M1,2908.76,1,2908.76,1.845,0.06,0,3.935,0.14,-0.14,0.00,46.27,0.00,1 +52,Exit short,2025-03-05 13:00,M1,2918.905,1,2910.605,-8.3,-0.29,0,0.455,0.02,-8.37,-0.29,37.97,0.00,1 +52,Entry short,2025-03-05 12:00,M1,2910.605,1,2910.605,-8.3,-0.29,0,0.455,0.02,-8.37,-0.29,37.97,0.00,1 +53,Exit long,2025-03-05 14:00,M1,2917.2,1,2918.905,-1.705,-0.06,0,2.95,0.10,-5.31,-0.18,36.265,0.00,1 +53,Entry long,2025-03-05 13:00,M1,2918.905,1,2918.905,-1.705,-0.06,0,2.95,0.10,-5.31,-0.18,36.265,0.00,1 +54,Exit short,2025-03-05 15:00,M1,2918.15,1,2917.2,-0.95,-0.03,0,4.15,0.14,-2.625,-0.09,35.315,0.00,1 +54,Entry short,2025-03-05 14:00,M1,2917.2,1,2917.2,-0.95,-0.03,0,4.15,0.14,-2.625,-0.09,35.315,0.00,1 +55,Exit long,2025-03-05 16:00,M1,2920.085,1,2918.15,1.935,0.07,0,3.85,0.13,-1.285,-0.04,37.25,0.00,1 +55,Entry long,2025-03-05 15:00,M1,2918.15,1,2918.15,1.935,0.07,0,3.85,0.13,-1.285,-0.04,37.25,0.00,1 +56,Exit short,2025-03-05 17:00,M1,2919.02,1,2920.085,1.065,0.04,0,6.645,0.23,-2.59,-0.09,38.315,0.00,1 +56,Entry short,2025-03-05 16:00,M1,2920.085,1,2920.085,1.065,0.04,0,6.645,0.23,-2.59,-0.09,38.315,0.00,1 +57,Exit long,2025-03-05 18:00,M1,2913.29,1,2919.02,-5.73,-0.20,0,0.31,0.01,-9.92,-0.34,32.585,0.00,1 +57,Entry long,2025-03-05 17:00,M1,2919.02,1,2919.02,-5.73,-0.20,0,0.31,0.01,-9.92,-0.34,32.585,0.00,1 +58,Exit short,2025-03-05 19:00,M1,2913.265,1,2913.29,0.025,0.00,0,1.235,0.04,-4.06,-0.14,32.61,0.00,1 +58,Entry short,2025-03-05 18:00,M1,2913.29,1,2913.29,0.025,0.00,0,1.235,0.04,-4.06,-0.14,32.61,0.00,1 +59,Exit long,2025-03-05 20:00,M1,2916.29,1,2913.265,3.025,0.10,0,6.125,0.21,-0.875,-0.03,35.635,0.00,1 +59,Entry long,2025-03-05 19:00,M1,2913.265,1,2913.265,3.025,0.10,0,6.125,0.21,-0.875,-0.03,35.635,0.00,1 +60,Exit short,2025-03-05 21:00,M1,2900.04,1,2916.29,16.25,0.56,0,21.94,0.75,-0.39,-0.01,51.885,0.00,1 +60,Entry short,2025-03-05 20:00,M1,2916.29,1,2916.29,16.25,0.56,0,21.94,0.75,-0.39,-0.01,51.885,0.00,1 +61,Exit long,2025-03-05 22:00,M1,2912.005,1,2900.04,11.965,0.41,0,17.215,0.59,-2.245,-0.08,63.85,0.00,1 +61,Entry long,2025-03-05 21:00,M1,2900.04,1,2900.04,11.965,0.41,0,17.215,0.59,-2.245,-0.08,63.85,0.00,1 +62,Exit short,2025-03-05 23:00,M1,2917.26,1,2912.005,-5.255,-0.18,0,4.1,0.14,-11.98,-0.41,58.595,0.00,1 +62,Entry short,2025-03-05 22:00,M1,2912.005,1,2912.005,-5.255,-0.18,0,4.1,0.14,-11.98,-0.41,58.595,0.00,1 +63,Exit long,2025-03-06 00:00,M1,2923.87,1,2917.26,6.61,0.23,0,12.72,0.44,-1.965,-0.07,65.205,0.00,1 +63,Entry long,2025-03-05 23:00,M1,2917.26,1,2917.26,6.61,0.23,0,12.72,0.44,-1.965,-0.07,65.205,0.00,1 +64,Exit short,2025-03-06 01:00,M1,2916.055,1,2923.87,7.815,0.27,0,10.385,0.36,-1.48,-0.05,73.02,0.00,1 +64,Entry short,2025-03-06 00:00,M1,2923.87,1,2923.87,7.815,0.27,0,10.385,0.36,-1.48,-0.05,73.02,0.00,1 +65,Exit long,2025-03-06 02:00,M1,2917.505,1,2916.055,1.45,0.05,0,4.73,0.16,-4.175,-0.14,74.47,0.00,1 +65,Entry long,2025-03-06 01:00,M1,2916.055,1,2916.055,1.45,0.05,0,4.73,0.16,-4.175,-0.14,74.47,0.00,1 +66,Exit short,2025-03-06 03:00,M1,2920.545,1,2917.505,-3.04,-0.10,0,1.14,0.04,-3.925,-0.13,71.43,0.00,1 +66,Entry short,2025-03-06 02:00,M1,2917.505,1,2917.505,-3.04,-0.10,0,1.14,0.04,-3.925,-0.13,71.43,0.00,1 +67,Exit long,2025-03-06 04:00,M1,2922.22,1,2920.545,1.675,0.06,0,2.565,0.09,-0.995,-0.03,73.105,0.00,1 +67,Entry long,2025-03-06 03:00,M1,2920.545,1,2920.545,1.675,0.06,0,2.565,0.09,-0.995,-0.03,73.105,0.00,1 +68,Exit short,2025-03-06 05:00,M1,2919.265,1,2922.22,2.955,0.10,0,4.355,0.15,-0.24,-0.01,76.06,0.00,1 +68,Entry short,2025-03-06 04:00,M1,2922.22,1,2922.22,2.955,0.10,0,4.355,0.15,-0.24,-0.01,76.06,0.00,1 +69,Exit long,2025-03-06 07:00,M1,2918.315,1,2919.265,-0.95,-0.03,0,0,0.00,-2.43,-0.08,75.11,0.00,1 +69,Entry long,2025-03-06 05:00,M1,2919.265,1,2919.265,-0.95,-0.03,0,0,0.00,-2.43,-0.08,75.11,0.00,1 +70,Exit short,2025-03-06 08:00,M1,2916.745,1,2918.315,1.57,0.05,0,1.715,0.06,-2.09,-0.07,76.68,0.00,1 +70,Entry short,2025-03-06 07:00,M1,2918.315,1,2918.315,1.57,0.05,0,1.715,0.06,-2.09,-0.07,76.68,0.00,1 +71,Exit long,2025-03-06 09:00,M1,2924.74,1,2916.745,7.995,0.27,0,9.06,0.31,-0.735,-0.03,84.675,0.00,1 +71,Entry long,2025-03-06 08:00,M1,2916.745,1,2916.745,7.995,0.27,0,9.06,0.31,-0.735,-0.03,84.675,0.00,1 +72,Exit short,2025-03-06 10:00,M1,2925.71,1,2924.74,-0.97,-0.03,0,1.63,0.06,-1.98,-0.07,83.705,0.00,1 +72,Entry short,2025-03-06 09:00,M1,2924.74,1,2924.74,-0.97,-0.03,0,1.63,0.06,-1.98,-0.07,83.705,0.00,1 +73,Exit long,2025-03-06 11:00,M1,2921.27,1,2925.71,-4.44,-0.15,0,0.005,0.00,-5.58,-0.19,79.265,0.00,1 +73,Entry long,2025-03-06 10:00,M1,2925.71,1,2925.71,-4.44,-0.15,0,0.005,0.00,-5.58,-0.19,79.265,0.00,1 +74,Exit short,2025-03-06 12:00,M1,2917.54,1,2921.27,3.73,0.13,0,4.785,0.16,-0.315,-0.01,82.995,0.00,1 +74,Entry short,2025-03-06 11:00,M1,2921.27,1,2921.27,3.73,0.13,0,4.785,0.16,-0.315,-0.01,82.995,0.00,1 +75,Exit long,2025-03-06 13:00,M1,2919.975,1,2917.54,2.435,0.08,0,3.2,0.11,-2.91,-0.10,85.43,0.00,1 +75,Entry long,2025-03-06 12:00,M1,2917.54,1,2917.54,2.435,0.08,0,3.2,0.11,-2.91,-0.10,85.43,0.00,1 +76,Exit short,2025-03-06 14:00,M1,2920.705,1,2919.975,-0.73,-0.03,0,0.06,0.00,-3.135,-0.11,84.7,0.00,1 +76,Entry short,2025-03-06 13:00,M1,2919.975,1,2919.975,-0.73,-0.03,0,0.06,0.00,-3.135,-0.11,84.7,0.00,1 +77,Exit long,2025-03-06 15:00,M1,2910.135,1,2920.705,-10.57,-0.36,0,1.83,0.06,-10.895,-0.37,74.13,0.00,1 +77,Entry long,2025-03-06 14:00,M1,2920.705,1,2920.705,-10.57,-0.36,0,1.83,0.06,-10.895,-0.37,74.13,0.00,1 +78,Exit short,2025-03-06 16:00,M1,2904.845,1,2910.135,5.29,0.18,0,9.035,0.31,-1.46,-0.05,79.42,0.00,1 +78,Entry short,2025-03-06 15:00,M1,2910.135,1,2910.135,5.29,0.18,0,9.035,0.31,-1.46,-0.05,79.42,0.00,1 +79,Exit long,2025-03-06 17:00,M1,2894.225,1,2904.845,-10.62,-0.37,0,0.57,0.02,-13.695,-0.47,68.8,0.00,1 +79,Entry long,2025-03-06 16:00,M1,2904.845,1,2904.845,-10.62,-0.37,0,0.57,0.02,-13.695,-0.47,68.8,0.00,1 +80,Exit short,2025-03-06 18:00,M1,2899.39,1,2894.225,-5.165,-0.18,0,2.195,0.08,-11.115,-0.38,63.635,0.00,1 +80,Entry short,2025-03-06 17:00,M1,2894.225,1,2894.225,-5.165,-0.18,0,2.195,0.08,-11.115,-0.38,63.635,0.00,1 +81,Exit long,2025-03-06 19:00,M1,2905.89,1,2899.39,6.5,0.22,0,8.3,0.29,-0.705,-0.02,70.135,0.00,1 +81,Entry long,2025-03-06 18:00,M1,2899.39,1,2899.39,6.5,0.22,0,8.3,0.29,-0.705,-0.02,70.135,0.00,1 +82,Exit short,2025-03-06 20:00,M1,2903.345,1,2905.89,2.545,0.09,0,6.525,0.22,-1.27,-0.04,72.68,0.00,1 +82,Entry short,2025-03-06 19:00,M1,2905.89,1,2905.89,2.545,0.09,0,6.525,0.22,-1.27,-0.04,72.68,0.00,1 +83,Exit long,2025-03-06 21:00,M1,2904.595,1,2903.345,1.25,0.04,0,6.09,0.21,-2.48,-0.09,73.93,0.00,1 +83,Entry long,2025-03-06 20:00,M1,2903.345,1,2903.345,1.25,0.04,0,6.09,0.21,-2.48,-0.09,73.93,0.00,1 +84,Exit short,2025-03-06 22:00,M1,2915.215,1,2904.595,-10.62,-0.37,0,1.15,0.04,-12.075,-0.42,63.31,0.00,1 +84,Entry short,2025-03-06 21:00,M1,2904.595,1,2904.595,-10.62,-0.37,0,1.15,0.04,-12.075,-0.42,63.31,0.00,1 +85,Exit long,2025-03-06 23:00,M1,2914.55,1,2915.215,-0.665,-0.02,0,7.635,0.26,-6.005,-0.21,62.645,0.00,1 +85,Entry long,2025-03-06 22:00,M1,2915.215,1,2915.215,-0.665,-0.02,0,7.635,0.26,-6.005,-0.21,62.645,0.00,1 +86,Exit short,2025-03-07 00:00,M1,2917.235,1,2914.55,-2.685,-0.09,0,1.905,0.07,-3.965,-0.14,59.96,0.00,1 +86,Entry short,2025-03-06 23:00,M1,2914.55,1,2914.55,-2.685,-0.09,0,1.905,0.07,-3.965,-0.14,59.96,0.00,1 +87,Exit long,2025-03-07 01:00,M1,2915.57,1,2917.235,-1.665,-0.06,0,4.29,0.15,-2.775,-0.10,58.295,0.00,1 +87,Entry long,2025-03-07 00:00,M1,2917.235,1,2917.235,-1.665,-0.06,0,4.29,0.15,-2.775,-0.10,58.295,0.00,1 +88,Exit short,2025-03-07 02:00,M1,2916.235,1,2915.57,-0.665,-0.02,0,1.655,0.06,-5.2,-0.18,57.63,0.00,1 +88,Entry short,2025-03-07 01:00,M1,2915.57,1,2915.57,-0.665,-0.02,0,1.655,0.06,-5.2,-0.18,57.63,0.00,1 +89,Exit long,2025-03-07 03:00,M1,2914.03,1,2916.235,-2.205,-0.08,0,1.53,0.05,-3.88,-0.13,55.425,0.00,1 +89,Entry long,2025-03-07 02:00,M1,2916.235,1,2916.235,-2.205,-0.08,0,1.53,0.05,-3.88,-0.13,55.425,0.00,1 +90,Exit short,2025-03-07 04:00,M1,2908.895,1,2914.03,5.135,0.18,0,5.505,0.19,-2.57,-0.09,60.56,0.00,1 +90,Entry short,2025-03-07 03:00,M1,2914.03,1,2914.03,5.135,0.18,0,5.505,0.19,-2.57,-0.09,60.56,0.00,1 +91,Exit long,2025-03-07 05:00,M1,2911.1,1,2908.895,2.205,0.08,0,3.075,0.11,-0.445,-0.02,62.765,0.00,1 +91,Entry long,2025-03-07 04:00,M1,2908.895,1,2908.895,2.205,0.08,0,3.075,0.11,-0.445,-0.02,62.765,0.00,1 +92,Exit short,2025-03-07 07:00,M1,2910.395,1,2911.1,0.705,0.02,0,1.055,0.04,-2.38,-0.08,63.47,0.00,1 +92,Entry short,2025-03-07 05:00,M1,2911.1,1,2911.1,0.705,0.02,0,1.055,0.04,-2.38,-0.08,63.47,0.00,1 +93,Exit long,2025-03-07 08:00,M1,2904.78,1,2910.395,-5.615,-0.19,0,0.015,0.00,-13.485,-0.46,57.855,0.00,1 +93,Entry long,2025-03-07 07:00,M1,2910.395,1,2910.395,-5.615,-0.19,0,0.015,0.00,-13.485,-0.46,57.855,0.00,1 +94,Exit short,2025-03-07 09:00,M1,2906.965,1,2904.78,-2.185,-0.08,0,3.21,0.11,-5.59,-0.19,55.67,0.00,1 +94,Entry short,2025-03-07 08:00,M1,2904.78,1,2904.78,-2.185,-0.08,0,3.21,0.11,-5.59,-0.19,55.67,0.00,1 +95,Exit long,2025-03-07 10:00,M1,2908.2,1,2906.965,1.235,0.04,0,2.295,0.08,-5.62,-0.19,56.905,0.00,1 +95,Entry long,2025-03-07 09:00,M1,2906.965,1,2906.965,1.235,0.04,0,2.295,0.08,-5.62,-0.19,56.905,0.00,1 +96,Exit short,2025-03-07 11:00,M1,2905.465,1,2908.2,2.735,0.09,0,4.18,0.14,-0.935,-0.03,59.64,0.00,1 +96,Entry short,2025-03-07 10:00,M1,2908.2,1,2908.2,2.735,0.09,0,4.18,0.14,-0.935,-0.03,59.64,0.00,1 +97,Exit long,2025-03-07 12:00,M1,2906.645,1,2905.465,1.18,0.04,0,2.945,0.10,-0.64,-0.02,60.82,0.00,1 +97,Entry long,2025-03-07 11:00,M1,2905.465,1,2905.465,1.18,0.04,0,2.945,0.10,-0.64,-0.02,60.82,0.00,1 +98,Exit short,2025-03-07 13:00,M1,2909.66,1,2906.645,-3.015,-0.10,0,0.2,0.01,-5.895,-0.20,57.805,0.00,1 +98,Entry short,2025-03-07 12:00,M1,2906.645,1,2906.645,-3.015,-0.10,0,0.2,0.01,-5.895,-0.20,57.805,0.00,1 +99,Exit long,2025-03-07 14:00,M1,2913.57,1,2909.66,3.91,0.13,0,5.16,0.18,-1.84,-0.06,61.715,0.00,1 +99,Entry long,2025-03-07 13:00,M1,2909.66,1,2909.66,3.91,0.13,0,5.16,0.18,-1.84,-0.06,61.715,0.00,1 +100,Exit short,2025-03-07 15:00,M1,2917.2,1,2913.57,-3.63,-0.12,0,1.85,0.06,-3.84,-0.13,58.085,0.00,1 +100,Entry short,2025-03-07 14:00,M1,2913.57,1,2913.57,-3.63,-0.12,0,1.85,0.06,-3.84,-0.13,58.085,0.00,1 +101,Exit long,2025-03-07 16:00,M1,2922.48,1,2917.2,5.28,0.18,0,5.88,0.20,-1.89,-0.06,63.365,0.00,1 +101,Entry long,2025-03-07 15:00,M1,2917.2,1,2917.2,5.28,0.18,0,5.88,0.20,-1.89,-0.06,63.365,0.00,1 +102,Exit short,2025-03-07 17:00,M1,2921.62,1,2922.48,0.86,0.03,0,2.235,0.08,-0.7,-0.02,64.225,0.00,1 +102,Entry short,2025-03-07 16:00,M1,2922.48,1,2922.48,0.86,0.03,0,2.235,0.08,-0.7,-0.02,64.225,0.00,1 +103,Exit long,2025-03-07 18:00,M1,2917.935,1,2921.62,-3.685,-0.13,0,0.51,0.02,-4.465,-0.15,60.54,0.00,1 +103,Entry long,2025-03-07 17:00,M1,2921.62,1,2921.62,-3.685,-0.13,0,0.51,0.02,-4.465,-0.15,60.54,0.00,1 +104,Exit short,2025-03-07 19:00,M1,2918.85,1,2917.935,-0.915,-0.03,0,1.2,0.04,-4.17,-0.14,59.625,0.00,1 +104,Entry short,2025-03-07 18:00,M1,2917.935,1,2917.935,-0.915,-0.03,0,1.2,0.04,-4.17,-0.14,59.625,0.00,1 +105,Exit long,2025-03-07 20:00,M1,2918.77,1,2918.85,-0.08,0.00,0,3.985,0.14,-0.77,-0.03,59.545,0.00,1 +105,Entry long,2025-03-07 19:00,M1,2918.85,1,2918.85,-0.08,0.00,0,3.985,0.14,-0.77,-0.03,59.545,0.00,1 +106,Exit short,2025-03-07 21:00,M1,2913.835,1,2918.77,4.935,0.17,0,8.595,0.29,-7.655,-0.26,64.48,0.00,1 +106,Entry short,2025-03-07 20:00,M1,2918.77,1,2918.77,4.935,0.17,0,8.595,0.29,-7.655,-0.26,64.48,0.00,1 +107,Exit long,2025-03-07 22:00,M1,2926.815,1,2913.835,12.98,0.45,0,14.975,0.51,-10.005,-0.34,77.46,0.00,1 +107,Entry long,2025-03-07 21:00,M1,2913.835,1,2913.835,12.98,0.45,0,14.975,0.51,-10.005,-0.34,77.46,0.00,1 +108,Exit short,2025-03-07 23:00,M1,2915.875,1,2926.815,10.94,0.37,0,18.155,0.62,-3.725,-0.13,88.4,0.00,1 +108,Entry short,2025-03-07 22:00,M1,2926.815,1,2926.815,10.94,0.37,0,18.155,0.62,-3.725,-0.13,88.4,0.00,1 +109,Exit long,2025-03-08 00:00,M1,2914.17,1,2915.875,-1.705,-0.06,0,3.595,0.12,-4.19,-0.14,86.695,0.00,1 +109,Entry long,2025-03-07 23:00,M1,2915.875,1,2915.875,-1.705,-0.06,0,3.595,0.12,-4.19,-0.14,86.695,0.00,1 +110,Exit short,2025-03-08 01:00,M1,2908.325,1,2914.17,5.845,0.20,0,9.095,0.31,-2.18,-0.07,92.54,0.00,1 +110,Entry short,2025-03-08 00:00,M1,2914.17,1,2914.17,5.845,0.20,0,9.095,0.31,-2.18,-0.07,92.54,0.00,1 +111,Exit long,2025-03-08 02:00,M1,2906.625,1,2908.325,-1.7,-0.06,0,0.425,0.01,-6.26,-0.22,90.84,0.00,1 +111,Entry long,2025-03-08 01:00,M1,2908.325,1,2908.325,-1.7,-0.06,0,0.425,0.01,-6.26,-0.22,90.84,0.00,1 +112,Exit short,2025-03-08 03:00,M1,2905.305,1,2906.625,1.32,0.05,0,1.945,0.07,-2.955,-0.10,92.16,0.00,1 +112,Entry short,2025-03-08 02:00,M1,2906.625,1,2906.625,1.32,0.05,0,1.945,0.07,-2.955,-0.10,92.16,0.00,1 +113,Exit long,2025-03-08 04:00,M1,2909.765,1,2905.305,4.46,0.15,0,5.955,0.20,-0.44,-0.02,96.62,0.00,1 +113,Entry long,2025-03-08 03:00,M1,2905.305,1,2905.305,4.46,0.15,0,5.955,0.20,-0.44,-0.02,96.62,0.00,1 +114,Exit short,2025-03-08 05:00,M1,2909.55,1,2909.765,0.215,0.01,0,1.245,0.04,-2.93,-0.10,96.835,0.00,1 +114,Entry short,2025-03-08 04:00,M1,2909.765,1,2909.765,0.215,0.01,0,1.245,0.04,-2.93,-0.10,96.835,0.00,1 +115,Exit long,2025-03-10 06:00,M1,2911.98,1,2909.55,2.43,0.08,0,5.77,0.20,0,0.00,99.265,0.00,1 +115,Entry long,2025-03-08 05:00,M1,2909.55,1,2909.55,2.43,0.08,0,5.77,0.20,0,0.00,99.265,0.00,1 +116,Exit short,2025-03-10 07:00,M1,2916.34,1,2911.98,-4.36,-0.15,0,0.385,0.01,-5.1,-0.18,94.905,0.00,1 +116,Entry short,2025-03-10 06:00,M1,2911.98,1,2911.98,-4.36,-0.15,0,0.385,0.01,-5.1,-0.18,94.905,0.00,1 +117,Exit long,2025-03-10 08:00,M1,2913.865,1,2916.34,-2.475,-0.08,0,1.73,0.06,-3.305,-0.11,92.43,0.00,1 +117,Entry long,2025-03-10 07:00,M1,2916.34,1,2916.34,-2.475,-0.08,0,1.73,0.06,-3.305,-0.11,92.43,0.00,1 +118,Exit short,2025-03-10 09:00,M1,2912.155,1,2913.865,1.71,0.06,0,2.49,0.09,-4.52,-0.16,94.14,0.00,1 +118,Entry short,2025-03-10 08:00,M1,2913.865,1,2913.865,1.71,0.06,0,2.49,0.09,-4.52,-0.16,94.14,0.00,1 +119,Exit long,2025-03-10 10:00,M1,2911.535,1,2912.155,-0.62,-0.02,0,2.45,0.08,-2.625,-0.09,93.52,0.00,1 +119,Entry long,2025-03-10 09:00,M1,2912.155,1,2912.155,-0.62,-0.02,0,2.45,0.08,-2.625,-0.09,93.52,0.00,1 +120,Exit short,2025-03-10 11:00,M1,2911.045,1,2911.535,0.49,0.02,0,3.2,0.11,-2.05,-0.07,94.01,0.00,1 +120,Entry short,2025-03-10 10:00,M1,2911.535,1,2911.535,0.49,0.02,0,3.2,0.11,-2.05,-0.07,94.01,0.00,1 +121,Exit long,2025-03-10 12:00,M1,2910.98,1,2911.045,-0.065,0.00,0,1.63,0.06,-0.665,-0.02,93.945,0.00,1 +121,Entry long,2025-03-10 11:00,M1,2911.045,1,2911.045,-0.065,0.00,0,1.63,0.06,-0.665,-0.02,93.945,0.00,1 +122,Exit short,2025-03-10 13:00,M1,2909.985,1,2910.98,0.995,0.03,0,3.945,0.14,-0.995,-0.03,94.94,0.00,1 +122,Entry short,2025-03-10 12:00,M1,2910.98,1,2910.98,0.995,0.03,0,3.945,0.14,-0.995,-0.03,94.94,0.00,1 +123,Exit long,2025-03-10 14:00,M1,2914.44,1,2909.985,4.455,0.15,0,4.715,0.16,-0.575,-0.02,99.395,0.00,1 +123,Entry long,2025-03-10 13:00,M1,2909.985,1,2909.985,4.455,0.15,0,4.715,0.16,-0.575,-0.02,99.395,0.00,1 +124,Exit short,2025-03-10 15:00,M1,2908.52,1,2914.44,5.92,0.20,0,10.075,0.35,-1.15,-0.04,105.315,0.00,1 +124,Entry short,2025-03-10 14:00,M1,2914.44,1,2914.44,5.92,0.20,0,10.075,0.35,-1.15,-0.04,105.315,0.00,1 +125,Exit long,2025-03-10 16:00,M1,2897.935,1,2908.52,-10.585,-0.36,0,2.15,0.07,-10.895,-0.37,94.73,0.00,1 +125,Entry long,2025-03-10 15:00,M1,2908.52,1,2908.52,-10.585,-0.36,0,2.15,0.07,-10.895,-0.37,94.73,0.00,1 +126,Exit short,2025-03-10 17:00,M1,2911.215,1,2897.935,-13.28,-0.46,0,1.225,0.04,-17.66,-0.61,81.45,0.00,1 +126,Entry short,2025-03-10 16:00,M1,2897.935,1,2897.935,-13.28,-0.46,0,1.225,0.04,-17.66,-0.61,81.45,0.00,1 +127,Exit long,2025-03-10 18:00,M1,2903.055,1,2911.215,-8.16,-0.28,0,0.08,0.00,-12.79,-0.44,73.29,0.00,1 +127,Entry long,2025-03-10 17:00,M1,2911.215,1,2911.215,-8.16,-0.28,0,0.08,0.00,-12.79,-0.44,73.29,0.00,1 +128,Exit short,2025-03-10 19:00,M1,2904.22,1,2903.055,-1.165,-0.04,0,7.355,0.25,-1.75,-0.06,72.125,0.00,1 +128,Entry short,2025-03-10 18:00,M1,2903.055,1,2903.055,-1.165,-0.04,0,7.355,0.25,-1.75,-0.06,72.125,0.00,1 +129,Exit long,2025-03-10 20:00,M1,2906.8,1,2904.22,2.58,0.09,0,3.495,0.12,-3.815,-0.13,74.705,0.00,1 +129,Entry long,2025-03-10 19:00,M1,2904.22,1,2904.22,2.58,0.09,0,3.495,0.12,-3.815,-0.13,74.705,0.00,1 +130,Exit short,2025-03-10 21:00,M1,2903.79,1,2906.8,3.01,0.10,0,9.095,0.31,-1.875,-0.06,77.715,0.00,1 +130,Entry short,2025-03-10 20:00,M1,2906.8,1,2906.8,3.01,0.10,0,9.095,0.31,-1.875,-0.06,77.715,0.00,1 +131,Exit long,2025-03-10 22:00,M1,2908.115,1,2903.79,4.325,0.15,0,5.425,0.19,-2.795,-0.10,82.04,0.00,1 +131,Entry long,2025-03-10 21:00,M1,2903.79,1,2903.79,4.325,0.15,0,5.425,0.19,-2.795,-0.10,82.04,0.00,1 +132,Exit short,2025-03-10 23:00,M1,2904.57,1,2908.115,3.545,0.12,0,7.475,0.26,-2.52,-0.09,85.585,0.00,1 +132,Entry short,2025-03-10 22:00,M1,2908.115,1,2908.115,3.545,0.12,0,7.475,0.26,-2.52,-0.09,85.585,0.00,1 +133,Exit long,2025-03-11 00:00,M1,2895.975,1,2904.57,-8.595,-0.30,0,2.325,0.08,-8.67,-0.30,76.99,0.00,1 +133,Entry long,2025-03-10 23:00,M1,2904.57,1,2904.57,-8.595,-0.30,0,2.325,0.08,-8.67,-0.30,76.99,0.00,1 +134,Exit short,2025-03-11 01:00,M1,2886.135,1,2895.975,9.84,0.34,0,13.155,0.45,-0.2,-0.01,86.83,0.00,1 +134,Entry short,2025-03-11 00:00,M1,2895.975,1,2895.975,9.84,0.34,0,13.155,0.45,-0.2,-0.01,86.83,0.00,1 +135,Exit long,2025-03-11 02:00,M1,2885.665,1,2886.135,-0.47,-0.02,0,3.6,0.12,-5.825,-0.20,86.36,0.00,1 +135,Entry long,2025-03-11 01:00,M1,2886.135,1,2886.135,-0.47,-0.02,0,3.6,0.12,-5.825,-0.20,86.36,0.00,1 +136,Exit short,2025-03-11 03:00,M1,2885.59,1,2885.665,0.075,0.00,0,5.31,0.18,-5.4,-0.19,86.435,0.00,1 +136,Entry short,2025-03-11 02:00,M1,2885.665,1,2885.665,0.075,0.00,0,5.31,0.18,-5.4,-0.19,86.435,0.00,1 +137,Exit long,2025-03-11 04:00,M1,2889.5,1,2885.59,3.91,0.14,0,4.015,0.14,-1.14,-0.04,90.345,0.00,1 +137,Entry long,2025-03-11 03:00,M1,2885.59,1,2885.59,3.91,0.14,0,4.015,0.14,-1.14,-0.04,90.345,0.00,1 +138,Exit short,2025-03-11 06:00,M1,2884.655,1,2889.5,4.845,0.17,0,5.385,0.19,-0.46,-0.02,95.19,0.00,1 +138,Entry short,2025-03-11 04:00,M1,2889.5,1,2889.5,4.845,0.17,0,5.385,0.19,-0.46,-0.02,95.19,0.00,1 +139,Exit long,2025-03-11 07:00,M1,2884.79,1,2884.655,0.135,0.00,0,1.22,0.04,-2.225,-0.08,95.325,0.00,1 +139,Entry long,2025-03-11 06:00,M1,2884.655,1,2884.655,0.135,0.00,0,1.22,0.04,-2.225,-0.08,95.325,0.00,1 +140,Exit short,2025-03-11 08:00,M1,2887.415,1,2884.79,-2.625,-0.09,0,0.46,0.02,-3.99,-0.14,92.7,0.00,1 +140,Entry short,2025-03-11 07:00,M1,2884.79,1,2884.79,-2.625,-0.09,0,0.46,0.02,-3.99,-0.14,92.7,0.00,1 +141,Exit long,2025-03-11 09:00,M1,2890.65,1,2887.415,3.235,0.11,0,3.66,0.13,-7.165,-0.25,95.935,0.00,1 +141,Entry long,2025-03-11 08:00,M1,2887.415,1,2887.415,3.235,0.11,0,3.66,0.13,-7.165,-0.25,95.935,0.00,1 +142,Exit short,2025-03-11 10:00,M1,2899.175,1,2890.65,-8.525,-0.29,0,0.635,0.02,-9.865,-0.34,87.41,0.00,1 +142,Entry short,2025-03-11 09:00,M1,2890.65,1,2890.65,-8.525,-0.29,0,0.635,0.02,-9.865,-0.34,87.41,0.00,1 +143,Exit long,2025-03-11 11:00,M1,2896.915,1,2899.175,-2.26,-0.08,0,0.295,0.01,-3.965,-0.14,85.15,0.00,1 +143,Entry long,2025-03-11 10:00,M1,2899.175,1,2899.175,-2.26,-0.08,0,0.295,0.01,-3.965,-0.14,85.15,0.00,1 +144,Exit short,2025-03-11 12:00,M1,2897.77,1,2896.915,-0.855,-0.03,0,1.4,0.05,-1.77,-0.06,84.295,0.00,1 +144,Entry short,2025-03-11 11:00,M1,2896.915,1,2896.915,-0.855,-0.03,0,1.4,0.05,-1.77,-0.06,84.295,0.00,1 +145,Exit long,2025-03-11 13:00,M1,2895.93,1,2897.77,-1.84,-0.06,0,1.415,0.05,-4.955,-0.17,82.455,0.00,1 +145,Entry long,2025-03-11 12:00,M1,2897.77,1,2897.77,-1.84,-0.06,0,1.415,0.05,-4.955,-0.17,82.455,0.00,1 +146,Exit short,2025-03-11 14:00,M1,2902.35,1,2895.93,-6.42,-0.22,0,2.31,0.08,-6.96,-0.24,76.035,0.00,1 +146,Entry short,2025-03-11 13:00,M1,2895.93,1,2895.93,-6.42,-0.22,0,2.31,0.08,-6.96,-0.24,76.035,0.00,1 +147,Exit long,2025-03-11 15:00,M1,2901.11,1,2902.35,-1.24,-0.04,0,1.35,0.05,-4.765,-0.16,74.795,0.00,1 +147,Entry long,2025-03-11 14:00,M1,2902.35,1,2902.35,-1.24,-0.04,0,1.35,0.05,-4.765,-0.16,74.795,0.00,1 +148,Exit short,2025-03-11 16:00,M1,2910.01,1,2901.11,-8.9,-0.31,0,0.555,0.02,-9.565,-0.33,65.895,0.00,1 +148,Entry short,2025-03-11 15:00,M1,2901.11,1,2901.11,-8.9,-0.31,0,0.555,0.02,-9.565,-0.33,65.895,0.00,1 +149,Exit long,2025-03-11 17:00,M1,2911.825,1,2910.01,1.815,0.06,0,2.53,0.09,-1.075,-0.04,67.71,0.00,1 +149,Entry long,2025-03-11 16:00,M1,2910.01,1,2910.01,1.815,0.06,0,2.53,0.09,-1.075,-0.04,67.71,0.00,1 +150,Exit short,2025-03-11 18:00,M1,2912.105,1,2911.825,-0.28,-0.01,0,0.88,0.03,-2.77,-0.10,67.43,0.00,1 +150,Entry short,2025-03-11 17:00,M1,2911.825,1,2911.825,-0.28,-0.01,0,0.88,0.03,-2.77,-0.10,67.43,0.00,1 +151,Exit long,2025-03-11 19:00,M1,2913.615,1,2912.105,1.51,0.05,0,3.375,0.12,-1.295,-0.04,68.94,0.00,1 +151,Entry long,2025-03-11 18:00,M1,2912.105,1,2912.105,1.51,0.05,0,3.375,0.12,-1.295,-0.04,68.94,0.00,1 +152,Exit short,2025-03-11 20:00,M1,2913.11,1,2913.615,0.505,0.02,0,2.955,0.10,-2.495,-0.09,69.445,0.00,1 +152,Entry short,2025-03-11 19:00,M1,2913.615,1,2913.615,0.505,0.02,0,2.955,0.10,-2.495,-0.09,69.445,0.00,1 +153,Exit long,2025-03-11 21:00,M1,2918.2,1,2913.11,5.09,0.17,0,9.115,0.31,-6.22,-0.21,74.535,0.00,1 +153,Entry long,2025-03-11 20:00,M1,2913.11,1,2913.11,5.09,0.17,0,9.115,0.31,-6.22,-0.21,74.535,0.00,1 +154,Exit short,2025-03-11 22:00,M1,2914.195,1,2918.2,4.005,0.14,0,6.43,0.22,-2.705,-0.09,78.54,0.00,1 +154,Entry short,2025-03-11 21:00,M1,2918.2,1,2918.2,4.005,0.14,0,6.43,0.22,-2.705,-0.09,78.54,0.00,1 +155,Exit long,2025-03-11 23:00,M1,2916.85,1,2914.195,2.655,0.09,0,5.775,0.20,-0.095,0.00,81.195,0.00,1 +155,Entry long,2025-03-11 22:00,M1,2914.195,1,2914.195,2.655,0.09,0,5.775,0.20,-0.095,0.00,81.195,0.00,1 +156,Exit short,2025-03-12 00:00,M1,2917.2,1,2916.85,-0.35,-0.01,0,1.63,0.06,-1.295,-0.04,80.845,0.00,1 +156,Entry short,2025-03-11 23:00,M1,2916.85,1,2916.85,-0.35,-0.01,0,1.63,0.06,-1.295,-0.04,80.845,0.00,1 +157,Exit long,2025-03-12 01:00,M1,2917.83,1,2917.2,0.63,0.02,0,3.555,0.12,-2.99,-0.10,81.475,0.00,1 +157,Entry long,2025-03-12 00:00,M1,2917.2,1,2917.2,0.63,0.02,0,3.555,0.12,-2.99,-0.10,81.475,0.00,1 +158,Exit short,2025-03-12 02:00,M1,2918.355,1,2917.83,-0.525,-0.02,0,2.545,0.09,-1.76,-0.06,80.95,0.00,1 +158,Entry short,2025-03-12 01:00,M1,2917.83,1,2917.83,-0.525,-0.02,0,2.545,0.09,-1.76,-0.06,80.95,0.00,1 +159,Exit long,2025-03-12 03:00,M1,2918.395,1,2918.355,0.04,0.00,0,3.045,0.10,-0.845,-0.03,80.99,0.00,1 +159,Entry long,2025-03-12 02:00,M1,2918.355,1,2918.355,0.04,0.00,0,3.045,0.10,-0.845,-0.03,80.99,0.00,1 +160,Exit short,2025-03-12 04:00,M1,2915.695,1,2918.395,2.7,0.09,0,3.26,0.11,-0.7,-0.02,83.69,0.00,1 +160,Entry short,2025-03-12 03:00,M1,2918.395,1,2918.395,2.7,0.09,0,3.26,0.11,-0.7,-0.02,83.69,0.00,1 +161,Exit long,2025-03-12 06:00,M1,2917.72,1,2915.695,2.025,0.07,0,2.57,0.09,-1.26,-0.04,85.715,0.00,1 +161,Entry long,2025-03-12 04:00,M1,2915.695,1,2915.695,2.025,0.07,0,2.57,0.09,-1.26,-0.04,85.715,0.00,1 +162,Exit short,2025-03-12 07:00,M1,2915.555,1,2917.72,2.165,0.07,0,2.245,0.08,-2.035,-0.07,87.88,0.00,1 +162,Entry short,2025-03-12 06:00,M1,2917.72,1,2917.72,2.165,0.07,0,2.245,0.08,-2.035,-0.07,87.88,0.00,1 +163,Exit long,2025-03-12 08:00,M1,2914.25,1,2915.555,-1.305,-0.04,0,0.03,0.00,-3.325,-0.11,86.575,0.00,1 +163,Entry long,2025-03-12 07:00,M1,2915.555,1,2915.555,-1.305,-0.04,0,0.03,0.00,-3.325,-0.11,86.575,0.00,1 +164,Exit short,2025-03-12 09:00,M1,2915,1,2914.25,-0.75,-0.03,0,3.35,0.11,-3.23,-0.11,85.825,0.00,1 +164,Entry short,2025-03-12 08:00,M1,2914.25,1,2914.25,-0.75,-0.03,0,3.35,0.11,-3.23,-0.11,85.825,0.00,1 +165,Exit long,2025-03-12 10:00,M1,2916.36,1,2915,1.36,0.05,0,3.39,0.12,-0.945,-0.03,87.185,0.00,1 +165,Entry long,2025-03-12 09:00,M1,2915,1,2915,1.36,0.05,0,3.39,0.12,-0.945,-0.03,87.185,0.00,1 +166,Exit short,2025-03-12 11:00,M1,2915.035,1,2916.36,1.325,0.05,0,1.41,0.05,-3.75,-0.13,88.51,0.00,1 +166,Entry short,2025-03-12 10:00,M1,2916.36,1,2916.36,1.325,0.05,0,1.41,0.05,-3.75,-0.13,88.51,0.00,1 +167,Exit long,2025-03-12 12:00,M1,2914.985,1,2915.035,-0.05,0.00,0,1.055,0.04,-2.405,-0.08,88.46,0.00,1 +167,Entry long,2025-03-12 11:00,M1,2915.035,1,2915.035,-0.05,0.00,0,1.055,0.04,-2.405,-0.08,88.46,0.00,1 +168,Exit short,2025-03-12 13:00,M1,2916.035,1,2914.985,-1.05,-0.04,0,2.82,0.10,-1.585,-0.05,87.41,0.00,1 +168,Entry short,2025-03-12 12:00,M1,2914.985,1,2914.985,-1.05,-0.04,0,2.82,0.10,-1.585,-0.05,87.41,0.00,1 +169,Exit long,2025-03-12 14:00,M1,2911.95,1,2916.035,-4.085,-0.14,0,5.15,0.18,-4.6,-0.16,83.325,0.00,1 +169,Entry long,2025-03-12 13:00,M1,2916.035,1,2916.035,-4.085,-0.14,0,5.15,0.18,-4.6,-0.16,83.325,0.00,1 +170,Exit short,2025-03-12 15:00,M1,2914.655,1,2911.95,-2.705,-0.09,0,2.855,0.10,-3.805,-0.13,80.62,0.00,1 +170,Entry short,2025-03-12 14:00,M1,2911.95,1,2911.95,-2.705,-0.09,0,2.855,0.10,-3.805,-0.13,80.62,0.00,1 +171,Exit long,2025-03-12 16:00,M1,2922.27,1,2914.655,7.615,0.26,0,7.625,0.26,-0.48,-0.02,88.235,0.00,1 +171,Entry long,2025-03-12 15:00,M1,2914.655,1,2914.655,7.615,0.26,0,7.625,0.26,-0.48,-0.02,88.235,0.00,1 +172,Exit short,2025-03-12 17:00,M1,2914.73,1,2922.27,7.54,0.26,0,8.53,0.29,-3.12,-0.11,95.775,0.00,1 +172,Entry short,2025-03-12 16:00,M1,2922.27,1,2922.27,7.54,0.26,0,8.53,0.29,-3.12,-0.11,95.775,0.00,1 +173,Exit long,2025-03-12 18:00,M1,2914.41,1,2914.73,-0.32,-0.01,0,2.335,0.08,-1.87,-0.06,95.455,0.00,1 +173,Entry long,2025-03-12 17:00,M1,2914.73,1,2914.73,-0.32,-0.01,0,2.335,0.08,-1.87,-0.06,95.455,0.00,1 +174,Exit short,2025-03-12 19:00,M1,2915.39,1,2914.41,-0.98,-0.03,0,1.615,0.06,-2.445,-0.08,94.475,0.00,1 +174,Entry short,2025-03-12 18:00,M1,2914.41,1,2914.41,-0.98,-0.03,0,1.615,0.06,-2.445,-0.08,94.475,0.00,1 +175,Exit long,2025-03-12 20:00,M1,2917.56,1,2915.39,2.17,0.07,0,7.905,0.27,-3.24,-0.11,96.645,0.00,1 +175,Entry long,2025-03-12 19:00,M1,2915.39,1,2915.39,2.17,0.07,0,7.905,0.27,-3.24,-0.11,96.645,0.00,1 +176,Exit short,2025-03-12 21:00,M1,2915.595,1,2917.56,1.965,0.07,0,11.45,0.39,-1.295,-0.04,98.61,0.00,1 +176,Entry short,2025-03-12 20:00,M1,2917.56,1,2917.56,1.965,0.07,0,11.45,0.39,-1.295,-0.04,98.61,0.00,1 +177,Exit long,2025-03-12 22:00,M1,2921.985,1,2915.595,6.39,0.22,0,6.975,0.24,-1.235,-0.04,105,0.00,1 +177,Entry long,2025-03-12 21:00,M1,2915.595,1,2915.595,6.39,0.22,0,6.975,0.24,-1.235,-0.04,105,0.00,1 +178,Exit short,2025-03-12 23:00,M1,2928.46,1,2921.985,-6.475,-0.22,0,0.5,0.02,-7.505,-0.26,98.525,0.00,1 +178,Entry short,2025-03-12 22:00,M1,2921.985,1,2921.985,-6.475,-0.22,0,0.5,0.02,-7.505,-0.26,98.525,0.00,1 +179,Exit long,2025-03-13 00:00,M1,2936.235,1,2928.46,7.775,0.27,0,10.735,0.37,-0.46,-0.02,106.3,0.00,1 +179,Entry long,2025-03-12 23:00,M1,2928.46,1,2928.46,7.775,0.27,0,10.735,0.37,-0.46,-0.02,106.3,0.00,1 +180,Exit short,2025-03-13 01:00,M1,2936.21,1,2936.235,0.025,0.00,0,0.29,0.01,-4.455,-0.15,106.325,0.00,1 +180,Entry short,2025-03-13 00:00,M1,2936.235,1,2936.235,0.025,0.00,0,0.29,0.01,-4.455,-0.15,106.325,0.00,1 +181,Exit long,2025-03-13 02:00,M1,2935.21,1,2936.21,-1,-0.03,0,1.245,0.04,-2.33,-0.08,105.325,0.00,1 +181,Entry long,2025-03-13 01:00,M1,2936.21,1,2936.21,-1,-0.03,0,1.245,0.04,-2.33,-0.08,105.325,0.00,1 +182,Exit short,2025-03-13 03:00,M1,2931.405,1,2935.21,3.805,0.13,0,4.21,0.14,-0.485,-0.02,109.13,0.00,1 +182,Entry short,2025-03-13 02:00,M1,2935.21,1,2935.21,3.805,0.13,0,4.21,0.14,-0.485,-0.02,109.13,0.00,1 +183,Exit long,2025-03-13 04:00,M1,2933.61,1,2931.405,2.205,0.08,0,2.905,0.10,-0.44,-0.02,111.335,0.00,1 +183,Entry long,2025-03-13 03:00,M1,2931.405,1,2931.405,2.205,0.08,0,2.905,0.10,-0.44,-0.02,111.335,0.00,1 +184,Exit short,2025-03-13 06:00,M1,2938.205,1,2933.61,-4.595,-0.16,0,0,0.00,-5.465,-0.19,106.74,0.00,1 +184,Entry short,2025-03-13 04:00,M1,2933.61,1,2933.61,-4.595,-0.16,0,0,0.00,-5.465,-0.19,106.74,0.00,1 +185,Exit long,2025-03-13 07:00,M1,2938.195,1,2938.205,-0.01,0.00,0,1.29,0.04,-3.41,-0.12,106.73,0.00,1 +185,Entry long,2025-03-13 06:00,M1,2938.205,1,2938.205,-0.01,0.00,0,1.29,0.04,-3.41,-0.12,106.73,0.00,1 +186,Exit short,2025-03-13 08:00,M1,2938.71,1,2938.195,-0.515,-0.02,0,0.55,0.02,-1.295,-0.04,106.215,0.00,1 +186,Entry short,2025-03-13 07:00,M1,2938.195,1,2938.195,-0.515,-0.02,0,0.55,0.02,-1.295,-0.04,106.215,0.00,1 +187,Exit long,2025-03-13 09:00,M1,2937.92,1,2938.71,-0.79,-0.03,0,2.85,0.10,-2.07,-0.07,105.425,0.00,1 +187,Entry long,2025-03-13 08:00,M1,2938.71,1,2938.71,-0.79,-0.03,0,2.85,0.10,-2.07,-0.07,105.425,0.00,1 +188,Exit short,2025-03-13 10:00,M1,2944.38,1,2937.92,-6.46,-0.22,0,0.37,0.01,-8.225,-0.28,98.965,0.00,1 +188,Entry short,2025-03-13 09:00,M1,2937.92,1,2937.92,-6.46,-0.22,0,0.37,0.01,-8.225,-0.28,98.965,0.00,1 +189,Exit long,2025-03-13 11:00,M1,2945.755,1,2944.38,1.375,0.05,0,2.815,0.10,-1.25,-0.04,100.34,0.00,1 +189,Entry long,2025-03-13 10:00,M1,2944.38,1,2944.38,1.375,0.05,0,2.815,0.10,-1.25,-0.04,100.34,0.00,1 +190,Exit short,2025-03-13 12:00,M1,2944.76,1,2945.755,0.995,0.03,0,2.24,0.08,-0.555,-0.02,101.335,0.00,1 +190,Entry short,2025-03-13 11:00,M1,2945.755,1,2945.755,0.995,0.03,0,2.24,0.08,-0.555,-0.02,101.335,0.00,1 +191,Exit long,2025-03-13 13:00,M1,2940.14,1,2944.76,-4.62,-0.16,0,0.34,0.01,-6.45,-0.22,96.715,0.00,1 +191,Entry long,2025-03-13 12:00,M1,2944.76,1,2944.76,-4.62,-0.16,0,0.34,0.01,-6.45,-0.22,96.715,0.00,1 +192,Exit short,2025-03-13 14:00,M1,2936.955,1,2940.14,3.185,0.11,0,4.625,0.16,-0.215,-0.01,99.9,0.00,1 +192,Entry short,2025-03-13 13:00,M1,2940.14,1,2940.14,3.185,0.11,0,4.625,0.16,-0.215,-0.01,99.9,0.00,1 +193,Exit long,2025-03-13 15:00,M1,2935.32,1,2936.955,-1.635,-0.06,0,1.235,0.04,-3.975,-0.14,98.265,0.00,1 +193,Entry long,2025-03-13 14:00,M1,2936.955,1,2936.955,-1.635,-0.06,0,1.235,0.04,-3.975,-0.14,98.265,0.00,1 +194,Exit short,2025-03-13 16:00,M1,2941.54,1,2935.32,-6.22,-0.21,0,0.435,0.01,-9.69,-0.33,92.045,0.00,1 +194,Entry short,2025-03-13 15:00,M1,2935.32,1,2935.32,-6.22,-0.21,0,0.435,0.01,-9.69,-0.33,92.045,0.00,1 +195,Exit long,2025-03-13 17:00,M1,2947.835,1,2941.54,6.295,0.21,0,7.39,0.25,-0.42,-0.01,98.34,0.00,1 +195,Entry long,2025-03-13 16:00,M1,2941.54,1,2941.54,6.295,0.21,0,7.39,0.25,-0.42,-0.01,98.34,0.00,1 +196,Exit short,2025-03-13 18:00,M1,2944.15,1,2947.835,3.685,0.13,0,5.705,0.19,-0.86,-0.03,102.025,0.00,1 +196,Entry short,2025-03-13 17:00,M1,2947.835,1,2947.835,3.685,0.13,0,5.705,0.19,-0.86,-0.03,102.025,0.00,1 +197,Exit long,2025-03-13 19:00,M1,2946.395,1,2944.15,2.245,0.08,0,4.5,0.15,0,0.00,104.27,0.00,1 +197,Entry long,2025-03-13 18:00,M1,2944.15,1,2944.15,2.245,0.08,0,4.5,0.15,0,0.00,104.27,0.00,1 +198,Exit short,2025-03-13 20:00,M1,2942.575,1,2946.395,3.82,0.13,0,5.175,0.18,-2.905,-0.10,108.09,0.00,1 +198,Entry short,2025-03-13 19:00,M1,2946.395,1,2946.395,3.82,0.13,0,5.175,0.18,-2.905,-0.10,108.09,0.00,1 +199,Exit long,2025-03-13 21:00,M1,2950.49,1,2942.575,7.915,0.27,0,9.967,0.34,-3.485,-0.12,116.005,0.00,1 +199,Entry long,2025-03-13 20:00,M1,2942.575,1,2942.575,7.915,0.27,0,9.967,0.34,-3.485,-0.12,116.005,0.00,1 +200,Exit short,2025-03-13 22:00,M1,2970.05,1,2950.49,-19.56,-0.66,0,0.535,0.02,-20.77,-0.70,96.445,0.00,1 +200,Entry short,2025-03-13 21:00,M1,2950.49,1,2950.49,-19.56,-0.66,0,0.535,0.02,-20.77,-0.70,96.445,0.00,1 +201,Exit long,2025-03-13 23:00,M1,2977.9,1,2970.05,7.85,0.26,0,8.4,0.28,-3.505,-0.12,104.295,0.00,1 +201,Entry long,2025-03-13 22:00,M1,2970.05,1,2970.05,7.85,0.26,0,8.4,0.28,-3.505,-0.12,104.295,0.00,1 +202,Exit short,2025-03-14 00:00,M1,2980.16,1,2977.9,-2.26,-0.08,0,1.205,0.04,-7.39,-0.25,102.035,0.00,1 +202,Entry short,2025-03-13 23:00,M1,2977.9,1,2977.9,-2.26,-0.08,0,1.205,0.04,-7.39,-0.25,102.035,0.00,1 +203,Exit long,2025-03-14 01:00,M1,2979.555,1,2980.16,-0.605,-0.02,0,2.11,0.07,-3.635,-0.12,101.43,0.00,1 +203,Entry long,2025-03-14 00:00,M1,2980.16,1,2980.16,-0.605,-0.02,0,2.11,0.07,-3.635,-0.12,101.43,0.00,1 +204,Exit short,2025-03-14 02:00,M1,2983.325,1,2979.555,-3.77,-0.13,0,0.36,0.01,-4.87,-0.16,97.66,0.00,1 +204,Entry short,2025-03-14 01:00,M1,2979.555,1,2979.555,-3.77,-0.13,0,0.36,0.01,-4.87,-0.16,97.66,0.00,1 +205,Exit long,2025-03-14 03:00,M1,2982.94,1,2983.325,-0.385,-0.01,0,2.245,0.08,-2.72,-0.09,97.275,0.00,1 +205,Entry long,2025-03-14 02:00,M1,2983.325,1,2983.325,-0.385,-0.01,0,2.245,0.08,-2.72,-0.09,97.275,0.00,1 +206,Exit short,2025-03-14 04:00,M1,2988.695,1,2982.94,-5.755,-0.19,0,1.595,0.05,-6.36,-0.21,91.52,0.00,1 +206,Entry short,2025-03-14 03:00,M1,2982.94,1,2982.94,-5.755,-0.19,0,1.595,0.05,-6.36,-0.21,91.52,0.00,1 +207,Exit long,2025-03-14 06:00,M1,2987.31,1,2988.695,-1.385,-0.05,0,1.36,0.05,-5.35,-0.18,90.135,0.00,1 +207,Entry long,2025-03-14 04:00,M1,2988.695,1,2988.695,-1.385,-0.05,0,1.36,0.05,-5.35,-0.18,90.135,0.00,1 +208,Exit short,2025-03-14 07:00,M1,2986.465,1,2987.31,0.845,0.03,0,1.865,0.06,-2.055,-0.07,90.98,0.00,1 +208,Entry short,2025-03-14 06:00,M1,2987.31,1,2987.31,0.845,0.03,0,1.865,0.06,-2.055,-0.07,90.98,0.00,1 +209,Exit long,2025-03-14 08:00,M1,2986.76,1,2986.465,0.295,0.01,0,2.365,0.08,-1.755,-0.06,91.275,0.00,1 +209,Entry long,2025-03-14 07:00,M1,2986.465,1,2986.465,0.295,0.01,0,2.365,0.08,-1.755,-0.06,91.275,0.00,1 +210,Exit short,2025-03-14 08:00,,2986.76,1,2986.76,0,0.00,0,0,0.00,0,0.00,91.275,0.00,0 +210,Entry short,2025-03-14 08:00,M1,2986.76,1,2986.76,0,0.00,0,0,0.00,0,0.00,91.275,0.00,0 \ No newline at end of file diff --git a/tests/test_e19_excursion_tape.cpp b/tests/test_e19_excursion_tape.cpp new file mode 100644 index 00000000..fbc05a37 --- /dev/null +++ b/tests/test_e19_excursion_tape.cpp @@ -0,0 +1,374 @@ +// R5 wave H, lane H-MEASURE, row E19 / G2-22: the excursion model, measured +// against TradingView. +// +// RULING A48 keeps per-lot MFE/MAE in the Pine source host +// (PineStrategyHost::owns_lot_excursions, closed_lot_excursion / +// owner_lot_excursion in src/source/pine_strategy_host.cpp, the H/L/C walk +// sample_open_trade_extremes with its entry-bar masks), while the kernel keeps +// a sampler of its own for every other host (NativeExecutionConsumer:: +// apply_excursion at each delivered path point, and the closing-row fold in +// build_close_trade_with_costs). The ruling was pinned against ab9714be's +// rows (tests/test_l11a_host_excursion_twin.cpp); this row pins both models +// against TradingView's own tape. +// +// The tape: `lab tv` export hm-e19-stop-reversal-grouping (ws-report-v1, +// rangeProof covered) of corpus probe order-stop-entry-reversal-grouping-01 +// ("PF probe 57", BINANCE:ETHUSDT.P 15m, 2025-04-01..2026-05-01, 1580 +// trades); its excursion cells equal the corpus tape's on all 1460 trades the +// two share. Three of its days are replayed here on the corpus 15m feed +// (fixtures/e19_excursion_tape/bars.inc), each through +// - the Pine adapter: a PineStrategyHost running the probe's script, and +// - the kernel: a bare NativeStrategyHost submitting the same requests +// (market lots, a stop reversal at the prior bar's high, a stop reversal +// at the prior bar's low, a flatten), so every fill is the same fill; +// and every closed row is compared with the tape's (fixtures/.../tv_rows.inc). +// +// What the lane's corpus-wide measurement found (both models run over the 312 +// probes: 431 390 trades, the two agree on all but 341; of the 326 of those a +// TradingView export identifies, the kernel's number is TradingView's on 315 +// and the adapter's on none) is what these rows pin, one of each class: +// A. entry-bar over-mask: a stop entry that fills at the bar's open has no +// path before it, yet the adapter masks the entry bar's adverse or +// favorable extreme (day 0 LREV: adverse 0 where TradingView has -3.22; +// day 1 SREV: favorable 1.47 where TradingView has 6.97); +// B. exit-bar over-fold: a lot closed at the exit bar's open still takes +// that bar's later extreme (day 1 L1/L2: adverse -16.35 / -3.44 where +// TradingView has -13.75 / -0.43, the entry bars' lows); +// C. an open print ahead of a same-bar stop fill: TradingView counts the +// exit bar's open (3925.80) in the favorable excursion; neither model +// samples a bar's open print, so both report the fill alone here +// (13.64 / 7.78, day 2 L1/L2) against TradingView's 13.65 / 7.79. (Under +// the Pine run's own price grid the kernel sampler reads the stop's +// half-tick crossing instead, 13.645 -- the "overshoot" the l11a twin +// retired in favour of ab9714be's 13.64; TradingView has neither.) +// The other six rows are controls: all three agree. +// D. a calc_on_order_fills scratch (second tape, hm-e19-coof-scratch: a +// long market entry fills at the 00:30 open and its own fill +// recalculation closes it at the same print): TradingView reports 0 / 0 +// on all four days, and so does the kernel; the adapter folds the whole +// entry bar into the lot (ab9714be pine_scheduler.cpp:357-366's +// update_per_trade_extremes on every fill recalculation). +// +// The adapter columns are pinned as MEASURED DIVERGENCES: a lane that moves +// the Pine host onto the kernel sampler, or corrects the adapter's masks, +// flips them to TradingView's value and must re-pin this row. +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; + +namespace { + +int passed = 0; +int failed = 0; + +#define CHECK(x) do { \ + if (x) { ++passed; } \ + else { ++failed; std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #x); } \ +} while (0) + +constexpr double kNaN = std::numeric_limits::quiet_NaN(); +bool near(double a, double b, double tol = 1e-6) { return std::abs(a - b) <= tol; } + +struct FeedBar { + std::int64_t ts; + double open, high, low, close, volume; +}; +struct TapeRow { + int day; + int trade; + bool is_long; + std::int64_t entry_ms, exit_ms; + double entry, exit, qty, favorable, adverse; // TradingView: adverse <= 0 +}; + +#include "fixtures/e19_excursion_tape/bars.inc" +#include "fixtures/e19_excursion_tape/tv_rows.inc" + +template +std::vector day_bars(const FeedBar (&rows)[N]) { + std::vector bars; + for (const FeedBar& r : rows) bars.push_back({r.open, r.high, r.low, r.close, r.volume, r.ts}); + return bars; +} + +int minute_of_day(std::int64_t ms) { + return static_cast(((ms / 60000) % 1440 + 1440) % 1440); +} + +// PF probe 57 as generated code runs it (strategy.pine, the probe's own +// source): the chart is UTC, so `hour`/`minute` are the bar open's. +class Probe57 final : public source::PineStrategyHost { +public: + Probe57() { + source::PineStrategyConfig c; + c.initial_capital = 1000000.0; + c.default_qty_type = static_cast(QtyType::FIXED); + c.default_qty_value = 1.0; + c.pyramiding = 2; + c.process_orders_on_close = false; + c.commission_value = 0.0; + c.slippage = 0; + configure_pine_strategy(c); + set_syminfo_mintick(0.01); + } + void on_source_bar(const Bar& bar) override { + const int m = minute_of_day(current_bar_.timestamp); + const double pos = live_position_size(); + if (m == 15 && pos == 0.0) strategy_entry("L1", true, kNaN, kNaN, 1.0, "long lot 1"); + if (m == 30 && pos > 0.0) strategy_entry("L2", true, kNaN, kNaN, 1.0, "long lot 2"); + if (m == 45 && pos > 0.0) + strategy_entry("SREV", false, kNaN, bar.high, 1.0, "short stop reversal"); + if (m == 75 && pos < 0.0) + strategy_entry("LREV", true, kNaN, bar.low, 1.0, "long stop reversal"); + if (m == 105 && pos != 0.0) strategy_close_all(); + } +}; + +// The same requests on a bare host: the kernel's own excursion model. +class KernelProbe final : public NativeStrategyHost { +public: + void on_native_bar(const Bar& bar, const NativeDecisionContext& ctx) override { + const int m = minute_of_day(ctx.script_bar_open_ms); + const double pos = physical_position().signed_units; + if (m == 15 && pos == 0.0) (void)submit({no::Transact{1.0}, "L1", "long lot 1"}); + if (m == 30 && pos > 0.0) (void)submit({no::Transact{1.0}, "L2", "long lot 2"}); + if (m == 45 && pos > 0.0) + (void)submit({no::Transact{-(pos + 1.0)}, "SREV", "short stop reversal", + no::Stop{bar.high}}); + if (m == 75 && pos < 0.0) + (void)submit({no::Transact{-pos + 1.0}, "LREV", "long stop reversal", + no::Stop{bar.low}}); + if (m == 105 && pos != 0.0) (void)submit({no::Flatten{}, "flat", ""}); + } +}; + +NativeRunSpec kernel_spec(int day) { + NativeRunSpec s; + s.identity = {"e19-tape-day-" + std::to_string(day), 1}; + s.input_tf = "15"; + s.script_tf = "15"; + s.ticker = "ETHUSDT.P"; + s.tickerid = "BINANCE:ETHUSDT.P"; + s.type = "crypto"; + s.currency = "USDT"; + s.basecurrency = "ETH"; + s.timezone = "UTC"; + s.session = "24x7"; + s.initial_capital = 1000000.0; + s.point_value = 1.0; + s.account_fx = 1.0; + s.price_tick = 0.01; + s.fee_kind = NativeFeeKind::Percent; + s.fee_value = 0.0; + return s; +} + +// The measured numbers, per tape row: {adapter favorable, adapter adverse, +// kernel favorable, kernel adverse} as closed-row magnitudes (>= 0). +struct Measured { + int trade; + double a_fav, a_adv, k_fav, k_adv; + const char* cls; +}; +const Measured kMeasured[] = { + {29, 4.98, 10.37, 4.98, 10.37, "control"}, + {30, 0.32, 12.67, 0.32, 12.67, "control"}, + {31, 3.58, 25.21, 3.58, 25.21, "control"}, + {32, 27.07, 0.00, 27.07, 3.22, "A entry-bar over-mask (adverse)"}, + {169, 0.14, 16.35, 0.14, 13.75, "B exit-bar over-fold (adverse)"}, + {170, 11.55, 3.44, 11.55, 0.43, "B exit-bar over-fold (adverse)"}, + {171, 1.47, 13.62, 6.97, 13.62, "A entry-bar over-mask (favorable)"}, + {172, 4.50, 21.77, 4.50, 21.77, "control"}, + {797, 13.64, 5.77, 13.64, 5.77, "C open print before a same-bar stop fill"}, + {798, 7.78, 6.01, 7.78, 6.01, "C open print before a same-bar stop fill"}, + {799, 5.69, 22.27, 5.69, 22.27, "control"}, + {800, 3.08, 24.92, 3.08, 24.92, "control"}, +}; + +// ── D. the calc_on_order_fills scratch ──────────────────────────────────── + +class CoofScratch final : public source::PineStrategyHost { +public: + CoofScratch() { + source::PineStrategyConfig c; + c.initial_capital = 1000000.0; + c.default_qty_type = static_cast(QtyType::FIXED); + c.default_qty_value = 1.0; + c.pyramiding = 1; + c.calc_on_order_fills = true; + configure_pine_strategy(c); + set_syminfo_mintick(0.01); + } + void on_source_bar(const Bar&) override { + if (minute_of_day(current_bar_.timestamp) == 15 && live_position_size() == 0.0) + strategy_entry("L", true); + if (live_position_size() > 0.0) strategy_close("L"); + } +}; + +// The kernel's scratch: the fill's own callback closes the lot at the same +// point through execute_current. +class KernelScratch final : public NativeStrategyHost { +public: + void on_native_bar(const Bar&, const NativeDecisionContext& ctx) override { + if (minute_of_day(ctx.script_bar_open_ms) == 15 && physical_position().signed_units == 0.0) + (void)submit({no::Transact{1.0}, "L", ""}); + } + void on_native_applied(const no::ExecutionAppliedEvent& e, const NativeDecisionContext&) override { + if (e.opened_units <= 0.0 || closing_) return; + closing_ = true; + const auto r = submit({no::Flatten{}, "close", ""}); + if (r.handle) closed_ = std::holds_alternative( + execute_current(NativeCurrentExecution{*r.handle})); + closing_ = false; + } + bool closed_ = false; +private: + bool closing_ = false; +}; + +void test_coof_scratch() { + const std::vector days[] = {day_bars(kCoofDay0), day_bars(kCoofDay1), + day_bars(kCoofDay2), day_bars(kCoofDay3)}; + // The adapter's magnitudes, pinned (the entry bar's extremes folded in). + const double adapter[4][2] = {{4.08, 4.21}, {11.75, 0.0}, {0.0, 11.19}, {6.44, 0.22}}; + int adapter_tv = 0, kernel_tv = 0; + for (const TapeRow& tv : kCoofTape) { + const int d = tv.day; + CoofScratch pine; + pine.run(days[d].data(), static_cast(days[d].size()), "15", "15"); + KernelScratch kernel; + CHECK(kernel.configure_native(kernel_spec(10 + d)).status == NativeSetupStatus::Applied); + kernel.run(days[d].data(), static_cast(days[d].size())); + CHECK(pine.last_error().empty()); + CHECK(kernel.last_error().empty()); + CHECK(kernel.closed_); + CHECK(pine.trade_count() == 1); + CHECK(kernel.trade_count() == 1); + if (pine.trade_count() != 1 || kernel.trade_count() != 1) continue; + const Trade& a = pine.get_trade(0); + const Trade& x = kernel.get_trade(0); + for (const Trade* t : {&a, &x}) { + CHECK(t->entry_time == tv.entry_ms); + CHECK(near(t->entry_price, tv.entry)); + CHECK(near(t->exit_price, tv.exit)); + } + // Same fill, same price. The Pine row is dated at the chart bar's open, + // as TradingView dates it; the bare host's current execution is dated + // at its point, the end of that bar (design doc section 3.7's note: + // bare hosts date rows on the execution point, the adapter on the bar). + CHECK(a.exit_time == tv.exit_ms); + CHECK(x.exit_time == tv.exit_ms + 900000); + const bool a_is_tv = near(a.max_runup, tv.favorable) && near(a.max_drawdown, -tv.adverse); + const bool k_is_tv = near(x.max_runup, tv.favorable) && near(x.max_drawdown, -tv.adverse); + adapter_tv += a_is_tv; + kernel_tv += k_is_tv; + std::printf("coof day%d #%d L %.2f->%.2f TV %.3f/%.3f | adapter %.3f/%.3f%s | kernel %.3f/%.3f%s [D coof scratch]\n", + d, tv.trade, tv.entry, tv.exit, tv.favorable, -tv.adverse, a.max_runup, a.max_drawdown, + a_is_tv ? " =TV" : " !=TV", x.max_runup, x.max_drawdown, k_is_tv ? " =TV" : " !=TV"); + CHECK(near(a.max_runup, adapter[d][0])); + CHECK(near(a.max_drawdown, adapter[d][1])); + CHECK(!a_is_tv); + CHECK(k_is_tv); + } + std::printf("coof scratch rows 4: adapter == TradingView %d, kernel == TradingView %d\n", adapter_tv, kernel_tv); + CHECK(adapter_tv == 0); + CHECK(kernel_tv == 4); +} + +} // namespace + +int main() { + const std::vector days[] = {day_bars(kDay0), day_bars(kDay1), day_bars(kDay2)}; + std::vector pine_rows[3]; + std::vector kernel_rows[3]; + for (int d = 0; d < 3; ++d) { + Probe57 pine; + pine.run(days[d].data(), static_cast(days[d].size()), "15", "15"); + CHECK(pine.last_error().empty()); + for (int i = 0; i < pine.trade_count(); ++i) pine_rows[d].push_back(pine.get_trade(i)); + + KernelProbe kernel; + CHECK(kernel.configure_native(kernel_spec(d)).status == NativeSetupStatus::Applied); + kernel.run(days[d].data(), static_cast(days[d].size())); + CHECK(kernel.last_error().empty()); + for (int i = 0; i < kernel.trade_count(); ++i) kernel_rows[d].push_back(kernel.get_trade(i)); + CHECK(pine_rows[d].size() == 4); + CHECK(kernel_rows[d].size() == 4); + } + + int agree_adapter = 0, agree_kernel = 0, n = 0; + int next[3] = {0, 0, 0}; + for (const TapeRow& tv : kTape) { + const int d = tv.day; + const int k = next[d]++; + if (k >= static_cast(pine_rows[d].size()) || k >= static_cast(kernel_rows[d].size())) { + CHECK(false); + continue; + } + const Trade& a = pine_rows[d][static_cast(k)]; + const Trade& x = kernel_rows[d][static_cast(k)]; + const Measured* m = nullptr; + for (const Measured& r : kMeasured) + if (r.trade == tv.trade) m = &r; + CHECK(m != nullptr); + if (m == nullptr) continue; + ++n; + // The same physical trade on all three: side, both fills, both dates. + for (const Trade* t : {&a, &x}) { + CHECK(t->is_long == tv.is_long); + CHECK(t->entry_time == tv.entry_ms); + CHECK(t->exit_time == tv.exit_ms); + CHECK(near(t->entry_price, tv.entry)); + CHECK(near(t->exit_price, tv.exit)); + CHECK(near(t->qty, tv.qty)); + } + const double tv_fav = tv.favorable, tv_adv = -tv.adverse; + const bool a_is_tv = near(a.max_runup, tv_fav) && near(a.max_drawdown, tv_adv); + const bool k_is_tv = near(x.max_runup, tv_fav) && near(x.max_drawdown, tv_adv); + agree_adapter += a_is_tv; + agree_kernel += k_is_tv; + std::printf("day%d #%d %s %.2f->%.2f TV %.3f/%.3f | adapter %.3f/%.3f%s | kernel %.3f/%.3f%s [%s]\n", + d, tv.trade, tv.is_long ? "L" : "S", tv.entry, tv.exit, tv_fav, tv_adv, + a.max_runup, a.max_drawdown, a_is_tv ? " =TV" : " !=TV", + x.max_runup, x.max_drawdown, k_is_tv ? " =TV" : " !=TV", m->cls); + // Both models, pinned to what they measure today. + CHECK(near(a.max_runup, m->a_fav)); + CHECK(near(a.max_drawdown, m->a_adv)); + CHECK(near(x.max_runup, m->k_fav)); + CHECK(near(x.max_drawdown, m->k_adv)); + const bool control = std::string(m->cls) == "control"; + const bool class_c = m->cls[0] == 'C'; + if (control) { + CHECK(a_is_tv); + CHECK(k_is_tv); + } else if (class_c) { + CHECK(!a_is_tv); + CHECK(!k_is_tv); + } else { + // Classes A and B: the kernel sampler is TradingView's, the + // adapter's host-owned model is not. + CHECK(!a_is_tv); + CHECK(k_is_tv); + } + } + CHECK(n == 12); + std::printf("tape rows %d: adapter == TradingView %d, kernel == TradingView %d\n", + n, agree_adapter, agree_kernel); + CHECK(agree_adapter == 6); + CHECK(agree_kernel == 10); + test_coof_scratch(); + std::printf("test_e19_excursion_tape: %d passed, %d failed\n", passed, failed); + return failed == 0 ? 0 : 1; +} diff --git a/tests/test_pine_dust_sweep_paired.cpp b/tests/test_pine_dust_sweep_paired.cpp new file mode 100644 index 00000000..42fe6323 --- /dev/null +++ b/tests/test_pine_dust_sweep_paired.cpp @@ -0,0 +1,328 @@ +// R5 wave H, lane H-MEASURE, row G2-32: the Pine source host's 1e-10 lot +// sweep, measured beside the kernel's settlement. +// +// After every applied execution PineStrategyHost::on_native_applied erases +// any lot of at most kQtyEpsilon (1e-10) from the book it shares with the +// kernel (pyramid_entries_, position_qty_, position_entry_price_, +// position_entry_count_), with no closing row and no event, and resets the +// position when nothing is left. The kernel's settlement keeps any positive +// remainder as a lot, and a bare NativeStrategyHost keeps it. +// +// ab9714be's rule (src/engine_orders.cpp settle_position_after_partial_exit, +// :531-541, beside the settlement's `split.kept > 0.0` survivors) reset only a +// WHOLE book of at most kQtyEpsilon; a dust lot beside a live one survived. +// The sweep is therefore not a restatement of it: it also erases a dust lot +// inside a live book (section 2). +// +// What TradingView does with such a remnant (section 3): the `lab tv` tape +// hm-g232-decimal-dust (ws-report-v1, rangeProof covered; +// fixtures/g232_decimal_dust) buys 0.1, 0.2 and 1.0 and sells 0.3 every day. +// TradingView's decimal quantities close the 0.1 and 0.2 lots whole and leave +// the 1.0 lot alone, three rows a day. In binary64 0.1 + 0.2 exceeds 0.3, so +// the kernel's FIFO split leaves a 2.8e-17 remnant of the 0.2 lot beside the +// 1.0 lot; the Pine host sweeps it and books TradingView's twelve rows, the +// bare host keeps it and books it as a row of its own at the flatten. +// +// Sections 1 and 2 are the AUDIT4 probes probe_dust / probe_dust3 (a sell of +// 2 - 1e-11 against two and three 1-unit lots), pinned on both hosts. +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; + +#ifndef PINEFORGE_G232_FIXTURE_DIR +#error "PINEFORGE_G232_FIXTURE_DIR must name tests/fixtures/g232_decimal_dust" +#endif + +namespace { + +int passed = 0; +int failed = 0; + +#define CHECK(x) do { \ + if (x) { ++passed; } \ + else { ++failed; std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #x); } \ +} while (0) + +constexpr double kNaN = std::numeric_limits::quiet_NaN(); +constexpr double kSell = 2.0 - 1e-11; +bool near(double a, double b, double tol = 1e-9) { return std::abs(a - b) <= tol; } + +struct FeedBar { + std::int64_t ts; + double open, high, low, close, volume; +}; +#include "fixtures/g232_decimal_dust/bars.inc" + +Bar mk(std::int64_t ts, double o) { + Bar b; + b.open = o; b.high = o + 1; b.low = o - 1; b.close = o + 0.5; b.volume = 1; b.timestamp = ts; + return b; +} + +std::vector synthetic(int n) { + std::vector bars; + for (int i = 0; i < n; ++i) bars.push_back(mk(300000LL * i, 100 + i)); + return bars; +} + +source::PineStrategyConfig fixed_config(int pyramiding) { + source::PineStrategyConfig c; + c.initial_capital = 100000.0; + c.default_qty_type = static_cast(QtyType::FIXED); + c.default_qty_value = 1.0; + c.pyramiding = pyramiding; + c.commission_value = 0.0; + c.slippage = 0; + return c; +} + +NativeRunSpec bare_spec(const std::string& key, const char* tf) { + NativeRunSpec spec; + spec.identity.session_key = key; + spec.identity.run_number = 1; + spec.input_tf = tf; spec.script_tf = tf; + spec.ticker = "ETHUSDT.P"; spec.tickerid = "BINANCE:ETHUSDT.P"; + spec.type = "crypto"; spec.currency = "USDT"; spec.basecurrency = "ETH"; + spec.timezone = "UTC"; spec.session = "24x7"; + spec.initial_capital = 100000.0; spec.point_value = 1.0; spec.account_fx = 1.0; + spec.price_tick = 0.01; spec.fee_kind = NativeFeeKind::Percent; spec.fee_value = 0.0; + return spec; +} + +// ── 1 and 2: the AUDIT4 shapes ───────────────────────────────────────────── + +// `lots` 1-unit entries on bars 0..lots-1, then a sell of 2 - 1e-11. +class PineDust final : public source::PineStrategyHost { +public: + explicit PineDust(int lots) : lots_(lots) { + configure_pine_strategy(fixed_config(lots)); + set_syminfo_mintick(0.01); + } + void on_source_bar(const Bar&) override { + const int i = pine_bar_index(); + if (i < lots_) strategy_entry("L" + std::to_string(i), true); + if (i == lots_) strategy_order("S", false, kSell); + } + using BacktestEngine::pyramid_entries_; + using BacktestEngine::position_qty_; +private: + int lots_; +}; + +class BareDust final : public NativeStrategyHost { +public: + explicit BareDust(int lots) : lots_(lots) {} + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + if (calls_ < lots_) (void)submit_market({no::Transact{1.0}, "L" + std::to_string(calls_), ""}); + if (calls_ == lots_) (void)submit_market({no::Transact{-kSell}, "S", ""}); + ++calls_; + } + using BacktestEngine::pyramid_entries_; + using BacktestEngine::position_qty_; +private: + int lots_; + int calls_ = 0; +}; + +void test_audit_shapes() { + std::printf("1-2. AUDIT4 probe_dust (2 lots) and probe_dust3 (3 lots): sell 2 - 1e-11\n"); + for (int lots : {2, 3}) { + const auto bars = synthetic(lots + 4); + PineDust pine(lots); + pine.run(bars.data(), static_cast(bars.size()), "5", "5"); + BareDust bare(lots); + CHECK(bare.configure_native(bare_spec("g232-dust-" + std::to_string(lots), "5")).status + == NativeSetupStatus::Applied); + bare.run(bars.data(), static_cast(bars.size())); + CHECK(pine.last_error().empty()); + CHECK(bare.last_error().empty()); + const auto pine_lots = pine.native_open_lots(bars.back().close); + const auto bare_lots = bare.native_open_lots(bars.back().close); + std::printf(" %d lots: pine rows=%d lots=%zu position=%.17g | bare rows=%d lots=%zu position=%.17g\n", + lots, pine.trade_count(), pine_lots.size(), pine.position_qty_, + bare.trade_count(), bare_lots.size(), bare.position_qty_); + // Both books charge the sell exactly: rows 1 and 0.99999999999. + for (const BacktestEngine* host : {static_cast(&pine), + static_cast(&bare)}) { + CHECK(host->trade_count() == 2); + if (host->trade_count() == 2) { + CHECK(host->get_trade(0).qty == 1.0); + CHECK(near(host->get_trade(1).qty, 0.99999999999, 1e-15)); + } + } + if (lots == 2) { + // Whole book: the Pine host resets flat -- ab9714be's rule too. + CHECK(pine_lots.empty()); + CHECK(pine.position_qty_ == 0.0); + CHECK(bare_lots.size() == 1); + if (bare_lots.size() == 1) CHECK(near(bare_lots[0].signed_units, 1.000000082740371e-11, 1e-24)); + } else { + // A dust lot INSIDE a live book: the Pine host erases it; the + // kernel (and ab9714be's settle rule) keep it. + CHECK(pine_lots.size() == 1); + CHECK(pine.position_qty_ == 1.0); + CHECK(bare_lots.size() == 2); + CHECK(near(bare.position_qty_, 1.00000000001, 1e-15)); + if (bare_lots.size() == 2) { + CHECK(near(bare_lots[0].signed_units, 1.000000082740371e-11, 1e-24)); + CHECK(bare_lots[1].signed_units == 1.0); + } + } + } +} + +// ── 3: TradingView's decimal quantities ──────────────────────────────────── + +int minute_of_day(std::int64_t ms) { return static_cast((ms / 60000) % 1440); } + +// hm-g232-decimal-dust/strategy.pine, as generated code runs it. +class PineDecimal final : public source::PineStrategyHost { +public: + PineDecimal() { + auto c = fixed_config(3); + c.initial_capital = 1000000.0; + configure_pine_strategy(c); + set_syminfo_mintick(0.01); + } + void on_source_bar(const Bar&) override { + const int m = minute_of_day(current_bar_.timestamp); + const double pos = live_position_size(); + if (m == 15 && pos == 0.0) strategy_entry("A", true, kNaN, kNaN, 0.1); + if (m == 30 && pos > 0.0) strategy_entry("B", true, kNaN, kNaN, 0.2); + if (m == 45 && pos > 0.0) strategy_entry("C", true, kNaN, kNaN, 1.0); + if (m == 60 && pos > 0.0) strategy_order("S", false, 0.3); + if (m == 120 && pos != 0.0) strategy_close_all(); + } +}; + +class BareDecimal final : public NativeStrategyHost { +public: + void on_native_bar(const Bar&, const NativeDecisionContext& ctx) override { + const int m = minute_of_day(ctx.script_bar_open_ms); + const double pos = physical_position().signed_units; + if (m == 15 && pos == 0.0) (void)submit({no::Transact{0.1}, "A", ""}); + if (m == 30 && pos > 0.0) (void)submit({no::Transact{0.2}, "B", ""}); + if (m == 45 && pos > 0.0) (void)submit({no::Transact{1.0}, "C", ""}); + if (m == 60 && pos > 0.0) (void)submit({no::Transact{-0.3}, "S", ""}); + if (m == 120 && pos != 0.0) (void)submit({no::Flatten{}, "flat", ""}); + } +}; + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +struct TvRow { + bool is_long; + std::int64_t entry_ms, exit_ms; + double entry, exit, qty; +}; + +// tv_trades.csv: one row per side, times UTC+8; trades newest last. +std::vector read_tape() { + std::ifstream in(std::string(PINEFORGE_G232_FIXTURE_DIR) + + "/hm-g232-decimal-dust/tv_trades.csv"); + std::vector rows; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 6) continue; + const std::size_t n = static_cast(std::stoi(cell[0])); + if (rows.size() < n) rows.resize(n); + int y, mo, d, h, mi; + if (std::sscanf(cell[2].c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) continue; + const std::int64_t ms = ((days_from_civil(y, static_cast(mo), static_cast(d)) * 24 + + h - 8) * 60 + mi) * 60000LL; + TvRow& row = rows[n - 1]; + row.is_long = cell[1].find("long") != std::string::npos; + row.qty = std::stod(cell[5]); + if (cell[1].rfind("Entry", 0) == 0) { row.entry_ms = ms; row.entry = std::stod(cell[4]); } + else { row.exit_ms = ms; row.exit = std::stod(cell[4]); } + } + return rows; +} + +template +std::vector day_bars(const FeedBar (&rows)[N]) { + std::vector bars; + for (const FeedBar& r : rows) bars.push_back({r.open, r.high, r.low, r.close, r.volume, r.ts}); + return bars; +} + +void test_decimal_tape() { + std::printf("3. TradingView tape hm-g232-decimal-dust: buy 0.1, 0.2, 1.0; sell 0.3; flatten\n"); + const std::vector tv = read_tape(); + CHECK(tv.size() == 12); + const std::vector days[] = {day_bars(kDay0), day_bars(kDay1), day_bars(kDay2), day_bars(kDay3)}; + std::vector pine_rows, bare_rows; + int bare_dust_rows = 0; + for (int d = 0; d < 4; ++d) { + PineDecimal pine; + pine.run(days[d].data(), static_cast(days[d].size()), "15", "15"); + CHECK(pine.last_error().empty()); + for (int i = 0; i < pine.trade_count(); ++i) pine_rows.push_back(pine.get_trade(i)); + CHECK(pine.native_open_lots(days[d].back().close).empty()); + + BareDecimal bare; + CHECK(bare.configure_native(bare_spec("g232-decimal-" + std::to_string(d), "15")).status + == NativeSetupStatus::Applied); + bare.run(days[d].data(), static_cast(days[d].size())); + CHECK(bare.last_error().empty()); + for (int i = 0; i < bare.trade_count(); ++i) { + bare_rows.push_back(bare.get_trade(i)); + if (bare.get_trade(i).qty < 1e-10) ++bare_dust_rows; + } + } + std::printf(" TradingView rows=%zu | Pine host rows=%zu | bare host rows=%zu (of which dust rows %d)\n", + tv.size(), pine_rows.size(), bare_rows.size(), bare_dust_rows); + for (const Trade& t : bare_rows) + if (t.qty < 1e-10) + std::printf(" bare dust row: %s qty=%.17g entry=%.2f exit=%.2f pnl=%.17g\n", + t.entry_id.c_str(), t.qty, t.entry_price, t.exit_price, t.pnl); + // The Pine host books TradingView's rows, trade for trade. + CHECK(pine_rows.size() == tv.size()); + for (std::size_t i = 0; i < tv.size() && i < pine_rows.size(); ++i) { + const Trade& p = pine_rows[i]; + CHECK(p.is_long == tv[i].is_long); + CHECK(p.entry_time == tv[i].entry_ms); + CHECK(p.exit_time == tv[i].exit_ms); + CHECK(near(p.entry_price, tv[i].entry, 1e-6)); + CHECK(near(p.exit_price, tv[i].exit, 1e-6)); + CHECK(near(p.qty, tv[i].qty, 1e-9)); + } + // The bare host keeps the remnant and closes it as a fourth row a day. + CHECK(bare_rows.size() == 16); + CHECK(bare_dust_rows == 4); +} + +} // namespace + +int main() { + test_audit_shapes(); + test_decimal_tape(); + std::printf("test_pine_dust_sweep_paired: %d passed, %d failed\n", passed, failed); + return failed == 0 ? 0 : 1; +} diff --git a/tests/test_session_ismarket_tape.cpp b/tests/test_session_ismarket_tape.cpp new file mode 100644 index 00000000..5d56e73a --- /dev/null +++ b/tests/test_session_ismarket_tape.cpp @@ -0,0 +1,271 @@ +/* + * test_session_ismarket_tape.cpp — R5 wave H, lane H-MEASURE, row G2-36. + * + * Generated Pine code reads `session.ismarket` through the legacy + * time-of-day predicate `pine_session_ismarket` (include/pineforge/ + * session_time.hpp, via PineStrategyHost's class-scope wrapper), while the + * kernel answers the same question as a session-day fact + * (NativeDecisionContext::in_session, which the Pine host also stores as + * session_ismarket_ before every source callback). The two read a session's + * day mask differently: the predicate tests each instant's own weekday, the + * kernel tests the trading date of the session day the instant belongs to. + * + * TradingView's answer (tests/fixtures/session_ismarket, six `lab tv` tapes, + * channel ws-report-v1, rangeProof covered): the probe reverses its position + * at the close of EVERY chart bar (process_orders_on_close), so every entry is + * one chart bar, dated at its open, and its Signal is the flag TradingView + * evaluated there ("M1" in market, "M0" not). On all six tapes — CME_MINI:ES1! + * 60 across both 2025 US DST switches and the Thanksgiving week, OANDA:EURUSD + * 60, OANDA:XAUUSD 60 and BINANCE:ETHUSDT.P 60 — every one of the 1138 bars + * is "M1", the Sunday-evening CME/FX opens included. + * + * What each reading gives on those bars, per host session string: + * 1. the campaign's lane facts (pineforge-lab config/symbol-lanes-v1.json: + * ES1! "1700-1600" America/Chicago, EURUSD "1700-1700" and XAUUSD + * "1800-1700" America/New_York, 24x7 UTC for the crypto perp): the + * predicate, the kernel fact and TradingView agree on every bar; + * 2. the same sessions spelled with TradingView's weekday mask ":23456": + * the kernel still agrees on every bar; the predicate reads each Sunday + * evening open (17:00 CT on ES1!, 17:00 / 18:00 ET on EURUSD / XAUUSD) + * as out of market, because the instant's weekday is Sunday while the + * session day it opens is Monday's; + * 3. a 24-hour day spelled "0000-2400": the kernel reads every bar in + * session, the predicate none ("2400" is not a time of day it parses); + * the TradingView spelling "0000-0000" is the control both read as 24h. + * The predicate's disagreements are pinned bar for bar, so a lane that routes + * generated session.ismarket to the kernel fact moves exactly these rows. + * + * Bars: session.ismarket is a function of the bar's time and the symbol's + * session and timezone only, so each tape is replayed on flat bars stamped at + * the tape's own entry times (every bar TradingView held on that chart). + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; + +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +#ifndef PINEFORGE_G236_FIXTURE_DIR +#error "PINEFORGE_G236_FIXTURE_DIR must name tests/fixtures/session_ismarket" +#endif + +namespace { + +constexpr std::int64_t kMinute = 60'000; + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +// "YYYY-MM-DD HH:MM" at a fixed UTC offset in hours -> UTC milliseconds. +std::int64_t utc_ms(int y, int mo, int d, int h, int mi, int offset_hours) { + const std::int64_t days = days_from_civil(y, static_cast(mo), + static_cast(d)); + return ((days * 24 + h - offset_hours) * 60 + mi) * kMinute; +} + +struct TapeBar { + std::int64_t ts = 0; // the chart bar's open, UTC ms + bool market = false; // TradingView's session.ismarket on it +}; + +// The tape's Entry rows (times UTC+8, the lab tv rendering): one per chart +// bar, in time order; its Signal is the flag. +std::vector read_tape(const char* slug) { + std::ifstream in(std::string(PINEFORGE_G236_FIXTURE_DIR) + "/" + slug + "/tv_trades.csv"); + std::vector bars; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 4 || cell[1].rfind("Entry", 0) != 0) continue; + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + if (std::sscanf(cell[2].c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) continue; + TapeBar bar; + bar.ts = utc_ms(y, mo, d, h, mi, 8); + bar.market = cell[3] == "M1"; + CHECK(cell[3] == "M1" || cell[3] == "M0"); + bars.push_back(bar); + } + // The export lists trades newest first on some channels; order by time. + for (std::size_t i = 1; i < bars.size(); ++i) + for (std::size_t j = i; j > 0 && bars[j - 1].ts > bars[j].ts; --j) + std::swap(bars[j - 1], bars[j]); + return bars; +} + +Bar flat_bar(std::int64_t ts) { + Bar b{}; + b.timestamp = ts; + b.open = 100.0; b.high = 101.0; b.low = 99.0; b.close = 100.5; + b.volume = 1.0; + return b; +} + +struct Seen { + std::int64_t ts = 0; + bool generated = false; // what generated `session.ismarket` evaluates + bool kernel = false; // the kernel's in-session fact the host stores +}; + +class IsMarketHost final : public source::PineStrategyHost { +public: + IsMarketHost(const std::string& session, const std::string& timezone) { + set_syminfo_session(session); + set_syminfo_timezone(timezone); + } + void on_source_bar(const Bar&) override { + // The call generated code emits for `session.ismarket` + // (corpus generated.cpp): the class-scope wrapper over + // session_time.hpp's predicate, at the presented bar's time. + seen.push_back({current_bar_.timestamp, + pine_session_ismarket(syminfo_.session, syminfo_.timezone, + current_bar_.timestamp), + session_ismarket_}); + } + std::vector seen; +}; + +// The kernel alone: a bare host reading NativeDecisionContext::in_session. +class BareHost final : public NativeStrategyHost { +public: + void on_native_bar(const Bar& bar, const NativeDecisionContext& ctx) override { + stamps.push_back(bar.timestamp); + in_session.push_back(ctx.in_session); + } + std::vector stamps; + std::vector in_session; +}; + +int local_weekday(std::int64_t ts, const std::string& tz) { + return pineforge::pine_dayofweek(ts, tz); // 1 = Sunday +} + +struct Case { + const char* slug; + const char* session; + const char* timezone; + int generated_misses; // bars where the predicate differs from TradingView + int sunday_misses; // of which on a local Sunday +}; + +void replay(const Case& c) { + const std::vector tape = read_tape(c.slug); + std::vector bars; + for (const TapeBar& t : tape) bars.push_back(flat_bar(t.ts)); + + IsMarketHost pine(c.session, c.timezone); + pine.run(bars.data(), static_cast(bars.size()), "60", "60"); + + NativeRunSpec spec; + spec.identity.session_key = std::string("g236-") + c.slug; + spec.identity.run_number = 1; + spec.input_tf = "60"; spec.script_tf = "60"; + spec.ticker = "MOCK"; spec.tickerid = "TEST:MOCK"; spec.type = "futures"; + spec.currency = "USD"; spec.timezone = c.timezone; spec.session = c.session; + spec.initial_capital = 1000000.0; spec.point_value = 1.0; spec.account_fx = 1.0; + spec.price_tick = 0.01; spec.fee_kind = NativeFeeKind::Percent; spec.fee_value = 0.0; + spec.slot_label_policy = NativeSlotLabelPolicy::FeedTolerant; + BareHost bare; + const bool configured = bare.configure_native(spec).status == NativeSetupStatus::Applied; + CHECK(configured); + if (configured) bare.run(bars.data(), static_cast(bars.size())); + + int tv_market = 0, kernel_agree = 0, bare_agree = 0, generated_agree = 0; + int generated_misses = 0, sunday_misses = 0; + const bool aligned = pine.seen.size() == tape.size() && bare.stamps.size() == tape.size(); + CHECK(pine.last_error().empty()); + CHECK(bare.last_error().empty()); + CHECK(aligned); + if (aligned) { + for (std::size_t i = 0; i < tape.size(); ++i) { + CHECK(pine.seen[i].ts == tape[i].ts); + CHECK(bare.stamps[i] == tape[i].ts); + tv_market += tape[i].market; + kernel_agree += pine.seen[i].kernel == tape[i].market; + bare_agree += bare.in_session[i] == tape[i].market; + if (pine.seen[i].generated == tape[i].market) { + ++generated_agree; + } else { + ++generated_misses; + if (local_weekday(tape[i].ts, c.timezone) == 1) ++sunday_misses; + } + } + } + const int n = static_cast(tape.size()); + std::printf(" %-28s %-17s %-17s bars=%d tv_M1=%d kernel==tv %d bare==tv %d generated==tv %d" + " (misses %d, on a local Sunday %d)\n", + c.slug, c.session, c.timezone, n, tv_market, kernel_agree, bare_agree, + generated_agree, generated_misses, sunday_misses); + if (!pine.last_error().empty()) std::printf(" pine last_error: %s\n", pine.last_error().c_str()); + if (!bare.last_error().empty()) std::printf(" bare last_error: %s\n", bare.last_error().c_str()); + // TradingView flags every chart bar of these regular-session charts. + CHECK(tv_market == n); + // The kernel fact is TradingView's on every bar, whatever the spelling. + CHECK(kernel_agree == n); + CHECK(bare_agree == n); + // The predicate generated code calls: exactly the pinned misses. + CHECK(generated_misses == c.generated_misses); + CHECK(sunday_misses == c.sunday_misses); +} + +} // namespace + +int main() { + std::printf("TradingView session.ismarket vs the generated predicate vs the kernel fact\n"); + const Case cases[] = { + // 1. the campaign's lane facts: everyone agrees. + {"hm-g236-es1-60-dst-mar", "1700-1600", "America/Chicago", 0, 0}, + {"hm-g236-es1-60-dst-nov", "1700-1600", "America/Chicago", 0, 0}, + {"hm-g236-es1-60-thanksgiving", "1700-1600", "America/Chicago", 0, 0}, + {"hm-g236-eurusd-60-dst-mar", "1700-1700", "America/New_York", 0, 0}, + {"hm-g236-xauusd-60-dst-mar", "1800-1700", "America/New_York", 0, 0}, + {"hm-g236-eth-60-24x7", "24x7", "UTC", 0, 0}, + // 2. the same sessions with a weekday mask: the predicate misses + // every Sunday-evening open. + {"hm-g236-es1-60-dst-mar", "1700-1600:23456", "America/Chicago", 14, 14}, + {"hm-g236-es1-60-dst-nov", "1700-1600:23456", "America/Chicago", 14, 14}, + {"hm-g236-es1-60-thanksgiving", "1700-1600:23456", "America/Chicago", 14, 14}, + {"hm-g236-eurusd-60-dst-mar", "1700-1700:23456", "America/New_York", 14, 14}, + {"hm-g236-xauusd-60-dst-mar", "1800-1700:23456", "America/New_York", 12, 12}, + // 3. a 24-hour day: "0000-2400" (predicate: never) and the + // TradingView spelling "0000-0000" (both: always). + {"hm-g236-eth-60-24x7", "0000-2400", "UTC", 97, 24}, + {"hm-g236-eth-60-24x7", "0000-0000", "UTC", 0, 0}, + }; + for (const Case& c : cases) replay(c); + std::printf("%d passed, %d failed\n", tests_passed, tests_failed); + return tests_failed == 0 ? 0 : 1; +} diff --git a/tests/test_short_seed_report_swap.cpp b/tests/test_short_seed_report_swap.cpp new file mode 100644 index 00000000..bd9ab66a --- /dev/null +++ b/tests/test_short_seed_report_swap.cpp @@ -0,0 +1,379 @@ +// R5 wave H, lane H-MEASURE, row G2-21: the short-seed report swaps, measured +// against the kernel's own lot identities. +// +// Finding 272's short-seed collision (tests/oracle/test_oracle_short_seed_percent.cpp): +// a SHORT seed S held, then in one bar entry(Long); entry(Short); +// close(Long); close(Short) under percent-of-equity (or cash) default +// sizing. The adapter submits the three live objects so that generic matching +// executes the "__close__Short" artifact BEFORE the final Short +// (PineExecutionAdapter::flush_pending_same_bar_commands arms the +// ShortSeedPlan); their acceptance handles are therefore in the opposite +// order to ab9714be's source report incarnations. The kernel books every row +// with the incarnation of the lot it closed (build_close_trade_with_costs, +// the shared producer); PineStrategyHost::project_short_seed_report_rows then +// swaps two rows' entry_incarnation -- every row still carrying the +// artifact's handle takes the final Short's, and the rows the closing event +// booked under the final Short's handle take the artifact's -- once the +// remnant is closed by a close of the seed id, and only for a non-FIXED +// default (report_swap_pending, pine_adapter.cpp). Nothing else of any row +// changes: the v19 closed-row digest folds no incarnation, and the H-MEASURE +// mutant that skips the swap (instrumented scratch build) fails exactly the +// two incarnation checks of test_native_oracle_short_seed_percent_full_l2. +// +// This row runs the oracle's remnant case once and reads both sides of the +// same run: the kernel's identities (AcceptedEvent handles, each +// ExecutionAppliedEvent's opened lot, native_open_lots before the remnant is +// closed) and the adapter's projected rows. A second case closes nothing +// after the collision: the swap stays pending and the rows keep the +// kernel's identities. TradingView exports no incarnation, so no tape can +// measure this projection; it is a report identity for the C ABI +// (strategy_closed_trade_entry_incarnation) and the corpus CSV's "Engine +// entry incarnation" column, where the grader's distinct-entry gate reads +// only whether two same-key entries differ -- true before and after the swap. +// +// Section 3 replays the `lab tv` tape hm-g221-short-seed (ws-report-v1, +// rangeProof covered; fixtures/g221_short_seed) -- the same collision once a +// day on BINANCE:ETHUSDT.P 15m, 2025-06-02..05, on the corpus feed: the Pine +// host books TradingView's 13 rows (three collisions that end flat, one that +// leaves a remnant) row for row, and the one day whose remnant a close of the +// seed id closes is the one day the swap runs. +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef PINEFORGE_G221_FIXTURE_DIR +#error "PINEFORGE_G221_FIXTURE_DIR must name tests/fixtures/g221_short_seed" +#endif + +using namespace pineforge; +namespace no = pineforge::native_order; + +namespace { + +int passed = 0; +int failed = 0; + +#define CHECK(x) do { \ + if (x) { ++passed; } \ + else { ++failed; std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #x); } \ +} while (0) + +Bar make_bar(double o, double h, double l, double c, std::int64_t ts) { return {o, h, l, c, 1000.0, ts}; } + +class ShortSeed final : public source::PineStrategyHost { +public: + explicit ShortSeed(bool close_remnant) : close_remnant_(close_remnant) { + source::PineStrategyConfig c; + c.initial_capital = 1000000.0; + c.default_qty_type = static_cast(QtyType::PERCENT_OF_EQUITY); + c.default_qty_value = 10.0; + c.pyramiding = 1; + c.commission_value = 0.0; + c.slippage = 0; + configure_pine_strategy(c); + fixture_retain_all_events(); + } + void on_source_bar(const Bar& bar) override { + const int i = pine_bar_index(); + if (i == 0) strategy_entry("Short", false); + if (i == 1) { + strategy_entry("Long", true); + strategy_entry("Short", false); + strategy_close("Long"); + strategy_close("Short"); + } + if (i == 2) { + lots_before_close = native_open_lots(bar.close); + if (close_remnant_) strategy_close("Short"); + } + } + std::vector lots_before_close; +private: + bool close_remnant_; +}; + +struct Identities { + std::optional seed, long_entry, final_short, artifact; + std::vector applied; +}; + +Identities read_identities(const ShortSeed& host) { + Identities out; + for (const auto& row : host.native_events(0)) { + if (!row.command) continue; + if (const auto* accepted = std::get_if(&*row.command)) { + const std::string& label = accepted->request().label; + const std::uint64_t inc = accepted->handle().incarnation; + if (label == "Short") (out.seed ? out.final_short : out.seed) = inc; + if (label == "Long") out.long_entry = inc; + // The first "__close__Short" is the collision's artifact; a later + // one is the script's close of the remnant. + if (label == "__close__Short" && !out.artifact) out.artifact = inc; + } else if (const auto* applied = std::get_if(&*row.command)) { + out.applied.push_back(*applied); + } + } + return out; +} + +// The lot each applied event opened, by the handle that opened it. +std::optional opened_by(const Identities& ids, std::uint64_t handle) { + for (const auto& a : ids.applied) + if (a.handle().incarnation == handle && a.opened_units != 0.0) return a.opened_lot_incarnation; + return std::nullopt; +} + +void test_remnant_closed() { + std::printf("1. remnant closed by close(\"Short\"): the adapter swaps two rows' identities\n"); + const Bar bars[] = { + make_bar(100.0, 100.0, 100.0, 100.0, 600'000), + make_bar(100.0, 100.5, 89.5, 90.0, 1'200'000), + make_bar(90.0, 90.5, 89.5, 90.0, 1'800'000), + make_bar(90.0, 90.5, 89.5, 90.0, 2'400'000), + make_bar(90.0, 90.0, 90.0, 90.0, 3'000'000), + }; + ShortSeed host(true); + host.run(bars, 5); + CHECK(host.last_error().empty()); + const Identities ids = read_identities(host); + CHECK(ids.seed && ids.long_entry && ids.final_short && ids.artifact); + CHECK(host.trade_count() == 4); + if (!(ids.seed && ids.long_entry && ids.final_short && ids.artifact) || host.trade_count() != 4) return; + const std::uint64_t L = *ids.long_entry, F = *ids.final_short, A = *ids.artifact; + std::printf(" handles: seed=%llu Long=%llu final Short=%llu __close__Short=%llu\n", + (unsigned long long)*ids.seed, (unsigned long long)L, + (unsigned long long)F, (unsigned long long)A); + // The adapter submits the artifact before the final Short. + CHECK(A == L + 1); + CHECK(F == L + 2); + // The kernel's identities: the artifact's lot and the final Short's lot + // are each booked under their own handle; before the remnant is closed the + // kernel book holds the final Short's lot alone. + CHECK(opened_by(ids, A) == std::optional(A)); + CHECK(opened_by(ids, F) == std::optional(F)); + CHECK(host.lots_before_close.size() == 1); + if (host.lots_before_close.size() == 1) { + CHECK(host.lots_before_close[0].entry_incarnation == F); + CHECK(host.lots_before_close[0].entry_label == "Short"); + } + const Trade& seed = host.get_trade(0); + const Trade& zero1 = host.get_trade(1); + const Trade& zero2 = host.get_trade(2); + const Trade& remnant = host.get_trade(3); + for (int i = 0; i < 4; ++i) { + const Trade& t = host.get_trade(i); + std::printf(" row %d %s %s -> %s qty=%.6f incarnation=%llu\n", i, t.is_long ? "L" : "S", + t.entry_id.c_str(), t.exit_id.c_str(), t.qty, (unsigned long long)t.entry_incarnation); + } + CHECK(seed.entry_id == "Short" && seed.entry_incarnation == *ids.seed); + CHECK(zero1.entry_id == "Long" && zero1.entry_incarnation == L); + // The adapter's projection: the artifact row reports the final Short's + // handle, the remnant row the artifact's -- the kernel booked each the + // other way round (A and F above). ab9714be's numbering: Long, Long+1, + // Long+2 (the oracle's `remnant == zero1 + 1`, `zero2 == zero1 + 2`). + CHECK(zero2.entry_id == "__close__Short"); + CHECK(zero2.entry_incarnation == F); + CHECK(zero2.entry_incarnation != *opened_by(ids, A)); + CHECK(remnant.entry_id == "Short"); + CHECK(remnant.entry_incarnation == A); + CHECK(remnant.entry_incarnation != host.lots_before_close[0].entry_incarnation); + CHECK(remnant.entry_incarnation == zero1.entry_incarnation + 1); + CHECK(zero2.entry_incarnation == zero1.entry_incarnation + 2); +} + +void test_remnant_open() { + std::printf("2. nothing closes the remnant: the swap stays pending, rows keep the kernel's identities\n"); + const Bar bars[] = { + make_bar(100.0, 100.0, 100.0, 100.0, 600'000), + make_bar(100.0, 100.5, 89.5, 90.0, 1'200'000), + make_bar(90.0, 90.5, 89.5, 90.0, 1'800'000), + make_bar(90.0, 90.5, 89.5, 90.0, 2'400'000), + }; + ShortSeed host(false); + host.run(bars, 4); + CHECK(host.last_error().empty()); + const Identities ids = read_identities(host); + CHECK(ids.long_entry && ids.final_short && ids.artifact); + if (!(ids.long_entry && ids.final_short && ids.artifact)) return; + const std::uint64_t A = *ids.artifact; + int artifact_rows = 0; + for (int i = 0; i < host.trade_count(); ++i) { + const Trade& t = host.get_trade(i); + std::printf(" row %d %s %s -> %s qty=%.6f incarnation=%llu%s\n", i, t.is_long ? "L" : "S", + t.entry_id.c_str(), t.exit_id.c_str(), t.qty, + (unsigned long long)t.entry_incarnation, t.open_at_end ? " (range end)" : ""); + if (t.entry_id == "__close__Short") { + ++artifact_rows; + CHECK(t.entry_incarnation == A); // the kernel's, unswapped + } + } + CHECK(artifact_rows == 1); + // The remnant is still open at the range's end: its report row (the + // shared producer's range-end row) carries the final Short's lot handle. + // ab9714be's numbering would have given the artifact row Long + 2 and the + // remnant Long + 1 here as well; the swap never runs, so neither moves. + bool range_end_remnant = false; + for (int i = host.trade_count(); i < host.report_trade_count(); ++i) { + const Trade& t = host.get_report_trade(i); + std::printf(" report row %d %s %s qty=%.6f incarnation=%llu%s\n", i, t.is_long ? "L" : "S", + t.entry_id.c_str(), t.qty, (unsigned long long)t.entry_incarnation, + t.open_at_end ? " (range end)" : ""); + if (t.open_at_end && t.entry_id == "Short") { + range_end_remnant = true; + CHECK(t.entry_incarnation == *ids.final_short); + } + } + CHECK(range_end_remnant); +} + +// ── 3. TradingView's tape ──────────────────────────────────────────────── + +struct FeedBar { + std::int64_t ts; + double open, high, low, close, volume; +}; +#include "fixtures/g221_short_seed/bars.inc" + +int minute_of_day(std::int64_t ms) { return static_cast((ms / 60000) % 1440); } + +// hm-g221-short-seed/strategy.pine as generated code runs it. +class TapeShortSeed final : public source::PineStrategyHost { +public: + TapeShortSeed() { + source::PineStrategyConfig c; + c.initial_capital = 1000000.0; + c.default_qty_type = static_cast(QtyType::PERCENT_OF_EQUITY); + c.default_qty_value = 10.0; + c.pyramiding = 1; + c.commission_value = 0.0; + c.slippage = 0; + configure_pine_strategy(c); + set_syminfo_mintick(0.01); + fixture_retain_all_events(); + } + void on_source_bar(const Bar&) override { + const int m = minute_of_day(current_bar_.timestamp); + const double pos = live_position_size(); + if (m == 15 && pos == 0.0) strategy_entry("Short", false); + if (m == 30 && pos < 0.0) { + strategy_entry("Long", true); + strategy_entry("Short", false); + strategy_close("Long"); + strategy_close("Short"); + } + if (m == 60 && pos != 0.0) strategy_close("Short"); + if (m == 90 && pos != 0.0) strategy_close_all(); + } +}; + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +struct TvRow { + bool is_long = false; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry = 0, exit = 0, qty = 0; + std::string entry_signal, exit_signal; +}; + +std::vector read_tape() { + std::ifstream in(std::string(PINEFORGE_G221_FIXTURE_DIR) + "/hm-g221-short-seed/tv_trades.csv"); + std::vector rows; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 6) continue; + const std::size_t n = static_cast(std::stoi(cell[0])); + if (rows.size() < n) rows.resize(n); + int y, mo, d, h, mi; + if (std::sscanf(cell[2].c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) continue; + const std::int64_t ms = ((days_from_civil(y, static_cast(mo), static_cast(d)) * 24 + + h - 8) * 60 + mi) * 60000LL; + TvRow& row = rows[n - 1]; + row.is_long = cell[1].find("long") != std::string::npos; + row.qty = std::stod(cell[5]); + if (cell[1].rfind("Entry", 0) == 0) { row.entry_ms = ms; row.entry = std::stod(cell[4]); row.entry_signal = cell[3]; } + else { row.exit_ms = ms; row.exit = std::stod(cell[4]); row.exit_signal = cell[3]; } + } + return rows; +} + +void test_tape() { + std::printf("3. TradingView tape hm-g221-short-seed: four collisions, 13 rows\n"); + std::vector bars; + for (const FeedBar& r : kBars) bars.push_back({r.open, r.high, r.low, r.close, r.volume, r.ts}); + TapeShortSeed host; + host.run(bars.data(), static_cast(bars.size()), "15", "15"); + CHECK(host.last_error().empty()); + const std::vector tv = read_tape(); + CHECK(tv.size() == 13); + CHECK(host.trade_count() == static_cast(tv.size())); + int swapped = 0; + for (std::size_t i = 0; i < tv.size() && static_cast(i) < host.trade_count(); ++i) { + const Trade& t = host.get_trade(static_cast(i)); + std::printf(" #%zu %s %s -> %s qty=%.4f (TV %.4f, %s -> %s) incarnation=%llu\n", i + 1, + t.is_long ? "L" : "S", t.entry_id.c_str(), t.exit_id.c_str(), t.qty, tv[i].qty, + tv[i].entry_signal.c_str(), tv[i].exit_signal.c_str(), + (unsigned long long)t.entry_incarnation); + CHECK(t.is_long == tv[i].is_long); + CHECK(t.entry_time == tv[i].entry_ms); + CHECK(t.exit_time == tv[i].exit_ms); + CHECK(std::abs(t.entry_price - tv[i].entry) < 1e-6); + CHECK(std::abs(t.exit_price - tv[i].exit) < 1e-6); + // TradingView prints the size truncated to 4 decimals (39.5353 for the + // engine's 39.53538...); the sizing itself is not this row's subject. + CHECK(std::abs(t.qty - tv[i].qty) < 1e-4); + } + // The remnant day (trades 8..10): the artifact row reports a handle one + // above the remnant's -- ab9714be's numbering -- only because the swap ran. + if (host.trade_count() == 13) { + const Trade& zero1 = host.get_trade(7); + const Trade& zero2 = host.get_trade(8); + const Trade& remnant = host.get_trade(9); + CHECK(zero2.entry_id == "__close__Short"); + CHECK(remnant.entry_id == "Short" && !remnant.is_long); + CHECK(remnant.entry_incarnation == zero1.entry_incarnation + 1); + CHECK(zero2.entry_incarnation == zero1.entry_incarnation + 2); + swapped = zero2.entry_incarnation > remnant.entry_incarnation; + // The three flat days keep the kernel's numbering: artifact = Long + 1. + for (int first : {1, 4, 11}) { + const Trade& l = host.get_trade(first); + const Trade& a = host.get_trade(first + 1); + CHECK(l.entry_id == "Long" && a.entry_id == "__close__Short"); + CHECK(a.entry_incarnation == l.entry_incarnation + 1); + } + } + CHECK(swapped == 1); +} + +} // namespace + +int main() { + test_remnant_closed(); + test_remnant_open(); + test_tape(); + std::printf("test_short_seed_report_swap: %d passed, %d failed\n", passed, failed); + return failed == 0 ? 0 : 1; +} From 8312ea2b3fdc8ca211e7d2d6fdca28812a9b9d88 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:20:40 +0800 Subject: [PATCH 04/83] H-MEASURE: F1(e) tape row, F1's magnified residual, request.security routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUDIT4 X14's aggregation rows, measured on the K-IDX tree (the kernel now books lots, rows and interval_index in script-bar space): - F1(e), tests/test_aggregated_entry_bar_index_tape.cpp: TradingView's own tape of a process_orders_on_close script whose every exit reads strategy.opentrades.entry_bar_index / closedtrades.exit_bar_index (lab tv hm-f1e-ebi-pooc-eth and its magnifier-on twin, BINANCE:ETHUSDT.P 15m), replayed on the chart, the plain aggregated (corpus 1m -> 15m) and the magnified path: 26 checks, 0 failures. With the four booking sites of e362bc4b reverted in a scratch build (the kernel books input slots again, and nothing re-stamps them) it fails 10 of 24 checks: no held exit fires, A/B/C close at their timeouts, D never enters. No corpus probe runs an aggregated process_orders_on_close script that reads a bar index; this engine row is the parity witness until the supervisor adds one. - F1's magnified residual, tests/test_magnified_aggregated_tape.cpp: instrumented, the three entry-bar tests of a magnified aggregated run now compare script bars with script bars (0 of 5170 out of space, 4996 on 91d65ad6); sixteen lab tv tapes of lane F1's differential script book its 12 rows as this engine does on every path (ab9714be's 9 were its own error). The variants the magnified run books exactly are asserted at chart-bar granularity; TradingView dates a magnified fill at its chart bar's open (218 of 218), a recorded presentation divergence. - M20 / G2-23, tests/test_adapter_security_route_conditions.cpp: every condition that keeps request.security on the source evaluator, each reachable one tripped alone against a control site, and where a kernel subscription spells the same site, the evaluator's value against a bare kernel host's on every chart bar. ADR-0001's row named four classes; the predicate has sixteen conditions, now listed and measured. Docs: design §3.8 (the gate that can now see F1's regression, AG1's TradingView proof, AG2's magnified dating, the magnified residual closed), ADR-0001's subscriptions row and native-engine.md's route paragraph; one design anchor into native-engine.md re-anchored. No src/ or include/ change. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 206a739d2d9a997ae0516064e668ea48979a89ca) INT26 pick note: the ADR conflicted on citation digits only (int16_digit_merge, digits carried from the pre-pick tree). hunkcheck: every hunk of 206a739d carried; the ADR differs from its diff in anchor digits only. --- docs/adr/0001-kernel-adapter-boundary.md | 2 +- docs/design/native-feature-parity.md | 28 +- docs/pages/native-engine.md | 10 +- tests/CMakeLists.txt | 8 + .../aggregated_entry_bar_index/README.md | 29 + .../aggregated_entry_bar_index/bars.inc | 842 ++++++++++++++++++ .../hm-f1e-ebi-pooc-eth-mag/meta.json | 17 + .../hm-f1e-ebi-pooc-eth-mag/metrics.json | 40 + .../hm-f1e-ebi-pooc-eth-mag/strategy.pine | 43 + .../hm-f1e-ebi-pooc-eth-mag/tv_trades.csv | 9 + .../hm-f1e-ebi-pooc-eth/meta.json | 17 + .../hm-f1e-ebi-pooc-eth/metrics.json | 40 + .../hm-f1e-ebi-pooc-eth/strategy.pine | 42 + .../hm-f1e-ebi-pooc-eth/tv_trades.csv | 9 + tests/fixtures/magnified_aggregated/README.md | 23 + .../hm-chart-diff-v0/meta.json | 17 + .../hm-chart-diff-v0/metrics.json | 40 + .../hm-chart-diff-v0/strategy.pine | 43 + .../hm-chart-diff-v0/tv_trades.csv | 25 + .../hm-chart-diff-v4/meta.json | 17 + .../hm-chart-diff-v4/metrics.json | 40 + .../hm-chart-diff-v4/strategy.pine | 43 + .../hm-chart-diff-v4/tv_trades.csv | 29 + .../hm-mag-diff-v0/meta.json | 17 + .../hm-mag-diff-v0/metrics.json | 40 + .../hm-mag-diff-v0/strategy.pine | 43 + .../hm-mag-diff-v0/tv_trades.csv | 25 + .../hm-mag-diff-v2/meta.json | 17 + .../hm-mag-diff-v2/metrics.json | 40 + .../hm-mag-diff-v2/strategy.pine | 43 + .../hm-mag-diff-v2/tv_trades.csv | 25 + .../hm-mag-diff-v4/meta.json | 17 + .../hm-mag-diff-v4/metrics.json | 40 + .../hm-mag-diff-v4/strategy.pine | 43 + .../hm-mag-diff-v4/tv_trades.csv | 29 + ...test_adapter_security_route_conditions.cpp | 435 +++++++++ .../test_aggregated_entry_bar_index_tape.cpp | 431 +++++++++ tests/test_magnified_aggregated_tape.cpp | 311 +++++++ 38 files changed, 2963 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/aggregated_entry_bar_index/README.md create mode 100644 tests/fixtures/aggregated_entry_bar_index/bars.inc create mode 100644 tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/meta.json create mode 100644 tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/metrics.json create mode 100644 tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/strategy.pine create mode 100644 tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/tv_trades.csv create mode 100644 tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/meta.json create mode 100644 tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/metrics.json create mode 100644 tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/strategy.pine create mode 100644 tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/tv_trades.csv create mode 100644 tests/fixtures/magnified_aggregated/README.md create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v0/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v0/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v0/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v0/tv_trades.csv create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v4/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v4/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v4/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v4/tv_trades.csv create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v0/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v0/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v0/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v0/tv_trades.csv create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v2/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v2/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v2/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v2/tv_trades.csv create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v4/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v4/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v4/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v4/tv_trades.csv create mode 100644 tests/test_adapter_security_route_conditions.cpp create mode 100644 tests/test_aggregated_entry_bar_index_tape.cpp create mode 100644 tests/test_magnified_aggregated_tape.cpp diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 22513a96..5b240eaa 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -599,7 +599,7 @@ Every line number in this section names its symbol on this tree, which | `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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end`): report shape, not a mark-to-market row (`sha256:71a6a8622f560fca3a27d37e098ea8b6f68f831abcca0b6c46ad004d3ffe9358` pine_adapter.cpp:2334-2341) | 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:2325-2327`) | 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:1583`): 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 sites the predicate leaves out (lower timeframe, lookahead, auxiliary, streams) keep the source evaluator | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | +| `subscriptions` | **adapter-hook** | `declare_timeframe_subscriptions` from the begin-time hook (`pine_strategy_host.cpp:1583`): 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 runs the predicate leaves out keep every site on the source evaluator: a stream, the chart-timeframe magnifier, an aggregated chart, the KI-55 range-start cut, the historical lookahead projection, an auxiliary feed, a `lookahead_on` or Heikin-Ashi or lower-timeframe site, a forex/cfd intraday daily request (the OTC pins), a registry whose `sec_id`s are not dense from 0, and a pairing the kernel refuses. 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 close ending within it of an interior lot boundary is one selected `Flatten` of whole lots (`source_fifo_prefix_openings`, `src/source/pine_adapter.cpp`), 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 | `tests/test_native_quantity_tolerance.cpp` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index cd176cab..b27c84e8 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -293,7 +293,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2925-2992) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2933-3000) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `sha256:60a689e9bbb16d77370ee18557929c420ee846d162b7d2bcd5472ffd74409e69` runner/CMakeLists.txt:37-45, `sha256:e7b0011fb5e730a159692b9f8ab97b2d43bd386e7c4802f13a4ae4c1db083c80` check_native_include_independence.py:36-39, the tests at `sha256:2c88205b43a02dafe8915e74417743b3b3faa0c65595686ccd305386505bdf55` runner/CMakeLists.txt:98-128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -873,7 +873,13 @@ regressions that R4 slice C (`73817c1d`) introduced, two of them on a chart whose script timeframe is coarser than its input feed (an *aggregated* chart). No gate could see them: neither aggregated corpus probe reads a bar index or runs `process_orders_on_close`, and the two SPY session probes have -no baseline rows. The oracle is the pre-R4 engine `ab9714be`, whose plain +no baseline rows. Since R5 lane H-MEASURE a CTest row does: +`tests/test_aggregated_entry_bar_index_tape.cpp` replays TradingView's own +tapes of a `process_orders_on_close` script whose every exit reads +`entry_bar_index` / `exit_bar_index` (`lab tv` `hm-f1e-ebi-pooc-eth`, and its +magnifier-on twin) on the chart, the plain aggregated and the magnified path, +and fails 10 of its 24 checks against the kernel's pre-K-IDX input-slot +booking (the four booking sites of `e362bc4b` reverted in a scratch build). The oracle is the pre-R4 engine `ab9714be`, whose plain aggregated run of one script equals its chart-timeframe run field for field, plus TradingView's own tapes (`tests/fixtures/session_islastbar`). @@ -888,8 +894,8 @@ this lane re-pins the affected v19 witnesses once. | ID | What TradingView and `ab9714be` do | What R4 slice C did | Owner and fix | Proof | |---|---|---|---|---| -| AG1 | A trade row counts **chart** bars: its entry and exit bar are the `bar_index` the script read there, so `bar_index - strategy.opentrades.entry_bar_index(0)` is the bars held, and a tape row one chart bar long says "Duration (bars)" 1 | The pre-K-IDX kernel used the first contributing INPUT slot as its public interval index on an aggregated run. That made a two-script-bar trade report input indices and made `avg_bars_in_trade` count input slots. | **Kernel (Option A).** `NativeExecutionConsumer::ScriptBucket::script_index` assigns the sequential script index when a bucket seals. `NativeCoordinate::interval_index`, `PyramidEntry::entry_bar_index`, `Trade::exit_bar_index`, open-lot rows and the metrics calculation all consume that script index. `NativeCoordinate::input_interval_index` preserves the first input slot. `PineStrategyHost::on_native_applied` no longer rewrites lots or rows; Pine's excursion and cohort sampling still pass their explicit chart source index to the source-layer sampler. | `kc_a310` / `kc_a310_c` in the K-IDX scratch: batch, stream and both magnified modes report `0/1/2`, entry/exit `1/2`, avg `2`; `tests/test_aggregated_path_regressions.cpp` and `tests/test_pooc_fill_stamp_aggregation.cpp` remain green. | -| AG2 | Every fill of a chart bar is dated at the bar's **open**: the e25-f-islastbar tape dates all 510 of its `process_orders_on_close` rows there, and `ab9714be`'s aggregated rows equal its chart rows | The kernel dates a close point (the calculation, the close after it, a modeled bar's close leg) at `calculation_time()`, the bucket's close, and a fill the host executes at the current point (a `calc_on_order_fills` first-open chain, a same-bar close-then-entry, the KI-62 cover) at its decision floor, which on the no-path route the bucket's inputs have already carried to the close. The chart timeframe's zero-width interval hides both, since every point of a bar sits at its label | **Adapter, at projection time.** `on_native_applied` dates the lot a fill opened (`PyramidEntry::time`, hence `Trade::entry_time`) and the rows it closed (`Trade::exit_time`) at the chart bar's open: on a plain aggregated chart every point of a confirmed bar's modeled path (its open, the segments, the calculation, the close after it), where the chart timeframe's own points all sit; under the magnifier the close points alone (lane E27's measured cut), so its sub-bar fills keep their sub-bar instant as `ab9714be` dated them. A realtime print keeps its own instant, as the chart-timeframe stream keeps it, and so does a stream's quiet carried interval | the audit's pooc probe rows (`ab9714be`'s 19:45Z / 13:30Z on all three paths), row 4, the KI-62 row and the stream row (a chart-timeframe stream and the same stream fed 1m agree on every row, a 10:30:01 print fill and an 11:00 carried fill included); E27's stamp rows flipped. Measured: the chart path unchanged byte for byte, the magnified run moves only its POOC close-fill instants | +| AG1 | A trade row counts **chart** bars: its entry and exit bar are the `bar_index` the script read there, so `bar_index - strategy.opentrades.entry_bar_index(0)` is the bars held, and a tape row one chart bar long says "Duration (bars)" 1 | The pre-K-IDX kernel used the first contributing INPUT slot as its public interval index on an aggregated run. That made a two-script-bar trade report input indices and made `avg_bars_in_trade` count input slots. | **Kernel (Option A).** `NativeExecutionConsumer::ScriptBucket::script_index` assigns the sequential script index when a bucket seals. `NativeCoordinate::interval_index`, `PyramidEntry::entry_bar_index`, `Trade::exit_bar_index`, open-lot rows and the metrics calculation all consume that script index. `NativeCoordinate::input_interval_index` preserves the first input slot. `PineStrategyHost::on_native_applied` no longer rewrites lots or rows; Pine's excursion and cohort sampling still pass their explicit chart source index to the source-layer sampler. | `kc_a310` / `kc_a310_c` in the K-IDX scratch: batch, stream and both magnified modes report `0/1/2`, entry/exit `1/2`, avg `2`; `tests/test_aggregated_path_regressions.cpp` and `tests/test_pooc_fill_stamp_aggregation.cpp` remain green. TradingView: `tests/test_aggregated_entry_bar_index_tape.cpp` (R5 lane H-MEASURE), a tape whose exits read the indices, booked field for field on all three paths. | +| AG2 | Every fill of a chart bar is dated at the bar's **open**: the e25-f-islastbar tape dates all 510 of its `process_orders_on_close` rows there, and `ab9714be`'s aggregated rows equal its chart rows | The kernel dates a close point (the calculation, the close after it, a modeled bar's close leg) at `calculation_time()`, the bucket's close, and a fill the host executes at the current point (a `calc_on_order_fills` first-open chain, a same-bar close-then-entry, the KI-62 cover) at its decision floor, which on the no-path route the bucket's inputs have already carried to the close. The chart timeframe's zero-width interval hides both, since every point of a bar sits at its label | **Adapter, at projection time.** `on_native_applied` dates the lot a fill opened (`PyramidEntry::time`, hence `Trade::entry_time`) and the rows it closed (`Trade::exit_time`) at the chart bar's open: on a plain aggregated chart every point of a confirmed bar's modeled path (its open, the segments, the calculation, the close after it), where the chart timeframe's own points all sit; under the magnifier the close points alone (lane E27's measured cut), so its sub-bar fills keep their sub-bar instant as `ab9714be` dated them. A realtime print keeps its own instant, as the chart-timeframe stream keeps it, and so does a stream's quiet carried interval | the audit's pooc probe rows (`ab9714be`'s 19:45Z / 13:30Z on all three paths), row 4, the KI-62 row and the stream row (a chart-timeframe stream and the same stream fed 1m agree on every row, a 10:30:01 print fill and an 11:00 carried fill included); E27's stamp rows flipped. Measured: the chart path unchanged byte for byte, the magnified run moves only its POOC close-fill instants. TradingView itself dates a magnified fill at its chart bar's open too (218 of 218 fills on sixteen `lab tv` tapes, R5 lane H-MEASURE), where the magnified run keeps a sub-bar fill's own instant: a recorded presentation divergence; `tests/test_magnified_aggregated_tape.cpp` compares those rows at chart-bar granularity | | AG3 | A `calc_on_order_fills` recalculation runs the script **on the bar the fill is on** and reads that bar's `session.ismarket` / `isfirstbar` / `islastbar`; `ab9714be` set them before any fill of the bar | The scheduler set the flags in the close callback only, so a recalculation published the PREVIOUS bar's: a fill at the next session day's 09:30 read `islastbar`. On the chart timeframe the close then read its lookahead from a count the recalculation had already advanced, one retained bar too far: a fill on 15:30 flagged 15:30 `islastbar` and, through E26's dual, 15:45 `isfirstbar` | **Adapter** (Pine language state, OT7). `scheduler_update_session_state` stays the one writer and runs once per script bar, at the bar's first publication: `PineScheduler::recalculate` asks before the script runs, `PineScheduler::bar` pine_scheduler_native.cpp:446 only when no recalculation published the bar. Since R5 lane F5 it selects the kernel's session-day facts of that bar, which every callback of the bar carries alike, so the scheduler's lookahead is gone. E26's session-day rule and its `isfirstbar` dual are unchanged | the audit's coof_session probes and the 15:30 case in `tests/test_aggregated_path_regressions.cpp`; section 7 of `tests/test_session_islastbar_aggregation.cpp` (TradingView's NYSE:F days with `calc_on_order_fills`, chart, aggregated and magnified: every callback of a bar reads the tape's flags) | **Recorded, not changed here.** @@ -898,6 +904,20 @@ this lane re-pins the affected v19 witnesses once. same-bar cohort checks still compare against the explicit chart source index selected by the Pine sampler. They no longer mutate the kernel's script-bar lot index; the K-IDX probes cover the kernel index and input companion. + **Closed (R5 lane H-MEASURE):** all three entry-bar tests of a magnified + aggregated run (the slippage mask, the same-bar excursion test, the + `calc_on_order_fills` extreme sample) now compare script-bar indices with + script-bar indices — instrumented, 0 of 5 170 comparisons out of space, + against 4 996 with 3 974 wrong verdicts on `91d65ad6` — and K-IDX moved + two excursions onto the chart path's values and nothing else. Lane F1's + "12 vs 9 rows" is settled by sixteen `lab tv` tapes of its differential + script: TradingView books the script's 12 rows as this engine does on + every path, and `ab9714be`'s 9 were its own error + (`tests/test_magnified_aggregated_tape.cpp`, the variants the magnified + run books exactly; the ones it does not are recorded divergences of their + own: a pyramiding-cap bypass under the magnifier with + `process_orders_on_close` and `calc_on_order_fills`, and a stop entry + already triggered at its bar's close filling a bar late). - The per-bar broker hash of an aggregated run moves because the kernel now folds its script coordinate and named input coordinate. Those v19 witnesses are re-pinned once in `tests/test_native_point_checks.cpp`; no pre-release diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 8ed0e8b2..1d3649a5 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -2554,7 +2554,15 @@ magnifier and aggregated charts (the calling-bar deferrals), `lookahead_on` `request.security_lower_tf` paths, the auxiliary slice — keeps the adapter's own drive, unchanged. The corpus' 23 `request.security` probes are all of the first kind but the two lower-timeframe ones; their trades are byte-identical -on either drive, which is the parity evidence for the kernel pump. +on either drive, which is the parity evidence for the kernel pump. Every +condition of the predicate is pinned, with what it changes where it can be +tripped alone, by `tests/test_adapter_security_route_conditions.cpp`: on the +feeds measured the magnifier and an aggregated chart change no delivered +value (conservative exclusions, not calling-bar deferrals the kernel lacks), +while the range-start cut, the projection's incomplete tail, `lookahead_on` +and the OTC pins do; a registry whose `sec_id`s are not dense from 0 and a +pairing the kernel refuses at `declare_timeframe_subscriptions` keep the +evaluator too. **Streams.** `stream_begin` accepts a non-empty `subscriptions`, so a forward-execution host reads the same series a backtest of the same bars diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 655d2c66..f77ce540 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1338,6 +1338,12 @@ list(APPEND TEST_SOURCES test_short_seed_report_swap) list(APPEND TEST_SOURCES test_pine_dust_sweep_paired) # G2-36 session.ismarket: the generated predicate, the kernel's in_session. list(APPEND TEST_SOURCES test_session_ismarket_tape) +# F1(e) an aggregated process_orders_on_close script reading entry_bar_index. +list(APPEND TEST_SOURCES test_aggregated_entry_bar_index_tape) +# F1's magnified residual on the aggregated chart. +list(APPEND TEST_SOURCES test_magnified_aggregated_tape) +# G2-23 every condition that keeps request.security on the source evaluator. +list(APPEND TEST_SOURCES test_adapter_security_route_conditions) # R4-D L4b-fix restores every removed ShortSeed, affordability, sizing and # deferred-close oracle as an individually compiled native-route twin. Keep @@ -2077,6 +2083,8 @@ foreach(_pf_hm_row IN ITEMS "test_short_seed_report_swap|PINEFORGE_G221_FIXTURE_DIR|g221_short_seed" "test_pine_dust_sweep_paired|PINEFORGE_G232_FIXTURE_DIR|g232_decimal_dust" "test_session_ismarket_tape|PINEFORGE_G236_FIXTURE_DIR|session_ismarket" + "test_aggregated_entry_bar_index_tape|PINEFORGE_HM_F1E_FIXTURE_DIR|aggregated_entry_bar_index" + "test_magnified_aggregated_tape|PINEFORGE_HM_MAG_FIXTURE_DIR|magnified_aggregated" "test_adapter_margin_schedule_differential|PINEFORGE_HM_M7A_FIXTURE_DIR|margin_entry_bar") string(REPLACE "|" ";" _pf_hm_fields "${_pf_hm_row}") list(GET _pf_hm_fields 0 _pf_hm_target) diff --git a/tests/fixtures/aggregated_entry_bar_index/README.md b/tests/fixtures/aggregated_entry_bar_index/README.md new file mode 100644 index 00000000..c9001604 --- /dev/null +++ b/tests/fixtures/aggregated_entry_bar_index/README.md @@ -0,0 +1,29 @@ +# Aggregated `entry_bar_index` tape (R5 lane H-MEASURE, AUDIT4 X14 F1(e)) + +TradingView's answer to which bar indices a `process_orders_on_close` script +reads on an aggregated chart. `tests/test_aggregated_entry_bar_index_tape.cpp` +replays it through the Pine adapter on the 15m chart, on the corpus 1m bars +aggregated to 15m, and on the same aggregation under the bar magnifier. + +Each directory is one `lab tv` export (pineforge-workflow, channel +`ws-report-v1`, `rangeProof: covered`), byte-identical: `strategy.pine`, +`tv_trades.csv` (times UTC+8), `meta.json`, `metrics.json`. + +| probe | chart | window | trades | tvTradesSha256 | note | +|---|---|---|---|---|---| +| `hm-f1e-ebi-pooc-eth` | BINANCE:ETHUSDT.P 15 | 2025-06-11 .. 2025-06-13 | 4 | `e7004ca5e207f86d3535bd20157dbc16af0d221968bf3b18464ab80109506c5f` | `tv-tape-hm-f1e-ebi-pooc-eth-e7004ca5` | +| `hm-f1e-ebi-pooc-eth-mag` | same, `use_bar_magnifier=true` | same | 4 | `0c6badea92194fe4f9c2df36a7f9aadb496f6d953d1dcdb5884cda5ec307d839` | `tv-tape-hm-f1e-ebi-pooc-eth-mag-0c6badea` | + +The script enters at 01:00, 05:00 and 09:00 UTC on 2025-06-12 and closes each +position on `bar_index - strategy.opentrades.entry_bar_index(0)` (held 1, 3, +then the last closed trade's `exit_bar_index - entry_bar_index` + 2 = 5), and +enters a fourth when `bar_index - strategy.closedtrades.exit_bar_index(2) == 4` +(held 2). The two tapes differ only in trade 2's favorable excursion (0.40 +without the magnifier, 1.86 with it). + +`bars.inc` holds BINANCE:ETHUSDT.P 1m rows 2025-06-12 00:00 .. 12:59 UTC copied +from the corpus 1m feed (gitlink `eede4a2`, sha256 +`db8c1332da093008cfbd063e05db0b33fe8f7fd35d78cf058a366519eb9f6cc5`, no gap in +the span) and the same span resampled to 15m by `scripts/derive_corpus_feeds.py`'s +rule. The eight tape prices are the +15m closes of their bars. diff --git a/tests/fixtures/aggregated_entry_bar_index/bars.inc b/tests/fixtures/aggregated_entry_bar_index/bars.inc new file mode 100644 index 00000000..944e0f4d --- /dev/null +++ b/tests/fixtures/aggregated_entry_bar_index/bars.inc @@ -0,0 +1,842 @@ +// Generated by exec/H-MEASURE-scratch/agg/f1e/gen_inc.py (R5 lane H-MEASURE, group agg) +// from the corpus 1m feed the tape's chart matches. {timestamp_ms, open, high, low, close, volume} +// BINANCE:ETHUSDT.P 1m, 2025-06-12 00:00 .. 12:59 UTC, feed sha256 db8c1332da093008cfbd063e05db0b33fe8f7fd35d78cf058a366519eb9f6cc5 +constexpr FeedBar kEth1m[780] = { + {1749686400000LL, 2770.35, 2770.95, 2768.57, 2770.53, 7327.185}, + {1749686460000LL, 2770.53, 2774.81, 2770.25, 2773.63, 5183.228}, + {1749686520000LL, 2773.63, 2776.35, 2773.63, 2774.25, 3476.675}, + {1749686580000LL, 2774.25, 2775.65, 2773.42, 2773.7, 1704.832}, + {1749686640000LL, 2773.7, 2773.7, 2767.88, 2767.88, 5028.882}, + {1749686700000LL, 2767.88, 2769.52, 2767.15, 2767.3, 3042.317}, + {1749686760000LL, 2767.31, 2768.95, 2765.16, 2766.23, 9774.822}, + {1749686820000LL, 2766.15, 2766.65, 2763.71, 2766, 7124.192}, + {1749686880000LL, 2765.99, 2767.63, 2764.11, 2767.26, 5854.284}, + {1749686940000LL, 2767.24, 2770.79, 2767.24, 2770.79, 2815.654}, + {1749687000000LL, 2770.79, 2773.26, 2770.4, 2771.99, 1841.704}, + {1749687060000LL, 2771.98, 2772.2, 2768.51, 2768.52, 1855.739}, + {1749687120000LL, 2768.51, 2770.39, 2767.48, 2769.71, 1392.069}, + {1749687180000LL, 2769.7, 2772.28, 2768.24, 2772.09, 1327.677}, + {1749687240000LL, 2772.09, 2772.25, 2768.59, 2769.06, 946.442}, + {1749687300000LL, 2769.07, 2773.85, 2769.07, 2773.58, 1630.777}, + {1749687360000LL, 2773.57, 2774, 2770.91, 2771.87, 1594.269}, + {1749687420000LL, 2771.87, 2771.88, 2768.56, 2770, 828.501}, + {1749687480000LL, 2770, 2773.58, 2770, 2771.81, 1888.605}, + {1749687540000LL, 2771.79, 2773.13, 2769.81, 2771.55, 1408.739}, + {1749687600000LL, 2771.54, 2773.31, 2769.81, 2772.98, 1826.217}, + {1749687660000LL, 2772.97, 2781.51, 2772.76, 2779.54, 16180.379}, + {1749687720000LL, 2779.53, 2781, 2778.56, 2780.64, 3382.411}, + {1749687780000LL, 2780.64, 2783.9, 2779.22, 2781.7, 10371.774}, + {1749687840000LL, 2781.7, 2781.7, 2779, 2779, 2531.424}, + {1749687900000LL, 2779.01, 2780.26, 2777.65, 2779.18, 4361.06}, + {1749687960000LL, 2779.19, 2781.5, 2775, 2776.26, 3734.01}, + {1749688020000LL, 2776.26, 2776.27, 2768, 2768.47, 5689.955}, + {1749688080000LL, 2768.47, 2768.47, 2765.01, 2766.78, 5955.106}, + {1749688140000LL, 2766.77, 2767.65, 2762.98, 2763.23, 9855.157}, + {1749688200000LL, 2763.24, 2766.2, 2761.07, 2765.74, 6828.273}, + {1749688260000LL, 2765.73, 2767.49, 2764.63, 2765.01, 2333.651}, + {1749688320000LL, 2765.01, 2765.1, 2760.87, 2762.68, 4380.49}, + {1749688380000LL, 2762.68, 2764.34, 2762.04, 2763.6, 2975.573}, + {1749688440000LL, 2763.61, 2764.67, 2761.82, 2764.36, 2810.236}, + {1749688500000LL, 2764.36, 2765.99, 2760, 2760.81, 2760.923}, + {1749688560000LL, 2760.81, 2762.61, 2759.2, 2761.78, 5867.47}, + {1749688620000LL, 2761.78, 2764.56, 2757.78, 2763.29, 5062.021}, + {1749688680000LL, 2763.28, 2771.82, 2762.98, 2770.51, 6400.496}, + {1749688740000LL, 2770.5, 2772.3, 2770.05, 2772.3, 3665.135}, + {1749688800000LL, 2772.29, 2772.35, 2767, 2767.01, 2893.13}, + {1749688860000LL, 2767, 2773.15, 2766.34, 2772.81, 2517.983}, + {1749688920000LL, 2772.82, 2773.15, 2771.06, 2772.28, 1268.465}, + {1749688980000LL, 2772.29, 2776.97, 2771.83, 2775.93, 4357.021}, + {1749689040000LL, 2775.92, 2775.99, 2774.1, 2774.1, 1704.769}, + {1749689100000LL, 2774.1, 2775.99, 2772.56, 2774.59, 2255.13}, + {1749689160000LL, 2774.58, 2776.44, 2774.02, 2775.19, 2641.212}, + {1749689220000LL, 2775.18, 2776.05, 2773.9, 2774.48, 2200.323}, + {1749689280000LL, 2774.48, 2776.1, 2773.89, 2776.09, 953.916}, + {1749689340000LL, 2776.09, 2777, 2770.56, 2770.66, 4241.178}, + {1749689400000LL, 2770.68, 2773.53, 2769.21, 2773.53, 2356.982}, + {1749689460000LL, 2773.53, 2773.86, 2770.34, 2770.89, 916.145}, + {1749689520000LL, 2770.89, 2771.4, 2768.73, 2770.41, 1354.912}, + {1749689580000LL, 2770.41, 2772.39, 2770.4, 2771.46, 1334.006}, + {1749689640000LL, 2771.46, 2774.32, 2771.46, 2773.25, 3642.45}, + {1749689700000LL, 2773.24, 2773.25, 2768.69, 2772, 3289.659}, + {1749689760000LL, 2772, 2772.98, 2771.48, 2772.31, 1240.927}, + {1749689820000LL, 2772.3, 2773.23, 2771.18, 2773.21, 853.837}, + {1749689880000LL, 2773.2, 2774.67, 2773.2, 2774.07, 1255.66}, + {1749689940000LL, 2774.07, 2774.9, 2773.14, 2774.49, 1451.716}, + {1749690000000LL, 2774.49, 2777.58, 2773.61, 2775.47, 3184.729}, + {1749690060000LL, 2775.46, 2775.79, 2772.48, 2773.7, 4626.522}, + {1749690120000LL, 2773.71, 2774.39, 2770.73, 2770.73, 5071.804}, + {1749690180000LL, 2770.74, 2770.74, 2768.41, 2768.41, 2004.767}, + {1749690240000LL, 2768.42, 2768.7, 2764.14, 2764.44, 4966.754}, + {1749690300000LL, 2764.43, 2767.35, 2764.15, 2766.26, 3189.861}, + {1749690360000LL, 2766.26, 2768.47, 2766.22, 2768.47, 1110.697}, + {1749690420000LL, 2768.47, 2774.53, 2768.46, 2771.84, 4077.537}, + {1749690480000LL, 2771.84, 2772.96, 2771, 2771.01, 1660.359}, + {1749690540000LL, 2771.01, 2772.79, 2770.75, 2772.79, 1043.863}, + {1749690600000LL, 2772.78, 2772.91, 2771, 2772.58, 1454.702}, + {1749690660000LL, 2772.58, 2772.94, 2771.89, 2772.31, 526.105}, + {1749690720000LL, 2772.31, 2774.88, 2772.11, 2772.11, 1238.308}, + {1749690780000LL, 2772.09, 2772.31, 2767.01, 2767.11, 1540.297}, + {1749690840000LL, 2767.1, 2768.32, 2765.73, 2766.39, 2368.575}, + {1749690900000LL, 2766.39, 2768.88, 2766.22, 2766.85, 1064.682}, + {1749690960000LL, 2766.85, 2767.19, 2765.1, 2765.76, 1710.154}, + {1749691020000LL, 2765.77, 2768.13, 2765.76, 2767.99, 1066.856}, + {1749691080000LL, 2767.99, 2774.39, 2767.42, 2774.39, 2123.779}, + {1749691140000LL, 2774.39, 2774.4, 2771.19, 2771.68, 1378.282}, + {1749691200000LL, 2771.67, 2772.65, 2769.63, 2770.89, 872.356}, + {1749691260000LL, 2770.88, 2771.03, 2767.21, 2767.26, 1514.902}, + {1749691320000LL, 2767.25, 2767.52, 2764.79, 2766.07, 2379.216}, + {1749691380000LL, 2766.06, 2766.53, 2763.65, 2763.79, 3645.402}, + {1749691440000LL, 2763.79, 2765.8, 2763.74, 2765, 2907.999}, + {1749691500000LL, 2765, 2767.34, 2764.85, 2767, 1169.339}, + {1749691560000LL, 2766.99, 2767, 2764.78, 2764.81, 1292.256}, + {1749691620000LL, 2764.81, 2766.76, 2763.87, 2763.87, 987.984}, + {1749691680000LL, 2763.87, 2766.4, 2760.01, 2764.14, 5073.833}, + {1749691740000LL, 2764.14, 2766.19, 2763.12, 2764.57, 1443.969}, + {1749691800000LL, 2764.57, 2766.26, 2761.21, 2761.51, 2887.022}, + {1749691860000LL, 2761.51, 2762.81, 2758.38, 2759.57, 10071.114}, + {1749691920000LL, 2759.57, 2760.57, 2757.84, 2758.52, 6368.917}, + {1749691980000LL, 2758.51, 2758.52, 2752.84, 2754.23, 10944.422}, + {1749692040000LL, 2754.24, 2756.78, 2754.14, 2756.78, 4357.249}, + {1749692100000LL, 2756.78, 2758.19, 2754.7, 2755.51, 2704.997}, + {1749692160000LL, 2755.51, 2756.94, 2753, 2753.99, 2527.953}, + {1749692220000LL, 2753.99, 2754.09, 2750.8, 2751.86, 5738.065}, + {1749692280000LL, 2751.85, 2755.5, 2751.68, 2753.94, 3075.247}, + {1749692340000LL, 2753.93, 2761.78, 2753.16, 2761.78, 3652.125}, + {1749692400000LL, 2761.78, 2764, 2760.53, 2760.79, 6444.491}, + {1749692460000LL, 2760.78, 2762.83, 2759.23, 2762.38, 2406.76}, + {1749692520000LL, 2762.39, 2765.7, 2761.99, 2765.63, 4973.04}, + {1749692580000LL, 2765.64, 2767.6, 2764.38, 2766.62, 3782.028}, + {1749692640000LL, 2766.62, 2767.49, 2765.46, 2766.18, 2335.379}, + {1749692700000LL, 2766.18, 2766.31, 2762.9, 2763.81, 1821.852}, + {1749692760000LL, 2763.81, 2764.47, 2762.05, 2764.22, 1932.557}, + {1749692820000LL, 2764.23, 2764.23, 2760.29, 2763.3, 2157.483}, + {1749692880000LL, 2763.3, 2763.94, 2759.26, 2760.33, 2887.013}, + {1749692940000LL, 2760.33, 2762, 2760, 2761.99, 2010.464}, + {1749693000000LL, 2762, 2766, 2761.99, 2765.86, 1691.971}, + {1749693060000LL, 2765.87, 2766.62, 2764.44, 2764.73, 1831.58}, + {1749693120000LL, 2764.74, 2767.2, 2764.74, 2766.05, 1376.711}, + {1749693180000LL, 2766.06, 2772.09, 2765.82, 2772.09, 5669}, + {1749693240000LL, 2772.08, 2773.5, 2770.28, 2771.58, 5834.892}, + {1749693300000LL, 2771.58, 2773.53, 2771.58, 2772.41, 2091.453}, + {1749693360000LL, 2772.42, 2774.56, 2772.41, 2773.93, 2180.67}, + {1749693420000LL, 2773.93, 2773.93, 2771.07, 2771.81, 2249.044}, + {1749693480000LL, 2771.8, 2773.87, 2771.3, 2773.87, 2733.893}, + {1749693540000LL, 2773.87, 2775.53, 2773.87, 2774.87, 3516.457}, + {1749693600000LL, 2774.87, 2775.02, 2772.44, 2773.75, 2656.596}, + {1749693660000LL, 2773.75, 2777.74, 2773.26, 2777.5, 3515.081}, + {1749693720000LL, 2777.49, 2780.64, 2776, 2778.82, 6607.282}, + {1749693780000LL, 2778.82, 2780.88, 2778.32, 2779.08, 5411.941}, + {1749693840000LL, 2779.07, 2782.05, 2778.16, 2781.24, 5788.614}, + {1749693900000LL, 2781.23, 2783, 2780.26, 2781.5, 5230.677}, + {1749693960000LL, 2781.49, 2783.48, 2781.49, 2782.52, 7378.041}, + {1749694020000LL, 2782.52, 2782.88, 2779.42, 2779.42, 3472.356}, + {1749694080000LL, 2779.42, 2779.58, 2776.68, 2777.98, 4054.451}, + {1749694140000LL, 2777.98, 2778.93, 2776.52, 2777.64, 2942.387}, + {1749694200000LL, 2777.63, 2777.63, 2774.63, 2774.63, 1837.784}, + {1749694260000LL, 2774.64, 2777.19, 2774.16, 2776.68, 2648.573}, + {1749694320000LL, 2776.68, 2777.49, 2775.69, 2777.06, 1490.758}, + {1749694380000LL, 2777.06, 2778.67, 2776.94, 2777.24, 1092.275}, + {1749694440000LL, 2777.24, 2777.25, 2772.41, 2773.35, 4248.158}, + {1749694500000LL, 2773.35, 2774.85, 2771.31, 2771.41, 2686.803}, + {1749694560000LL, 2771.4, 2772.83, 2769.56, 2769.69, 3445.636}, + {1749694620000LL, 2769.7, 2771.05, 2769.44, 2769.98, 2847.196}, + {1749694680000LL, 2769.97, 2771.24, 2769.9, 2770.11, 1340.872}, + {1749694740000LL, 2770.11, 2770.83, 2769.21, 2770.38, 1509.889}, + {1749694800000LL, 2770.38, 2772.16, 2769.5, 2770.32, 1857.694}, + {1749694860000LL, 2770.32, 2772.66, 2770.07, 2771.51, 1742.776}, + {1749694920000LL, 2771.5, 2772.46, 2769.08, 2769.09, 981.125}, + {1749694980000LL, 2769.09, 2769.89, 2766.13, 2769.51, 4177.052}, + {1749695040000LL, 2769.5, 2770.83, 2768.58, 2769.62, 2187.761}, + {1749695100000LL, 2769.63, 2770.73, 2767.49, 2770.72, 1745.162}, + {1749695160000LL, 2770.71, 2770.72, 2769.03, 2769.9, 630.59}, + {1749695220000LL, 2769.9, 2772.78, 2769.89, 2772.3, 1231.692}, + {1749695280000LL, 2772.3, 2773.85, 2771.2, 2771.62, 1868.28}, + {1749695340000LL, 2771.61, 2772.02, 2770.73, 2772, 625.84}, + {1749695400000LL, 2772, 2772.99, 2770.53, 2771.81, 919.942}, + {1749695460000LL, 2771.82, 2773.01, 2770.53, 2771.21, 738.078}, + {1749695520000LL, 2771.21, 2771.4, 2767.11, 2767.12, 1438.966}, + {1749695580000LL, 2767.11, 2768.6, 2766.59, 2767.34, 1512.615}, + {1749695640000LL, 2767.35, 2769.54, 2766.57, 2769.51, 1049.071}, + {1749695700000LL, 2769.52, 2772.5, 2768.64, 2768.64, 1368.452}, + {1749695760000LL, 2768.64, 2769.61, 2767.76, 2769.41, 1462.056}, + {1749695820000LL, 2769.42, 2771.54, 2768.5, 2771.41, 643.76}, + {1749695880000LL, 2771.41, 2773, 2770.88, 2771.47, 930.73}, + {1749695940000LL, 2771.48, 2773.69, 2771.47, 2772.96, 2070.408}, + {1749696000000LL, 2772.96, 2773.49, 2771.47, 2771.47, 554.836}, + {1749696060000LL, 2771.47, 2771.48, 2769.75, 2771.15, 666.929}, + {1749696120000LL, 2771.16, 2772.17, 2769.82, 2770.03, 688.311}, + {1749696180000LL, 2770.06, 2770.17, 2767.67, 2767.87, 1639.074}, + {1749696240000LL, 2767.86, 2768.89, 2766.55, 2766.56, 823.096}, + {1749696300000LL, 2766.55, 2766.56, 2763.12, 2765.41, 5071.973}, + {1749696360000LL, 2765.41, 2770.47, 2765.41, 2770, 1835.223}, + {1749696420000LL, 2770.01, 2770.88, 2767.36, 2767.36, 790.925}, + {1749696480000LL, 2767.36, 2767.62, 2765.57, 2766.37, 2014.648}, + {1749696540000LL, 2766.37, 2768.99, 2766.18, 2768.98, 1162.932}, + {1749696600000LL, 2768.99, 2769.65, 2768, 2768.01, 1686.708}, + {1749696660000LL, 2768.01, 2768.93, 2766.11, 2768.93, 1213.057}, + {1749696720000LL, 2768.92, 2769.76, 2766.91, 2767.17, 2859.443}, + {1749696780000LL, 2767.17, 2767.93, 2765.6, 2766.26, 1151.624}, + {1749696840000LL, 2766.27, 2768.18, 2765.62, 2766.11, 954.181}, + {1749696900000LL, 2766.11, 2767.2, 2765.61, 2766, 756.057}, + {1749696960000LL, 2766.01, 2766.43, 2761.08, 2762.09, 16846.2}, + {1749697020000LL, 2762.09, 2764.5, 2758.87, 2758.88, 11698.438}, + {1749697080000LL, 2758.87, 2759.42, 2756.99, 2758.44, 5884.004}, + {1749697140000LL, 2758.44, 2758.66, 2755.55, 2756.87, 8819.94}, + {1749697200000LL, 2756.87, 2759.11, 2756.42, 2758.39, 5515.796}, + {1749697260000LL, 2758.39, 2759.62, 2756.24, 2757.48, 3646.135}, + {1749697320000LL, 2757.48, 2759.91, 2757.48, 2758.9, 2600.816}, + {1749697380000LL, 2758.9, 2759.42, 2755, 2756.02, 4063.303}, + {1749697440000LL, 2756.01, 2756.02, 2752.82, 2753.16, 6210.987}, + {1749697500000LL, 2753.19, 2755.45, 2753.18, 2754.01, 3109.211}, + {1749697560000LL, 2754, 2754.26, 2753, 2754.03, 2358.561}, + {1749697620000LL, 2754.03, 2757.5, 2753.87, 2756.89, 2775.124}, + {1749697680000LL, 2756.89, 2757.58, 2754.39, 2755, 3381.787}, + {1749697740000LL, 2754.99, 2760.38, 2754.99, 2759.42, 3911.366}, + {1749697800000LL, 2759.41, 2760.31, 2757.4, 2760, 3845.223}, + {1749697860000LL, 2759.99, 2760.98, 2759.63, 2760.13, 3616.822}, + {1749697920000LL, 2760.13, 2760.37, 2758.56, 2760.06, 1405.247}, + {1749697980000LL, 2760.06, 2760.93, 2758.79, 2759.89, 1109.041}, + {1749698040000LL, 2759.9, 2760.7, 2758.31, 2760.39, 2352.275}, + {1749698100000LL, 2760.4, 2761, 2759.35, 2759.77, 1540.67}, + {1749698160000LL, 2759.77, 2759.98, 2757.67, 2758.99, 1972.78}, + {1749698220000LL, 2759, 2759.28, 2757, 2757.47, 913.571}, + {1749698280000LL, 2757.47, 2758.78, 2756.93, 2757, 1469.329}, + {1749698340000LL, 2757.01, 2757.78, 2755.64, 2757.31, 1568.545}, + {1749698400000LL, 2757.31, 2757.32, 2745.31, 2747.57, 26906.974}, + {1749698460000LL, 2747.58, 2749.4, 2746.27, 2746.84, 10353.893}, + {1749698520000LL, 2746.83, 2751.47, 2745.01, 2749.82, 10198.412}, + {1749698580000LL, 2749.82, 2754.98, 2749.16, 2753.01, 5074.555}, + {1749698640000LL, 2753.01, 2753.01, 2749.84, 2750.87, 4890.72}, + {1749698700000LL, 2750.87, 2752.93, 2748.02, 2751.42, 4113.956}, + {1749698760000LL, 2751.43, 2751.43, 2745.54, 2747.47, 7745.266}, + {1749698820000LL, 2747.46, 2749.89, 2746, 2746.1, 5565.26}, + {1749698880000LL, 2746.11, 2747.88, 2745.36, 2747.17, 4799.286}, + {1749698940000LL, 2747.16, 2751, 2747.16, 2751, 4001.703}, + {1749699000000LL, 2750.99, 2753, 2750.45, 2751.41, 5325.884}, + {1749699060000LL, 2751.4, 2754.97, 2751.19, 2754.77, 3704.193}, + {1749699120000LL, 2754.78, 2757, 2753.81, 2756.71, 3451.276}, + {1749699180000LL, 2756.71, 2757.15, 2755.71, 2755.71, 4396.637}, + {1749699240000LL, 2755.71, 2755.71, 2753.64, 2753.65, 2107.854}, + {1749699300000LL, 2753.65, 2758, 2753.65, 2758, 3051.871}, + {1749699360000LL, 2758, 2758.16, 2755.8, 2757.41, 3013.829}, + {1749699420000LL, 2757.41, 2760.62, 2757.08, 2760.13, 4737.229}, + {1749699480000LL, 2760.14, 2762.64, 2759.84, 2762.39, 9366.398}, + {1749699540000LL, 2762.39, 2764.91, 2761.03, 2762.55, 3830.606}, + {1749699600000LL, 2762.55, 2763.88, 2761.92, 2762.07, 2433.9}, + {1749699660000LL, 2762.06, 2763.94, 2761.31, 2763.94, 3105.389}, + {1749699720000LL, 2763.93, 2765.79, 2762.62, 2764.57, 5466.737}, + {1749699780000LL, 2764.57, 2768.06, 2763.42, 2767.11, 14402.012}, + {1749699840000LL, 2767.11, 2770.27, 2765.85, 2770.27, 13805.858}, + {1749699900000LL, 2770.27, 2770.46, 2765.67, 2765.83, 7388.856}, + {1749699960000LL, 2765.83, 2776.33, 2765.83, 2774.83, 12739.116}, + {1749700020000LL, 2774.82, 2774.83, 2772.61, 2772.73, 9061.449}, + {1749700080000LL, 2772.72, 2773.49, 2771.48, 2772.83, 5500.746}, + {1749700140000LL, 2772.83, 2773.83, 2771.81, 2772.07, 4540.511}, + {1749700200000LL, 2772.06, 2772.07, 2769.47, 2770.25, 5938.31}, + {1749700260000LL, 2770.26, 2772.68, 2770.25, 2771.39, 3237.569}, + {1749700320000LL, 2771.39, 2771.9, 2767.5, 2767.86, 17133.14}, + {1749700380000LL, 2767.86, 2769.4, 2767.31, 2769.09, 4743.684}, + {1749700440000LL, 2769.09, 2771.96, 2769, 2770.92, 3967.2}, + {1749700500000LL, 2770.92, 2770.93, 2768.74, 2768.74, 1700.475}, + {1749700560000LL, 2768.75, 2770.73, 2768.06, 2768.06, 2890.444}, + {1749700620000LL, 2768.06, 2768.07, 2765.56, 2767.45, 2806.75}, + {1749700680000LL, 2767.44, 2767.45, 2764.95, 2766.3, 2017.989}, + {1749700740000LL, 2766.29, 2766.3, 2763.74, 2763.99, 2665.459}, + {1749700800000LL, 2764, 2764.78, 2763.05, 2764.29, 2695.609}, + {1749700860000LL, 2764.29, 2765.85, 2764.29, 2765.62, 3568.812}, + {1749700920000LL, 2765.62, 2768.42, 2765, 2765.01, 4204.158}, + {1749700980000LL, 2765.01, 2765.57, 2762.04, 2762.17, 1986.8}, + {1749701040000LL, 2762.17, 2764.77, 2761.46, 2764.1, 1991.857}, + {1749701100000LL, 2764.09, 2764.09, 2762, 2762.72, 1435.867}, + {1749701160000LL, 2762.73, 2763.55, 2762.1, 2762.81, 1468.293}, + {1749701220000LL, 2762.82, 2763.49, 2760.21, 2761.51, 2247.827}, + {1749701280000LL, 2761.5, 2763.62, 2761.23, 2763.62, 763.58}, + {1749701340000LL, 2763.62, 2764.15, 2761.73, 2762.1, 873.783}, + {1749701400000LL, 2762.11, 2762.53, 2761, 2761.6, 1262.38}, + {1749701460000LL, 2761.6, 2761.61, 2757, 2758.61, 5653}, + {1749701520000LL, 2758.61, 2759.41, 2757.43, 2758.26, 2427.952}, + {1749701580000LL, 2758.27, 2758.49, 2758.03, 2758.03, 1169.743}, + {1749701640000LL, 2758.03, 2758.03, 2755.64, 2756.22, 3176.436}, + {1749701700000LL, 2756.22, 2757.4, 2755.9, 2757.4, 1561.788}, + {1749701760000LL, 2757.4, 2760.96, 2756.86, 2759.9, 5524.16}, + {1749701820000LL, 2759.91, 2762.79, 2759.54, 2760.37, 4367.379}, + {1749701880000LL, 2760.38, 2760.67, 2756.78, 2757.57, 3139.283}, + {1749701940000LL, 2757.57, 2757.85, 2754.11, 2754.51, 4344.771}, + {1749702000000LL, 2754.5, 2754.94, 2752.23, 2753.79, 6706.802}, + {1749702060000LL, 2753.79, 2755, 2750.93, 2751.07, 5485.066}, + {1749702120000LL, 2751.07, 2752, 2750.36, 2751.14, 4743.639}, + {1749702180000LL, 2751.13, 2753.9, 2751.13, 2752.29, 2186.423}, + {1749702240000LL, 2752.29, 2756.19, 2750.1, 2754.89, 5830.961}, + {1749702300000LL, 2754.88, 2757.49, 2753.75, 2757.36, 4075.265}, + {1749702360000LL, 2757.36, 2759.53, 2757.29, 2758.1, 3366.782}, + {1749702420000LL, 2758.1, 2760.36, 2757.98, 2758.54, 2776.371}, + {1749702480000LL, 2758.53, 2759.86, 2758.1, 2759.86, 2084.186}, + {1749702540000LL, 2759.86, 2760.24, 2757.43, 2757.55, 1357.824}, + {1749702600000LL, 2757.55, 2761, 2757.5, 2760.73, 2051.246}, + {1749702660000LL, 2760.73, 2761.16, 2758.2, 2758.2, 2327.481}, + {1749702720000LL, 2758.2, 2760, 2758.07, 2758.72, 963.448}, + {1749702780000LL, 2758.72, 2759.39, 2754.06, 2754.06, 1767.411}, + {1749702840000LL, 2754.06, 2754.96, 2753.38, 2754.54, 1905.324}, + {1749702900000LL, 2754.54, 2755.84, 2753.38, 2754.22, 3004.554}, + {1749702960000LL, 2754.23, 2754.65, 2751.26, 2751.26, 2641.78}, + {1749703020000LL, 2751.26, 2752.54, 2748.58, 2749.39, 8576.527}, + {1749703080000LL, 2749.39, 2751.63, 2749.24, 2751.1, 4162.759}, + {1749703140000LL, 2751.1, 2753.59, 2750.58, 2753.58, 2049.065}, + {1749703200000LL, 2753.58, 2754.71, 2750.51, 2751.17, 2534.274}, + {1749703260000LL, 2751.16, 2751.17, 2749.47, 2750.94, 2959.36}, + {1749703320000LL, 2750.94, 2751.99, 2749.71, 2751.7, 2125.205}, + {1749703380000LL, 2751.69, 2753.5, 2751.4, 2752.55, 1433.05}, + {1749703440000LL, 2752.55, 2753.78, 2752.12, 2752.13, 1050.228}, + {1749703500000LL, 2752.12, 2753.78, 2751.24, 2753.78, 2086.197}, + {1749703560000LL, 2753.78, 2758, 2753.78, 2756.53, 3729.159}, + {1749703620000LL, 2756.52, 2757.9, 2756.13, 2757.86, 2607.599}, + {1749703680000LL, 2757.85, 2758.9, 2757.4, 2757.92, 1529.712}, + {1749703740000LL, 2757.93, 2760.29, 2757.92, 2760.29, 2911.12}, + {1749703800000LL, 2760.28, 2760.78, 2759.35, 2759.74, 2039.039}, + {1749703860000LL, 2759.75, 2760.21, 2757.64, 2757.65, 1089.448}, + {1749703920000LL, 2757.64, 2758.4, 2755.77, 2758.4, 1609.883}, + {1749703980000LL, 2758.4, 2761.42, 2758.39, 2760.29, 1702.71}, + {1749704040000LL, 2760.29, 2761, 2759.72, 2759.91, 1785.071}, + {1749704100000LL, 2759.9, 2760.46, 2758.68, 2759.04, 1073.412}, + {1749704160000LL, 2759.05, 2760, 2759.04, 2759.74, 507}, + {1749704220000LL, 2759.74, 2760, 2759.05, 2760, 497.461}, + {1749704280000LL, 2760, 2761.39, 2759.58, 2761.21, 1368.62}, + {1749704340000LL, 2761.21, 2762.38, 2761.21, 2761.53, 1103.036}, + {1749704400000LL, 2761.52, 2761.53, 2759.25, 2759.97, 2163.146}, + {1749704460000LL, 2759.98, 2761.29, 2759.3, 2759.83, 2756.362}, + {1749704520000LL, 2759.84, 2759.99, 2758.39, 2758.83, 762.49}, + {1749704580000LL, 2758.83, 2758.83, 2755, 2755, 1246.197}, + {1749704640000LL, 2755.01, 2755.66, 2752.8, 2752.8, 3082.14}, + {1749704700000LL, 2752.8, 2757.05, 2752.49, 2757.05, 3055.018}, + {1749704760000LL, 2757.05, 2758.14, 2755.44, 2756.48, 1292.308}, + {1749704820000LL, 2756.48, 2756.62, 2752.91, 2753.5, 1995.66}, + {1749704880000LL, 2753.5, 2753.99, 2752.5, 2752.5, 2040.169}, + {1749704940000LL, 2752.5, 2752.89, 2750.55, 2752.44, 5111.85}, + {1749705000000LL, 2752.44, 2754.14, 2751.7, 2753.13, 1834.643}, + {1749705060000LL, 2753.13, 2754.5, 2753.12, 2754.01, 804.475}, + {1749705120000LL, 2754.01, 2756.29, 2754.01, 2755.01, 1170.297}, + {1749705180000LL, 2755.01, 2755.67, 2754.77, 2755.22, 1247.483}, + {1749705240000LL, 2755.23, 2757.09, 2755.23, 2757.09, 1167.212}, + {1749705300000LL, 2757.1, 2757.56, 2756.69, 2757.16, 1400.748}, + {1749705360000LL, 2757.16, 2758.46, 2757, 2758.19, 1480.755}, + {1749705420000LL, 2758.19, 2761, 2758.19, 2761, 2560.515}, + {1749705480000LL, 2760.99, 2761, 2758.77, 2759.68, 1463.272}, + {1749705540000LL, 2759.68, 2760.99, 2758.39, 2759.39, 1213.681}, + {1749705600000LL, 2759.4, 2760.32, 2759.16, 2759.33, 1560.136}, + {1749705660000LL, 2759.32, 2761.71, 2759.32, 2761.71, 1531.053}, + {1749705720000LL, 2761.71, 2764.96, 2761.7, 2764.96, 4767.047}, + {1749705780000LL, 2764.96, 2766.98, 2764.38, 2766.15, 6459.118}, + {1749705840000LL, 2766.14, 2767.2, 2765.37, 2765.93, 4032.791}, + {1749705900000LL, 2765.93, 2766.71, 2765.72, 2766.16, 2393.901}, + {1749705960000LL, 2766.17, 2767, 2765.86, 2767, 2360.41}, + {1749706020000LL, 2766.99, 2767, 2766.08, 2766.67, 1784.636}, + {1749706080000LL, 2766.68, 2767, 2766.08, 2766.34, 1913.006}, + {1749706140000LL, 2766.34, 2766.34, 2764.44, 2765.44, 2313.268}, + {1749706200000LL, 2765.43, 2766, 2764.8, 2764.82, 1774.802}, + {1749706260000LL, 2764.81, 2766.73, 2763.91, 2766.11, 4062.114}, + {1749706320000LL, 2766.12, 2766.93, 2764.86, 2766.22, 1129.415}, + {1749706380000LL, 2766.21, 2767.8, 2764.38, 2765.7, 2882.908}, + {1749706440000LL, 2765.69, 2765.69, 2762.34, 2763.82, 1930.902}, + {1749706500000LL, 2763.82, 2766, 2763.44, 2765.99, 1167.793}, + {1749706560000LL, 2766, 2769.77, 2766, 2767.82, 4594.622}, + {1749706620000LL, 2767.82, 2768.14, 2766.78, 2767.75, 1630.978}, + {1749706680000LL, 2767.74, 2767.75, 2766.5, 2766.51, 1580.06}, + {1749706740000LL, 2766.51, 2766.52, 2764, 2764.01, 2007.86}, + {1749706800000LL, 2764.01, 2764.16, 2760.25, 2761.4, 3724.433}, + {1749706860000LL, 2761.41, 2761.49, 2760, 2760.56, 2398.731}, + {1749706920000LL, 2760.56, 2761.21, 2757.92, 2757.97, 3997.75}, + {1749706980000LL, 2757.98, 2758.79, 2757.22, 2757.31, 4888.464}, + {1749707040000LL, 2757.31, 2758.78, 2757.31, 2758.36, 1862.079}, + {1749707100000LL, 2758.36, 2759.54, 2758.36, 2758.69, 2430.446}, + {1749707160000LL, 2758.68, 2759.35, 2758.16, 2759.35, 1099.189}, + {1749707220000LL, 2759.35, 2759.59, 2758.72, 2758.73, 860.818}, + {1749707280000LL, 2758.73, 2759.73, 2758.16, 2759.44, 972.525}, + {1749707340000LL, 2759.44, 2761.78, 2759.44, 2761.64, 2751.355}, + {1749707400000LL, 2761.64, 2762.72, 2760.52, 2760.52, 1511.781}, + {1749707460000LL, 2760.52, 2761.52, 2759.42, 2761.52, 731.416}, + {1749707520000LL, 2761.52, 2762.91, 2761.51, 2761.84, 1432.612}, + {1749707580000LL, 2761.83, 2763.86, 2761.07, 2763.86, 1181.838}, + {1749707640000LL, 2763.85, 2764.5, 2763.57, 2763.69, 1354.849}, + {1749707700000LL, 2763.69, 2763.7, 2762.59, 2763.43, 665.302}, + {1749707760000LL, 2763.44, 2768.23, 2763.44, 2765.79, 7231.895}, + {1749707820000LL, 2765.79, 2765.92, 2764.44, 2764.67, 1345.326}, + {1749707880000LL, 2764.67, 2765.13, 2763.62, 2763.62, 872.451}, + {1749707940000LL, 2763.63, 2764.32, 2763.63, 2764, 962.826}, + {1749708000000LL, 2764, 2764.36, 2762.24, 2764.36, 1487.555}, + {1749708060000LL, 2764.35, 2764.77, 2762.84, 2763.12, 1389.231}, + {1749708120000LL, 2763.13, 2765.32, 2763.12, 2764.6, 901.592}, + {1749708180000LL, 2764.6, 2764.99, 2762.59, 2762.59, 862.019}, + {1749708240000LL, 2762.59, 2762.59, 2759.21, 2759.45, 2484.313}, + {1749708300000LL, 2759.45, 2760.6, 2759.21, 2759.21, 918.34}, + {1749708360000LL, 2759.22, 2759.22, 2758.19, 2758.76, 1077.03}, + {1749708420000LL, 2758.76, 2762.94, 2758.75, 2762.53, 1575.413}, + {1749708480000LL, 2762.53, 2763.03, 2761.16, 2761.51, 1313.057}, + {1749708540000LL, 2761.51, 2761.51, 2759.45, 2759.83, 722.884}, + {1749708600000LL, 2759.84, 2760.19, 2758.6, 2758.85, 1047.401}, + {1749708660000LL, 2758.86, 2759.45, 2758.28, 2759.26, 670.61}, + {1749708720000LL, 2759.26, 2759.39, 2758.88, 2759.26, 469.301}, + {1749708780000LL, 2759.25, 2759.98, 2758.6, 2759.98, 796.267}, + {1749708840000LL, 2759.97, 2760.36, 2759.04, 2759.46, 894.683}, + {1749708900000LL, 2759.47, 2760.14, 2758.58, 2758.65, 2790.893}, + {1749708960000LL, 2758.64, 2760.87, 2758.26, 2758.7, 1427.527}, + {1749709020000LL, 2758.7, 2758.71, 2753.99, 2754.09, 6145.13}, + {1749709080000LL, 2754.09, 2758.07, 2753.92, 2756.17, 3564.866}, + {1749709140000LL, 2756.16, 2758.33, 2755.58, 2757.39, 1070.204}, + {1749709200000LL, 2757.39, 2759.07, 2756.68, 2756.69, 1747.399}, + {1749709260000LL, 2756.69, 2756.96, 2754.76, 2754.76, 1734.899}, + {1749709320000LL, 2754.76, 2758.51, 2754.76, 2758.51, 1763.782}, + {1749709380000LL, 2758.52, 2764.21, 2758.51, 2763.43, 5005.558}, + {1749709440000LL, 2763.42, 2764.08, 2763.06, 2763.2, 2205.629}, + {1749709500000LL, 2763.2, 2763.82, 2759.45, 2759.45, 1682.953}, + {1749709560000LL, 2759.42, 2760.41, 2758.82, 2760.18, 1260.422}, + {1749709620000LL, 2760.19, 2764.17, 2760.18, 2764.17, 1400.693}, + {1749709680000LL, 2764.16, 2764.17, 2763.05, 2763.06, 992.308}, + {1749709740000LL, 2763.06, 2763.07, 2762, 2762.49, 571.029}, + {1749709800000LL, 2762.5, 2763.63, 2761.83, 2763.62, 944.977}, + {1749709860000LL, 2763.63, 2765, 2762.17, 2762.9, 1956.234}, + {1749709920000LL, 2762.9, 2766.27, 2762.89, 2765.07, 2777.657}, + {1749709980000LL, 2765.06, 2765.08, 2763.06, 2764.56, 1477.459}, + {1749710040000LL, 2764.56, 2765.78, 2764.35, 2765.54, 1098.068}, + {1749710100000LL, 2765.54, 2765.55, 2764.14, 2764.15, 1060.89}, + {1749710160000LL, 2764.14, 2765.33, 2764.14, 2764.56, 1262.043}, + {1749710220000LL, 2764.57, 2766.93, 2764.25, 2766.79, 2584.153}, + {1749710280000LL, 2766.79, 2772, 2766.79, 2770.39, 18166.877}, + {1749710340000LL, 2770.38, 2773, 2770.38, 2772.2, 5608.184}, + {1749710400000LL, 2772.21, 2774, 2770.92, 2773.99, 9767.217}, + {1749710460000LL, 2774, 2775.31, 2771.73, 2773.63, 9341.027}, + {1749710520000LL, 2773.62, 2774.24, 2771.96, 2771.96, 4151.846}, + {1749710580000LL, 2771.97, 2773.87, 2771.96, 2773.79, 2751.33}, + {1749710640000LL, 2773.79, 2775.75, 2772.29, 2772.59, 4103.869}, + {1749710700000LL, 2772.58, 2772.59, 2770.56, 2771.39, 2923.091}, + {1749710760000LL, 2771.4, 2771.5, 2769.38, 2769.78, 2897.059}, + {1749710820000LL, 2769.78, 2770.74, 2767.76, 2768.34, 3759.701}, + {1749710880000LL, 2768.34, 2768.92, 2766.57, 2767.92, 7488.173}, + {1749710940000LL, 2767.92, 2768.36, 2767, 2768.36, 1394.885}, + {1749711000000LL, 2768.36, 2768.43, 2767.22, 2767.22, 1204.427}, + {1749711060000LL, 2767.22, 2768.08, 2767.22, 2767.6, 772.309}, + {1749711120000LL, 2767.59, 2770, 2767.21, 2770, 1385.521}, + {1749711180000LL, 2770, 2770.81, 2769, 2770.55, 1999.444}, + {1749711240000LL, 2770.55, 2770.55, 2767.6, 2767.71, 1430.067}, + {1749711300000LL, 2767.7, 2769.17, 2767.7, 2769.07, 638.331}, + {1749711360000LL, 2769.07, 2769.82, 2767.86, 2768.24, 1387.793}, + {1749711420000LL, 2768.25, 2768.92, 2767.72, 2768.67, 906.465}, + {1749711480000LL, 2768.68, 2769.15, 2767.04, 2767.51, 1176.02}, + {1749711540000LL, 2767.51, 2768.09, 2766.92, 2767.16, 1476.016}, + {1749711600000LL, 2767.16, 2767.86, 2766.08, 2766.3, 1333.05}, + {1749711660000LL, 2766.31, 2769.85, 2766.31, 2768.18, 1943.227}, + {1749711720000LL, 2768.17, 2768.18, 2766.32, 2767.06, 1083.609}, + {1749711780000LL, 2767.07, 2769.18, 2767.06, 2769, 1596.625}, + {1749711840000LL, 2769.01, 2769.73, 2768.3, 2768.57, 1545.757}, + {1749711900000LL, 2768.58, 2768.82, 2766.09, 2766.1, 1089.353}, + {1749711960000LL, 2766.1, 2766.1, 2763.8, 2764.76, 4225.273}, + {1749712020000LL, 2764.75, 2765.22, 2764.08, 2765.22, 1179.188}, + {1749712080000LL, 2765.22, 2768.39, 2765.22, 2768.38, 3540.726}, + {1749712140000LL, 2768.38, 2772.99, 2768.38, 2770.89, 2899.985}, + {1749712200000LL, 2770.9, 2771.86, 2770, 2770.01, 1229.191}, + {1749712260000LL, 2770.01, 2771.82, 2770, 2770.38, 1874.026}, + {1749712320000LL, 2770.38, 2770.98, 2769.3, 2770.56, 1980.526}, + {1749712380000LL, 2770.56, 2770.57, 2769, 2769, 903.765}, + {1749712440000LL, 2768.99, 2768.99, 2767.26, 2767.26, 1225.814}, + {1749712500000LL, 2767.27, 2767.27, 2764.82, 2764.82, 1983.496}, + {1749712560000LL, 2764.83, 2764.83, 2763.51, 2763.52, 1639.334}, + {1749712620000LL, 2763.51, 2765.29, 2763.51, 2765.28, 1747.86}, + {1749712680000LL, 2765.29, 2768.91, 2765.29, 2768.91, 1300.842}, + {1749712740000LL, 2768.91, 2769.58, 2766.46, 2766.61, 1711.387}, + {1749712800000LL, 2766.62, 2768.74, 2765.22, 2765.23, 1426.141}, + {1749712860000LL, 2765.22, 2767.3, 2765.22, 2765.31, 952.508}, + {1749712920000LL, 2765.32, 2766.53, 2763.11, 2765.25, 4145.128}, + {1749712980000LL, 2765.27, 2766.67, 2763.22, 2765.92, 4289.542}, + {1749713040000LL, 2765.92, 2765.93, 2762.12, 2762.37, 2910.584}, + {1749713100000LL, 2762.38, 2763.75, 2759.09, 2760.29, 5667.97}, + {1749713160000LL, 2760.28, 2760.29, 2758.09, 2759.4, 4551.059}, + {1749713220000LL, 2759.39, 2759.39, 2757.64, 2757.75, 2471.972}, + {1749713280000LL, 2757.75, 2758.43, 2757.3, 2757.59, 2342.612}, + {1749713340000LL, 2757.59, 2757.59, 2756, 2757.12, 6026.105}, + {1749713400000LL, 2757.13, 2758.57, 2756.71, 2758.05, 4607.439}, + {1749713460000LL, 2758.04, 2758.99, 2755.65, 2756.8, 3828.784}, + {1749713520000LL, 2756.8, 2758.85, 2756.15, 2756.16, 4463.66}, + {1749713580000LL, 2756.16, 2757, 2754.16, 2756.84, 5959.406}, + {1749713640000LL, 2756.84, 2759.51, 2756.15, 2758.98, 4312.125}, + {1749713700000LL, 2758.98, 2759.48, 2756.59, 2757.67, 3161.368}, + {1749713760000LL, 2757.67, 2757.67, 2753.88, 2754, 3402.317}, + {1749713820000LL, 2754.01, 2754.51, 2751.55, 2752.54, 7100.498}, + {1749713880000LL, 2752.55, 2752.98, 2750.1, 2750.1, 12390.916}, + {1749713940000LL, 2750.11, 2750.46, 2746.81, 2749.87, 20574.495}, + {1749714000000LL, 2749.87, 2750.62, 2729.13, 2738.25, 66943.036}, + {1749714060000LL, 2738.26, 2739.66, 2733.73, 2738.12, 24340.276}, + {1749714120000LL, 2738.12, 2743.62, 2738.11, 2743.18, 13026.822}, + {1749714180000LL, 2743.18, 2744, 2742.19, 2743.99, 5804.365}, + {1749714240000LL, 2743.99, 2745.67, 2743.52, 2744.2, 5288.958}, + {1749714300000LL, 2744.2, 2746.8, 2741.25, 2745.66, 6806.766}, + {1749714360000LL, 2745.67, 2745.67, 2741.42, 2742.34, 4116.65}, + {1749714420000LL, 2742.33, 2748.92, 2742.33, 2747.8, 5856.439}, + {1749714480000LL, 2747.8, 2751, 2747.64, 2748.77, 7267.783}, + {1749714540000LL, 2748.76, 2749.7, 2746.46, 2746.74, 3125.602}, + {1749714600000LL, 2746.74, 2746.96, 2744.4, 2744.4, 5189.54}, + {1749714660000LL, 2744.4, 2748, 2744.09, 2747.43, 3581.558}, + {1749714720000LL, 2747.43, 2749.3, 2746.25, 2748.71, 4504.254}, + {1749714780000LL, 2748.67, 2748.67, 2746.07, 2746.07, 2730.375}, + {1749714840000LL, 2746.07, 2746.35, 2738.5, 2739.62, 10539.057}, + {1749714900000LL, 2739.62, 2743.75, 2738.17, 2743.5, 5171.206}, + {1749714960000LL, 2743.49, 2745.78, 2742.68, 2745.21, 2503.183}, + {1749715020000LL, 2745.22, 2747.5, 2743.02, 2743.03, 4290.553}, + {1749715080000LL, 2743.02, 2743.5, 2740.48, 2741.12, 3452.272}, + {1749715140000LL, 2741.12, 2743, 2739.41, 2739.45, 6660.898}, + {1749715200000LL, 2739.45, 2741.99, 2738.1, 2741.21, 6793.77}, + {1749715260000LL, 2741.2, 2743.72, 2738.25, 2738.36, 9102.862}, + {1749715320000LL, 2738.36, 2739.69, 2736.78, 2738.08, 6756.227}, + {1749715380000LL, 2738.05, 2740.56, 2737.49, 2740.4, 4871.323}, + {1749715440000LL, 2740.4, 2740.53, 2737.43, 2737.76, 3079.162}, + {1749715500000LL, 2737.76, 2737.76, 2731.18, 2732.15, 20080.037}, + {1749715560000LL, 2732.15, 2734.29, 2731.7, 2732.5, 13239.86}, + {1749715620000LL, 2732.51, 2736.12, 2732.01, 2735.78, 5825.979}, + {1749715680000LL, 2735.77, 2737.16, 2735, 2736.23, 2941.451}, + {1749715740000LL, 2736.24, 2738.58, 2735.36, 2737.53, 3567.762}, + {1749715800000LL, 2737.54, 2744.3, 2737.53, 2742.59, 7586.842}, + {1749715860000LL, 2742.6, 2744.71, 2741.51, 2744.71, 5199.48}, + {1749715920000LL, 2744.71, 2746.99, 2744.14, 2746.61, 4581.455}, + {1749715980000LL, 2746.62, 2749.58, 2746.44, 2748.35, 9027.737}, + {1749716040000LL, 2748.35, 2748.69, 2747.36, 2748.08, 4262.61}, + {1749716100000LL, 2748.08, 2748.09, 2744.43, 2746.85, 4303.373}, + {1749716160000LL, 2746.86, 2750.93, 2746.55, 2746.86, 5437.793}, + {1749716220000LL, 2746.87, 2750, 2745.3, 2747.49, 4513.882}, + {1749716280000LL, 2747.49, 2750.9, 2745.9, 2746.48, 3168.334}, + {1749716340000LL, 2746.49, 2749.35, 2746.48, 2749.34, 2759.103}, + {1749716400000LL, 2749.35, 2750.09, 2747.24, 2749.91, 5589.087}, + {1749716460000LL, 2749.92, 2752.94, 2749.91, 2752.72, 6999.644}, + {1749716520000LL, 2752.73, 2754.77, 2752.04, 2752.39, 5659.21}, + {1749716580000LL, 2752.38, 2753.77, 2751.82, 2753.25, 2754.837}, + {1749716640000LL, 2753.26, 2753.69, 2751.82, 2753.48, 3831.705}, + {1749716700000LL, 2753.49, 2753.77, 2751.75, 2752.08, 3655.99}, + {1749716760000LL, 2752.09, 2755.44, 2751.55, 2754.85, 5097.377}, + {1749716820000LL, 2754.84, 2755, 2751.93, 2751.93, 2322.246}, + {1749716880000LL, 2751.93, 2753.81, 2751.48, 2753.81, 1413.331}, + {1749716940000LL, 2753.81, 2754.92, 2752.82, 2753.09, 2181.872}, + {1749717000000LL, 2753.1, 2754.44, 2750.57, 2753.42, 2274.942}, + {1749717060000LL, 2753.42, 2754.69, 2752.5, 2752.88, 1359.687}, + {1749717120000LL, 2752.87, 2754.31, 2751.08, 2754.04, 2424.55}, + {1749717180000LL, 2754.03, 2754.29, 2752.09, 2754.27, 1504.506}, + {1749717240000LL, 2754.28, 2755.66, 2750.25, 2750.25, 5166.614}, + {1749717300000LL, 2750.25, 2750.76, 2748.28, 2749.72, 3577.809}, + {1749717360000LL, 2749.72, 2750.2, 2746.8, 2748.2, 2733.613}, + {1749717420000LL, 2748.19, 2750.4, 2747.22, 2750.4, 3364.283}, + {1749717480000LL, 2750.41, 2750.41, 2748, 2749.3, 1097.275}, + {1749717540000LL, 2749.29, 2749.88, 2748, 2748.09, 1208.869}, + {1749717600000LL, 2748.08, 2751.68, 2746.5, 2751.68, 1677.464}, + {1749717660000LL, 2751.7, 2752.67, 2749.73, 2750.62, 2024.219}, + {1749717720000LL, 2750.61, 2752, 2749.69, 2751.33, 1044.084}, + {1749717780000LL, 2751.33, 2753.25, 2751.33, 2753.23, 954.709}, + {1749717840000LL, 2753.24, 2754.22, 2752.45, 2752.59, 1965.816}, + {1749717900000LL, 2752.57, 2752.81, 2749.72, 2750.13, 1413.995}, + {1749717960000LL, 2750.13, 2751.3, 2749.7, 2751.3, 1584.015}, + {1749718020000LL, 2751.3, 2755.57, 2751.3, 2755.2, 2060.211}, + {1749718080000LL, 2755.21, 2758, 2753.71, 2757.61, 7058.256}, + {1749718140000LL, 2757.6, 2759.14, 2756, 2756, 5151.358}, + {1749718200000LL, 2756, 2759.13, 2755.55, 2758.46, 3634.736}, + {1749718260000LL, 2758.45, 2759, 2756.87, 2758.99, 1595.435}, + {1749718320000LL, 2759, 2759.49, 2758.14, 2758.66, 2256.39}, + {1749718380000LL, 2758.66, 2758.92, 2757.59, 2758.88, 1798.012}, + {1749718440000LL, 2758.89, 2759.77, 2757.75, 2758.39, 3345.111}, + {1749718500000LL, 2758.38, 2759.88, 2758.38, 2759.8, 1638.366}, + {1749718560000LL, 2759.8, 2759.8, 2757, 2757.41, 1317.929}, + {1749718620000LL, 2757.41, 2757.54, 2755.87, 2755.96, 1613.921}, + {1749718680000LL, 2755.97, 2757.05, 2755.33, 2756.12, 2086.664}, + {1749718740000LL, 2756.11, 2757, 2755.64, 2756.26, 1195.266}, + {1749718800000LL, 2756.26, 2756.3, 2755.12, 2756.05, 1602.889}, + {1749718860000LL, 2756.06, 2757.08, 2755.01, 2756.01, 1428.264}, + {1749718920000LL, 2756.01, 2759.82, 2755.58, 2759.81, 1835.524}, + {1749718980000LL, 2759.82, 2760.94, 2758.05, 2760.01, 5715.942}, + {1749719040000LL, 2760.01, 2760.01, 2755.62, 2755.63, 1498.169}, + {1749719100000LL, 2755.62, 2757.33, 2754.7, 2755.58, 2486.172}, + {1749719160000LL, 2755.58, 2757.22, 2754.56, 2754.57, 1346.068}, + {1749719220000LL, 2754.56, 2755.68, 2753.1, 2753.38, 3058.628}, + {1749719280000LL, 2753.39, 2753.77, 2753, 2753.37, 2508.509}, + {1749719340000LL, 2753.38, 2755.46, 2753.37, 2755.46, 1375.763}, + {1749719400000LL, 2755.46, 2756.18, 2754.77, 2755.6, 2591.599}, + {1749719460000LL, 2755.61, 2756.3, 2754.03, 2755.07, 1778.545}, + {1749719520000LL, 2755.07, 2756.28, 2754.89, 2756.27, 1019.353}, + {1749719580000LL, 2756.27, 2756.48, 2755.02, 2755.19, 1109.785}, + {1749719640000LL, 2755.19, 2755.19, 2752.25, 2752.26, 1529.907}, + {1749719700000LL, 2752.26, 2754.2, 2751.02, 2754.19, 2940.716}, + {1749719760000LL, 2754.2, 2755.44, 2752.32, 2752.56, 1577.938}, + {1749719820000LL, 2752.56, 2752.57, 2750.77, 2751.3, 2024.037}, + {1749719880000LL, 2751.3, 2752.14, 2750.51, 2750.88, 2373.832}, + {1749719940000LL, 2750.88, 2751.89, 2750.84, 2751.88, 1010.072}, + {1749720000000LL, 2751.89, 2753.61, 2751.65, 2752.22, 2840.388}, + {1749720060000LL, 2752.22, 2753.01, 2748.9, 2749.94, 4835.4}, + {1749720120000LL, 2749.95, 2750.47, 2749.57, 2750.46, 1296.085}, + {1749720180000LL, 2750.47, 2750.98, 2749.88, 2750.6, 771.484}, + {1749720240000LL, 2750.6, 2750.8, 2749.57, 2749.58, 976.299}, + {1749720300000LL, 2749.57, 2749.8, 2747.4, 2749.13, 3686.809}, + {1749720360000LL, 2749.13, 2755.5, 2748.63, 2755.49, 2424.132}, + {1749720420000LL, 2755.49, 2756.73, 2754.09, 2754.64, 2678.274}, + {1749720480000LL, 2754.63, 2755.13, 2753.01, 2753.21, 1252.085}, + {1749720540000LL, 2753.21, 2754.55, 2752.17, 2754.04, 2815.201}, + {1749720600000LL, 2754.05, 2757.4, 2754.04, 2756.22, 1131.997}, + {1749720660000LL, 2756.22, 2756.6, 2755.02, 2756.54, 592.744}, + {1749720720000LL, 2756.54, 2756.54, 2755.22, 2755.22, 925.553}, + {1749720780000LL, 2755.23, 2755.98, 2754.4, 2755, 2026.735}, + {1749720840000LL, 2755.01, 2755.58, 2754.15, 2755.04, 1374.128}, + {1749720900000LL, 2755.05, 2759.3, 2755.05, 2758.3, 2515.91}, + {1749720960000LL, 2758.31, 2759.27, 2758.01, 2759.26, 1530.26}, + {1749721020000LL, 2759.26, 2760.6, 2758.22, 2758.87, 5734.098}, + {1749721080000LL, 2758.86, 2758.92, 2756.06, 2757.18, 2490.363}, + {1749721140000LL, 2757.17, 2757.72, 2756.36, 2756.82, 1330.271}, + {1749721200000LL, 2756.83, 2757, 2756.09, 2756.25, 696.192}, + {1749721260000LL, 2756.25, 2756.96, 2755.87, 2756.69, 898.313}, + {1749721320000LL, 2756.68, 2756.71, 2756.09, 2756.37, 831.164}, + {1749721380000LL, 2756.36, 2756.37, 2755, 2755.14, 940.81}, + {1749721440000LL, 2755.15, 2755.26, 2754.43, 2754.65, 1223.228}, + {1749721500000LL, 2754.64, 2756.11, 2754.31, 2755.48, 1280.312}, + {1749721560000LL, 2755.48, 2755.48, 2753.07, 2753.68, 964.923}, + {1749721620000LL, 2753.67, 2753.68, 2751.72, 2751.86, 1857.391}, + {1749721680000LL, 2751.85, 2752.59, 2751.4, 2751.4, 1503.281}, + {1749721740000LL, 2751.41, 2752.86, 2751.4, 2752.76, 789.74}, + {1749721800000LL, 2752.76, 2752.84, 2751.42, 2751.76, 847.677}, + {1749721860000LL, 2751.76, 2752.35, 2751.44, 2751.89, 823.97}, + {1749721920000LL, 2751.9, 2751.99, 2751.2, 2751.6, 1068.813}, + {1749721980000LL, 2751.59, 2752.82, 2751.09, 2752.82, 1220.891}, + {1749722040000LL, 2752.81, 2753.65, 2751.24, 2751.99, 2248.777}, + {1749722100000LL, 2751.98, 2752.98, 2751.98, 2752.66, 1179.199}, + {1749722160000LL, 2752.67, 2752.99, 2751.34, 2752.56, 789.069}, + {1749722220000LL, 2752.56, 2753.53, 2752.12, 2752.81, 1105.198}, + {1749722280000LL, 2752.81, 2752.82, 2751.2, 2751.28, 1206.454}, + {1749722340000LL, 2751.28, 2751.69, 2751.1, 2751.51, 676.029}, + {1749722400000LL, 2751.52, 2751.52, 2750.02, 2750.87, 1838.36}, + {1749722460000LL, 2750.87, 2751.89, 2750.32, 2751.88, 806.231}, + {1749722520000LL, 2751.89, 2752.4, 2750.09, 2750.13, 952.792}, + {1749722580000LL, 2750.13, 2750.89, 2749.14, 2749.27, 2435.827}, + {1749722640000LL, 2749.26, 2749.27, 2744.88, 2745.45, 7970.667}, + {1749722700000LL, 2745.46, 2747.44, 2745.15, 2747.26, 3934.355}, + {1749722760000LL, 2747.27, 2747.27, 2745.56, 2746.5, 2776.293}, + {1749722820000LL, 2746.5, 2747.77, 2745.56, 2747.69, 2238.761}, + {1749722880000LL, 2747.68, 2747.99, 2745.99, 2746.39, 1842.19}, + {1749722940000LL, 2746.38, 2747.74, 2745.99, 2747.74, 1380.429}, + {1749723000000LL, 2747.74, 2748.96, 2746.83, 2747.82, 1815.379}, + {1749723060000LL, 2747.82, 2748.55, 2746.07, 2746.07, 1174.5}, + {1749723120000LL, 2746.07, 2746.61, 2741.66, 2745.65, 8762.182}, + {1749723180000LL, 2745.65, 2745.65, 2742.75, 2743.02, 2176.114}, + {1749723240000LL, 2743.03, 2745.99, 2743.02, 2743.94, 1045.473}, + {1749723300000LL, 2743.94, 2746.4, 2743.58, 2743.58, 1874.648}, + {1749723360000LL, 2743.57, 2743.57, 2738.09, 2739.27, 10838.322}, + {1749723420000LL, 2739.28, 2745.97, 2739.27, 2745.11, 6382.358}, + {1749723480000LL, 2745.12, 2745.9, 2742.22, 2743.5, 2738.306}, + {1749723540000LL, 2743.51, 2749.21, 2743.5, 2747.05, 3436.008}, + {1749723600000LL, 2747.05, 2749.9, 2745.47, 2745.48, 4245.857}, + {1749723660000LL, 2745.47, 2749.86, 2745.01, 2747.75, 3866.378}, + {1749723720000LL, 2747.75, 2749.44, 2747.11, 2748.13, 2891.803}, + {1749723780000LL, 2748.14, 2748.14, 2742.75, 2743.88, 1982.657}, + {1749723840000LL, 2743.89, 2744.12, 2740.67, 2740.87, 5782.557}, + {1749723900000LL, 2740.87, 2740.88, 2735.29, 2736.56, 10054.274}, + {1749723960000LL, 2736.56, 2738.03, 2735.44, 2736.15, 7189.678}, + {1749724020000LL, 2736.14, 2737.56, 2734.57, 2735.5, 5461.884}, + {1749724080000LL, 2735.49, 2738.45, 2733.06, 2737.99, 7856.303}, + {1749724140000LL, 2738, 2741.2, 2737.99, 2739.09, 3602.524}, + {1749724200000LL, 2739.08, 2742.81, 2738.08, 2741.64, 3560.689}, + {1749724260000LL, 2741.64, 2741.9, 2739.6, 2741.24, 3939.37}, + {1749724320000LL, 2741.24, 2741.54, 2740, 2740.73, 1241.436}, + {1749724380000LL, 2740.74, 2740.84, 2735, 2735.7, 5484.654}, + {1749724440000LL, 2735.7, 2737.66, 2735.31, 2737.11, 3608.577}, + {1749724500000LL, 2737.1, 2737.8, 2735, 2735, 2247.245}, + {1749724560000LL, 2735, 2736.49, 2732, 2734.35, 6468.893}, + {1749724620000LL, 2734.35, 2735.02, 2733.23, 2733.75, 4171.712}, + {1749724680000LL, 2733.76, 2738.44, 2733.38, 2737.48, 3204.981}, + {1749724740000LL, 2737.48, 2738.41, 2736.67, 2737.11, 3911.379}, + {1749724800000LL, 2737.11, 2741, 2736.88, 2740.01, 3533.301}, + {1749724860000LL, 2740, 2742.18, 2737.98, 2741.98, 3902.137}, + {1749724920000LL, 2741.98, 2742.66, 2740.79, 2741.81, 2639.146}, + {1749724980000LL, 2741.81, 2741.82, 2740.49, 2741.04, 1846.414}, + {1749725040000LL, 2741.05, 2742.12, 2740, 2741.89, 2280.233}, + {1749725100000LL, 2741.89, 2744.8, 2741, 2744, 4632.294}, + {1749725160000LL, 2744.01, 2745.56, 2741.83, 2742.78, 4901.963}, + {1749725220000LL, 2742.77, 2745.52, 2742.6, 2745.46, 4525.09}, + {1749725280000LL, 2745.45, 2745.46, 2742.43, 2743.24, 3306.164}, + {1749725340000LL, 2743.23, 2744.98, 2742.72, 2742.83, 3067.249}, + {1749725400000LL, 2742.82, 2743.55, 2741.17, 2741.78, 3505.129}, + {1749725460000LL, 2741.78, 2744.37, 2740.93, 2744.37, 1833.768}, + {1749725520000LL, 2744.38, 2745.32, 2742.55, 2743.92, 2498.972}, + {1749725580000LL, 2743.92, 2743.93, 2742.07, 2742.08, 1007.113}, + {1749725640000LL, 2742.08, 2743.27, 2740.5, 2743.26, 1146.879}, + {1749725700000LL, 2743.26, 2744.44, 2742.13, 2742.83, 2701.741}, + {1749725760000LL, 2742.83, 2742.94, 2740.5, 2740.57, 1978.094}, + {1749725820000LL, 2740.57, 2740.9, 2738.52, 2739.49, 3999.975}, + {1749725880000LL, 2739.53, 2752.45, 2739.52, 2751, 36779.966}, + {1749725940000LL, 2751, 2751, 2748.36, 2749.21, 6525.001}, + {1749726000000LL, 2749.21, 2752.18, 2747.72, 2751.82, 6624.496}, + {1749726060000LL, 2751.82, 2752.93, 2748.62, 2748.62, 4327.696}, + {1749726120000LL, 2748.62, 2749.06, 2745.65, 2745.99, 2802.018}, + {1749726180000LL, 2745.99, 2750.64, 2745.9, 2749.23, 2514.039}, + {1749726240000LL, 2749.23, 2751, 2749.16, 2750.1, 2122.185}, + {1749726300000LL, 2750.1, 2750.33, 2746.74, 2747.04, 2138.129}, + {1749726360000LL, 2747.04, 2747.27, 2744.82, 2745.09, 1522.158}, + {1749726420000LL, 2745.09, 2745.6, 2744.82, 2745.22, 1499.105}, + {1749726480000LL, 2745.22, 2745.51, 2744.28, 2744.29, 1510.987}, + {1749726540000LL, 2744.29, 2745.72, 2743.16, 2745.71, 2979.948}, + {1749726600000LL, 2745.71, 2748.54, 2745.7, 2746.44, 2351.218}, + {1749726660000LL, 2746.44, 2747.47, 2745.02, 2745.57, 855.693}, + {1749726720000LL, 2745.58, 2749.56, 2745.03, 2748.78, 3157.028}, + {1749726780000LL, 2748.78, 2749.7, 2748.16, 2748.78, 2113.461}, + {1749726840000LL, 2748.77, 2748.77, 2747.03, 2747.03, 1595.066}, + {1749726900000LL, 2747.03, 2747.2, 2744.68, 2745.44, 2196.746}, + {1749726960000LL, 2745.44, 2748.44, 2745.04, 2747.34, 2467.987}, + {1749727020000LL, 2747.34, 2747.35, 2745.55, 2746.38, 1729.134}, + {1749727080000LL, 2746.37, 2746.38, 2741.48, 2743.09, 4605.643}, + {1749727140000LL, 2743.1, 2744.12, 2741.8, 2742.6, 1882.448}, + {1749727200000LL, 2742.61, 2744.28, 2741.46, 2741.47, 1456.299}, + {1749727260000LL, 2741.47, 2741.97, 2740, 2740.88, 6285.618}, + {1749727320000LL, 2740.88, 2740.92, 2739.37, 2740, 2658.656}, + {1749727380000LL, 2740, 2740.01, 2737.66, 2738.9, 4882.15}, + {1749727440000LL, 2738.9, 2740.72, 2738.89, 2740.19, 3308.145}, + {1749727500000LL, 2740.2, 2741.84, 2739.26, 2741.43, 3733.019}, + {1749727560000LL, 2741.44, 2742.64, 2738.74, 2739.04, 1451.759}, + {1749727620000LL, 2739.04, 2739.43, 2737, 2737.55, 3132.023}, + {1749727680000LL, 2737.55, 2737.55, 2732.37, 2733.51, 12594.256}, + {1749727740000LL, 2733.5, 2734.6, 2718.46, 2726.73, 57976.356}, + {1749727800000LL, 2726.74, 2728.93, 2724, 2725.52, 20399.377}, + {1749727860000LL, 2725.53, 2727.72, 2722.64, 2727.45, 9098.11}, + {1749727920000LL, 2727.45, 2729.63, 2726.48, 2727.67, 8186.258}, + {1749727980000LL, 2727.67, 2730.46, 2726.58, 2726.76, 11484.288}, + {1749728040000LL, 2726.77, 2728.35, 2725, 2725.38, 6731.597}, + {1749728100000LL, 2725.39, 2728.4, 2724.13, 2726.01, 5396.163}, + {1749728160000LL, 2726.01, 2731.81, 2725.33, 2731.62, 7328.745}, + {1749728220000LL, 2731.62, 2733.93, 2731.5, 2731.65, 4818.376}, + {1749728280000LL, 2731.66, 2732.08, 2728.04, 2730.28, 5934.597}, + {1749728340000LL, 2730.27, 2730.39, 2727.19, 2727.71, 3306.243}, + {1749728400000LL, 2727.71, 2735.4, 2727.71, 2734.68, 7539.9}, + {1749728460000LL, 2734.68, 2735.66, 2732.6, 2732.8, 3150.713}, + {1749728520000LL, 2732.79, 2733.3, 2730.65, 2733.16, 1696.14}, + {1749728580000LL, 2733.16, 2734.17, 2732.1, 2734.16, 3001.017}, + {1749728640000LL, 2734.17, 2735.57, 2734, 2734.08, 3803.991}, + {1749728700000LL, 2734.07, 2735, 2733.51, 2733.69, 2672.893}, + {1749728760000LL, 2733.69, 2736.5, 2730, 2736.49, 11715.715}, + {1749728820000LL, 2736.5, 2738.22, 2735, 2735.01, 5638.099}, + {1749728880000LL, 2735.01, 2736.63, 2734.23, 2734.67, 3372.617}, + {1749728940000LL, 2734.67, 2735.23, 2734.01, 2734.57, 2336.586}, + {1749729000000LL, 2734.57, 2738.5, 2734.51, 2735.04, 5283.264}, + {1749729060000LL, 2735.05, 2736.53, 2731.86, 2732.13, 2735.339}, + {1749729120000LL, 2732.13, 2732.14, 2730.1, 2731.01, 3201.02}, + {1749729180000LL, 2731, 2731.65, 2729.64, 2729.85, 3209.022}, + {1749729240000LL, 2729.85, 2731.53, 2728.89, 2730.39, 3244.424}, + {1749729300000LL, 2730.39, 2731.83, 2729.86, 2730.88, 2077.384}, + {1749729360000LL, 2730.88, 2731.3, 2729.36, 2730, 2471.277}, + {1749729420000LL, 2730.01, 2730.56, 2728.79, 2729.48, 4819.36}, + {1749729480000LL, 2729.48, 2729.67, 2728, 2728.59, 5187.844}, + {1749729540000LL, 2728.58, 2728.96, 2727.71, 2728.76, 3930.073}, + {1749729600000LL, 2728.76, 2731, 2728.6, 2729.76, 4812.249}, + {1749729660000LL, 2729.75, 2732.5, 2729.75, 2731.59, 4217.529}, + {1749729720000LL, 2731.59, 2732.12, 2730.17, 2731.11, 2776.437}, + {1749729780000LL, 2731.1, 2733.64, 2731.02, 2732.66, 3801.309}, + {1749729840000LL, 2732.67, 2734.48, 2732.6, 2733.98, 3437.381}, + {1749729900000LL, 2733.98, 2734.65, 2730, 2730.42, 5176.736}, + {1749729960000LL, 2730.42, 2730.47, 2726.13, 2728.92, 6861.856}, + {1749730020000LL, 2728.91, 2728.91, 2722.02, 2722.17, 11783.37}, + {1749730080000LL, 2722.17, 2725.52, 2722, 2722.74, 8241.493}, + {1749730140000LL, 2722.73, 2723.11, 2712, 2713.91, 45691.375}, + {1749730200000LL, 2713.9, 2717.64, 2710.78, 2714.68, 25518.111}, + {1749730260000LL, 2714.68, 2717.41, 2713.78, 2717.35, 11938.378}, + {1749730320000LL, 2717.34, 2717.61, 2715.46, 2716.15, 5354.871}, + {1749730380000LL, 2716.15, 2716.19, 2711.98, 2715.94, 6485.461}, + {1749730440000LL, 2715.94, 2719.73, 2715.94, 2717.01, 6613.093}, + {1749730500000LL, 2717.01, 2719, 2714.06, 2717.14, 13856.067}, + {1749730560000LL, 2717.13, 2719.5, 2717.13, 2718.41, 3485.766}, + {1749730620000LL, 2718.41, 2720.98, 2717.62, 2718.81, 6354.938}, + {1749730680000LL, 2718.81, 2722.78, 2718.81, 2720.82, 6803.039}, + {1749730740000LL, 2720.82, 2722.99, 2720.03, 2721.57, 6174.485}, + {1749730800000LL, 2721.57, 2722, 2718.82, 2718.82, 3081.197}, + {1749730860000LL, 2718.83, 2721.63, 2718.1, 2721.36, 3356.192}, + {1749730920000LL, 2721.35, 2724.79, 2721.31, 2724.47, 7314.499}, + {1749730980000LL, 2724.48, 2725.45, 2723.66, 2725.03, 5192.57}, + {1749731040000LL, 2725.03, 2725.49, 2724, 2724, 4030.636}, + {1749731100000LL, 2724.01, 2725.1, 2722.03, 2724.82, 2921.864}, + {1749731160000LL, 2724.82, 2725.87, 2724.36, 2725.47, 3528.566}, + {1749731220000LL, 2725.47, 2725.48, 2722.77, 2723.71, 2151.238}, + {1749731280000LL, 2723.71, 2724.76, 2723.45, 2723.96, 2059.705}, + {1749731340000LL, 2723.97, 2724.25, 2722.77, 2723.62, 2818.326}, + {1749731400000LL, 2723.63, 2731.86, 2721.88, 2729.96, 23022.033}, + {1749731460000LL, 2729.96, 2738.72, 2729.19, 2736.06, 21227.623}, + {1749731520000LL, 2736.07, 2739, 2734.35, 2737.73, 10563.684}, + {1749731580000LL, 2737.74, 2737.99, 2734.25, 2735.36, 6845.34}, + {1749731640000LL, 2735.36, 2737.99, 2735.2, 2737.25, 4336.13}, + {1749731700000LL, 2737.24, 2737.72, 2735.28, 2735.67, 4358.79}, + {1749731760000LL, 2735.68, 2743.44, 2735.67, 2742.84, 13665.29}, + {1749731820000LL, 2742.84, 2744.82, 2741.74, 2743.78, 11950.637}, + {1749731880000LL, 2743.78, 2744.87, 2741.01, 2741.17, 10357.984}, + {1749731940000LL, 2741.17, 2742.74, 2740, 2741.21, 6364.282}, + {1749732000000LL, 2741.21, 2744.53, 2739.08, 2739.62, 11110.905}, + {1749732060000LL, 2739.62, 2742.65, 2737.57, 2741.9, 9567.238}, + {1749732120000LL, 2741.78, 2744.51, 2741.76, 2742.86, 12578.685}, + {1749732180000LL, 2742.86, 2744.66, 2742.05, 2742.77, 4909.172}, + {1749732240000LL, 2742.77, 2744.24, 2740.62, 2742.7, 4691.509}, + {1749732300000LL, 2742.7, 2743.3, 2739.74, 2739.74, 3945.059}, + {1749732360000LL, 2739.72, 2740.51, 2737.36, 2737.68, 6011.203}, + {1749732420000LL, 2737.68, 2738.97, 2736.37, 2738.28, 3819.602}, + {1749732480000LL, 2738.28, 2741.92, 2738.17, 2739.74, 4180.408}, + {1749732540000LL, 2739.74, 2739.75, 2737.6, 2739.2, 2789.862}, + {1749732600000LL, 2739.19, 2739.2, 2736.3, 2737.97, 3697.776}, + {1749732660000LL, 2737.96, 2738.11, 2734.27, 2734.47, 3757.889}, + {1749732720000LL, 2734.46, 2736.2, 2733.4, 2735.82, 4126.495}, + {1749732780000LL, 2735.82, 2737.8, 2734.91, 2734.91, 2662.873}, + {1749732840000LL, 2734.91, 2735.46, 2732.6, 2733.17, 5149.644}, + {1749732900000LL, 2733.16, 2736.47, 2732.59, 2734.98, 3963.908}, + {1749732960000LL, 2734.99, 2735.09, 2730.85, 2731.38, 4359.105}, + {1749733020000LL, 2731.39, 2734.14, 2730.2, 2732.6, 4060.371}, + {1749733080000LL, 2732.61, 2735.14, 2732.12, 2735.14, 1518.18}, + {1749733140000LL, 2735.14, 2737.37, 2734.87, 2735.43, 3738.655}, +}; + +// The same span resampled to 15m by scripts/derive_corpus_feeds.py's rule +// (the corpus' derived chart feed). +constexpr FeedBar kEth15[52] = { + {1749686400000LL, 2770.35, 2776.35, 2763.71, 2769.06, 58695.702}, + {1749687300000LL, 2769.07, 2783.9, 2762.98, 2763.23, 71238.384}, + {1749688200000LL, 2763.24, 2776.97, 2757.78, 2774.1, 55825.636}, + {1749689100000LL, 2774.1, 2777, 2768.69, 2774.49, 29988.053}, + {1749690000000LL, 2774.49, 2777.58, 2764.14, 2766.39, 38064.88}, + {1749690900000LL, 2766.39, 2774.4, 2760.01, 2764.57, 28631.009}, + {1749691800000LL, 2764.57, 2767.6, 2750.8, 2766.18, 72268.809}, + {1749692700000LL, 2766.18, 2775.53, 2759.26, 2774.87, 39985.04}, + {1749693600000LL, 2774.87, 2783.48, 2772.41, 2773.35, 58374.974}, + {1749694500000LL, 2773.35, 2774.85, 2766.13, 2772, 28878.368}, + {1749695400000LL, 2772, 2773.69, 2766.55, 2766.56, 16506.324}, + {1749696300000LL, 2766.55, 2770.88, 2755.55, 2756.87, 62745.353}, + {1749697200000LL, 2756.87, 2760.98, 2752.82, 2760.39, 49901.694}, + {1749698100000LL, 2760.4, 2761, 2745.01, 2751, 91114.92}, + {1749699000000LL, 2750.99, 2770.27, 2750.45, 2770.27, 82199.673}, + {1749699900000LL, 2770.27, 2776.33, 2763.74, 2763.99, 86331.698}, + {1749700800000LL, 2764, 2768.42, 2755.64, 2756.22, 34926.097}, + {1749701700000LL, 2756.22, 2762.79, 2750.1, 2757.55, 57550.7}, + {1749702600000LL, 2757.55, 2761.16, 2748.58, 2752.13, 39551.712}, + {1749703500000LL, 2752.12, 2762.38, 2751.24, 2761.53, 25639.467}, + {1749704400000LL, 2761.52, 2761.53, 2750.55, 2757.09, 29729.45}, + {1749705300000LL, 2757.1, 2767.2, 2756.69, 2765.44, 37234.337}, + {1749706200000LL, 2765.43, 2769.77, 2757.22, 2758.36, 39632.911}, + {1749707100000LL, 2758.36, 2768.23, 2758.16, 2764, 25404.629}, + {1749708000000LL, 2764, 2765.32, 2758.19, 2759.46, 16609.696}, + {1749708900000LL, 2759.47, 2764.21, 2753.92, 2762.49, 33363.292}, + {1749709800000LL, 2762.5, 2775.75, 2761.83, 2772.59, 67051.831}, + {1749710700000LL, 2772.58, 2772.59, 2766.57, 2767.16, 30839.302}, + {1749711600000LL, 2767.16, 2772.99, 2763.8, 2767.26, 27650.115}, + {1749712500000LL, 2767.27, 2769.58, 2756, 2757.12, 43166.54}, + {1749713400000LL, 2757.13, 2759.51, 2729.13, 2744.2, 185204.465}, + {1749714300000LL, 2744.2, 2751, 2738.17, 2739.45, 75796.136}, + {1749715200000LL, 2739.45, 2749.58, 2731.18, 2748.08, 106916.557}, + {1749716100000LL, 2748.08, 2755.44, 2744.43, 2753.09, 59687.784}, + {1749717000000LL, 2753.1, 2755.66, 2746.5, 2752.59, 32378.44}, + {1749717900000LL, 2752.57, 2759.88, 2749.7, 2756.26, 37749.665}, + {1749718800000LL, 2756.26, 2760.94, 2752.25, 2752.26, 30885.117}, + {1749719700000LL, 2752.26, 2756.73, 2747.4, 2754.04, 33502.752}, + {1749720600000LL, 2754.05, 2760.6, 2754.04, 2754.65, 24241.766}, + {1749721500000LL, 2754.64, 2756.11, 2751.09, 2751.51, 17561.724}, + {1749722400000LL, 2751.52, 2752.4, 2741.66, 2743.94, 41149.553}, + {1749723300000LL, 2743.94, 2749.9, 2733.06, 2739.09, 78203.557}, + {1749724200000LL, 2739.08, 2742.81, 2732, 2741.89, 52040.167}, + {1749725100000LL, 2741.89, 2752.45, 2738.52, 2749.21, 82409.398}, + {1749726000000LL, 2749.21, 2752.93, 2743.16, 2747.03, 38113.227}, + {1749726900000LL, 2747.03, 2748.44, 2718.46, 2726.73, 110360.239}, + {1749727800000LL, 2726.74, 2735.66, 2722.64, 2734.08, 101875.515}, + {1749728700000LL, 2734.07, 2738.5, 2727.71, 2728.76, 61894.917}, + {1749729600000LL, 2728.76, 2734.65, 2710.78, 2717.01, 152709.649}, + {1749730500000LL, 2717.01, 2725.87, 2714.06, 2723.62, 73129.088}, + {1749731400000LL, 2723.63, 2744.87, 2721.88, 2742.7, 155549.302}, + {1749732300000LL, 2742.7, 2743.3, 2730.2, 2735.43, 57781.03}, +}; diff --git a/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/meta.json b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/meta.json new file mode 100644 index 00000000..c07808cf --- /dev/null +++ b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM F1e entry_bar_index POOC ETH magnifier", + "slug": "hm-f1e-ebi-pooc-eth-mag", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T19:02:06.267Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-11", + "to": "2025-06-13", + "timeoutMs": 120000 + }, + "pine_sha256": "e0031970763afa46c071a3dfa6c42b183f55b098ca50a84fcf9a6b78f2a62ff5", + "tv_trades_sha256": "0c6badea92194fe4f9c2df36a7f9aadb496f6d953d1dcdb5884cda5ec307d839", + "tv_trades_rows": 8 +} diff --git a/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/metrics.json b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/metrics.json new file mode 100644 index 00000000..a3d7042b --- /dev/null +++ b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-f1e-ebi-pooc-eth-mag", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-11", + "to": "2025-06-13", + "deepBacktesting": true, + "trades": 4, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "e0031970763afa46c071a3dfa6c42b183f55b098ca50a84fcf9a6b78f2a62ff5", + "tvTradesCsvHash": "0c6badea92194fe4f9c2df36a7f9aadb496f6d953d1dcdb5884cda5ec307d839", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74410", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1749600000000, + "to": 1749772800000 + }, + "returnedRange": { + "from": 1749600000000, + "to": 1749772800000 + }, + "rangeProof": "covered", + "rawFrameSha256": "7b7f3660d26fd83fd868be4df908f75e84dfc40dc1165486f9adf6c2aad04d94", + "decodedReportSha256": "be25d10a457b3c9fa2ca2f8fb58ecfab96cf15affa042d8f59955c4cf2075206", + "closedTrades": 4, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T19:02:06.266Z" +} diff --git a/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/strategy.pine b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/strategy.pine new file mode 100644 index 00000000..1c664980 --- /dev/null +++ b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1(e) probe (R5 lane H-MEASURE, research group agg). +// Does an aggregated chart count CHART bars in strategy.opentrades.entry_bar_index +// and strategy.closedtrades.entry_bar_index / exit_bar_index? +// BINANCE:ETHUSDT.P 15m, process_orders_on_close: every fill is at a bar's close. +// This copy runs with use_bar_magnifier=true (the magnified-path control of the same tape). +// Entries at fixed bar opens (UTC); every exit is keyed on a bar-index difference: +// A long 2025-06-12 01:00, closed once bar_index - strategy.opentrades.entry_bar_index(0) >= 1 +// B short 2025-06-12 05:00, closed once held >= 3 +// C long 2025-06-12 09:00, closed once held >= (last closed exit_bar_index - entry_bar_index) + 2 +// D short entered when bar_index - strategy.closedtrades.exit_bar_index(2) == 4, closed once held >= 2 +// A position still open 3 h after its entry is closed "timeout" (a guard; never fires when +// bar indices count chart bars). Exit comments carry the held count TradingView computed. +strategy("HM F1e entry_bar_index POOC ETH magnifier", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=true, margin_long=100, margin_short=100, use_bar_magnifier=true) + +tA = timestamp("UTC", 2025, 6, 12, 1, 0) +tB = timestamp("UTC", 2025, 6, 12, 5, 0) +tC = timestamp("UTC", 2025, 6, 12, 9, 0) + +int lastDur = na +if strategy.closedtrades > 0 + lastDur := strategy.closedtrades.exit_bar_index(strategy.closedtrades - 1) - strategy.closedtrades.entry_bar_index(strategy.closedtrades - 1) + +if strategy.opentrades > 0 + held = bar_index - strategy.opentrades.entry_bar_index(0) + id = strategy.opentrades.entry_id(0) + int need = id == "A" ? 1 : id == "B" ? 3 : id == "D" ? 2 : lastDur + 2 + if held >= need + strategy.close(id, comment="held" + str.tostring(held)) + else if time - strategy.opentrades.entry_time(0) >= 3 * 60 * 60 * 1000 + strategy.close(id, comment="timeout") +else + if time == tA + strategy.entry("A", strategy.long) + else if time == tB + strategy.entry("B", strategy.short) + else if time == tC + strategy.entry("C", strategy.long) + else if strategy.closedtrades == 3 and bar_index - strategy.closedtrades.exit_bar_index(2) == 4 + strategy.entry("D", strategy.short) diff --git a/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/tv_trades.csv b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/tv_trades.csv new file mode 100644 index 00000000..08683efa --- /dev/null +++ b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth-mag/tv_trades.csv @@ -0,0 +1,9 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-12 09:15,held1,2764.57,1,2766.39,-1.82,-0.07,0,8.01,0.29,-6.38,-0.23,-1.82,0.00,1 +1,Entry long,2025-06-12 09:00,A,2766.39,1,2766.39,-1.82,-0.07,0,8.01,0.29,-6.38,-0.23,-1.82,0.00,1 +2,Exit short,2025-06-12 13:45,held3,2764,1,2757.09,-6.91,-0.25,0,1.86,0.07,-12.68,-0.46,-8.73,-0.01,3 +2,Entry short,2025-06-12 13:00,B,2757.09,1,2757.09,-6.91,-0.25,0,1.86,0.07,-12.68,-0.46,-8.73,-0.01,3 +3,Exit long,2025-06-12 18:15,held5,2739.09,1,2752.26,-13.17,-0.48,0,8.34,0.30,-19.2,-0.70,-21.9,-0.02,5 +3,Entry long,2025-06-12 17:00,C,2752.26,1,2752.26,-13.17,-0.48,0,8.34,0.30,-19.2,-0.70,-21.9,-0.02,5 +4,Exit short,2025-06-12 19:45,held2,2728.76,1,2726.73,-2.03,-0.07,0,4.09,0.15,-11.77,-0.43,-23.93,-0.02,2 +4,Entry short,2025-06-12 19:15,D,2726.73,1,2726.73,-2.03,-0.07,0,4.09,0.15,-11.77,-0.43,-23.93,-0.02,2 \ No newline at end of file diff --git a/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/meta.json b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/meta.json new file mode 100644 index 00000000..f61ca2c2 --- /dev/null +++ b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM F1e entry_bar_index POOC ETH", + "slug": "hm-f1e-ebi-pooc-eth", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:39:29.185Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-11", + "to": "2025-06-13", + "timeoutMs": 120000 + }, + "pine_sha256": "96c2f0a7d2c40c719cd9c0d74d4fdaa8b6dd4ea4a85ba573ded7545cb216a4e4", + "tv_trades_sha256": "e7004ca5e207f86d3535bd20157dbc16af0d221968bf3b18464ab80109506c5f", + "tv_trades_rows": 8 +} diff --git a/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/metrics.json b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/metrics.json new file mode 100644 index 00000000..a5b59a57 --- /dev/null +++ b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-f1e-ebi-pooc-eth", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-11", + "to": "2025-06-13", + "deepBacktesting": true, + "trades": 4, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "96c2f0a7d2c40c719cd9c0d74d4fdaa8b6dd4ea4a85ba573ded7545cb216a4e4", + "tvTradesCsvHash": "e7004ca5e207f86d3535bd20157dbc16af0d221968bf3b18464ab80109506c5f", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74386", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1749600000000, + "to": 1749772800000 + }, + "returnedRange": { + "from": 1749600000000, + "to": 1749772800000 + }, + "rangeProof": "covered", + "rawFrameSha256": "2e8ef4508f85c462d1ee03c32f7a3efd9694c9bfbd98e871a6353c886859bb94", + "decodedReportSha256": "e8ce0cfba20633ba3e07e31793de589559d4da36e4a526574990f4df3b23f6bb", + "closedTrades": 4, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:39:29.181Z" +} diff --git a/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/strategy.pine b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/strategy.pine new file mode 100644 index 00000000..15479f2e --- /dev/null +++ b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/strategy.pine @@ -0,0 +1,42 @@ +//@version=6 +// H-MEASURE F1(e) probe (R5 lane H-MEASURE, research group agg). +// Does an aggregated chart count CHART bars in strategy.opentrades.entry_bar_index +// and strategy.closedtrades.entry_bar_index / exit_bar_index? +// BINANCE:ETHUSDT.P 15m, process_orders_on_close: every fill is at a bar's close. +// Entries at fixed bar opens (UTC); every exit is keyed on a bar-index difference: +// A long 2025-06-12 01:00, closed once bar_index - strategy.opentrades.entry_bar_index(0) >= 1 +// B short 2025-06-12 05:00, closed once held >= 3 +// C long 2025-06-12 09:00, closed once held >= (last closed exit_bar_index - entry_bar_index) + 2 +// D short entered when bar_index - strategy.closedtrades.exit_bar_index(2) == 4, closed once held >= 2 +// A position still open 3 h after its entry is closed "timeout" (a guard; never fires when +// bar indices count chart bars). Exit comments carry the held count TradingView computed. +strategy("HM F1e entry_bar_index POOC ETH", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0, slippage=0, + process_orders_on_close=true, margin_long=100, margin_short=100) + +tA = timestamp("UTC", 2025, 6, 12, 1, 0) +tB = timestamp("UTC", 2025, 6, 12, 5, 0) +tC = timestamp("UTC", 2025, 6, 12, 9, 0) + +int lastDur = na +if strategy.closedtrades > 0 + lastDur := strategy.closedtrades.exit_bar_index(strategy.closedtrades - 1) - strategy.closedtrades.entry_bar_index(strategy.closedtrades - 1) + +if strategy.opentrades > 0 + held = bar_index - strategy.opentrades.entry_bar_index(0) + id = strategy.opentrades.entry_id(0) + int need = id == "A" ? 1 : id == "B" ? 3 : id == "D" ? 2 : lastDur + 2 + if held >= need + strategy.close(id, comment="held" + str.tostring(held)) + else if time - strategy.opentrades.entry_time(0) >= 3 * 60 * 60 * 1000 + strategy.close(id, comment="timeout") +else + if time == tA + strategy.entry("A", strategy.long) + else if time == tB + strategy.entry("B", strategy.short) + else if time == tC + strategy.entry("C", strategy.long) + else if strategy.closedtrades == 3 and bar_index - strategy.closedtrades.exit_bar_index(2) == 4 + strategy.entry("D", strategy.short) diff --git a/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/tv_trades.csv b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/tv_trades.csv new file mode 100644 index 00000000..7bd8e415 --- /dev/null +++ b/tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth/tv_trades.csv @@ -0,0 +1,9 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-12 09:15,held1,2764.57,1,2766.39,-1.82,-0.07,0,8.01,0.29,-6.38,-0.23,-1.82,0.00,1 +1,Entry long,2025-06-12 09:00,A,2766.39,1,2766.39,-1.82,-0.07,0,8.01,0.29,-6.38,-0.23,-1.82,0.00,1 +2,Exit short,2025-06-12 13:45,held3,2764,1,2757.09,-6.91,-0.25,0,0.4,0.01,-12.68,-0.46,-8.73,-0.01,3 +2,Entry short,2025-06-12 13:00,B,2757.09,1,2757.09,-6.91,-0.25,0,0.4,0.01,-12.68,-0.46,-8.73,-0.01,3 +3,Exit long,2025-06-12 18:15,held5,2739.09,1,2752.26,-13.17,-0.48,0,8.34,0.30,-19.2,-0.70,-21.9,-0.02,5 +3,Entry long,2025-06-12 17:00,C,2752.26,1,2752.26,-13.17,-0.48,0,8.34,0.30,-19.2,-0.70,-21.9,-0.02,5 +4,Exit short,2025-06-12 19:45,held2,2728.76,1,2726.73,-2.03,-0.07,0,4.09,0.15,-11.77,-0.43,-23.93,-0.02,2 +4,Entry short,2025-06-12 19:15,D,2726.73,1,2726.73,-2.03,-0.07,0,4.09,0.15,-11.77,-0.43,-23.93,-0.02,2 \ No newline at end of file diff --git a/tests/fixtures/magnified_aggregated/README.md b/tests/fixtures/magnified_aggregated/README.md new file mode 100644 index 00000000..588db6f7 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/README.md @@ -0,0 +1,23 @@ +# Magnified aggregated tapes (R5 lane H-MEASURE, AUDIT4 X14 "F1's magnified residual") + +Lane F1's differential probe (its eight variants v0..v7) as +Pine, on NYSE:F 15m over the three days `tests/fixtures/session_islastbar/bars.inc` +holds (2025-07-02, the 2025-07-03 half day, 2025-07-07); the script's bar counter +starts at 2025-07-02 09:30 ET, the fixture's first bar. +`tests/test_magnified_aggregated_tape.cpp` replays the magnifier-on tapes on the +1m bars aggregated to 15m under the magnifier, and the magnifier-off tapes on the +15m chart and the plain aggregation. + +Each directory is one `lab tv` export (`ws-report-v1`, `rangeProof: covered`, +window 2025-07-01 .. 2025-07-08), byte-identical. + +| probe | variant | trades | tvTradesSha256 | note | +|---|---|---|---|---| +| `hm-mag-diff-v0` | plain, magnifier on | 12 | `53c28aaf8caa6ed1708a9aa42f2965db37724c87d95691fb16bbb56dbb69906e` | `tv-tape-hm-mag-diff-v0-53c28aaf` | +| `hm-mag-diff-v2` | calc_on_order_fills, magnifier on | 12 | `444e7bc4eaac5b16e224007225bc55ed1c36fecd4453b8eab95e6c1ea70c6656` | `tv-tape-hm-mag-diff-v2-444e7bc4` | +| `hm-mag-diff-v4` | slippage 2, pyramiding 3, magnifier on | 14 | `d18ca4ad316311a4aad91503800402f1873c927c3cadc575b3b1a1ab669d196e` | `tv-tape-hm-mag-diff-v4-d18ca4ad` | +| `hm-chart-diff-v0` | plain, magnifier off | 12 | `b2652feb01b83236315a92112acb5b108626d11d959ed4b17effc6087454ae2c` | `tv-tape-hm-chart-diff-v0-b2652feb` | +| `hm-chart-diff-v4` | slippage 2, pyramiding 3, magnifier off | 14 | `5293cc0c2bc88dabba65ff73a024d88b28292845c537f0f4ccf58922e4d502a1` | `tv-tape-hm-chart-diff-v4-5293cc0c` | + +TradingView dates every magnified fill at its chart bar's open; the rows are +compared at chart-bar granularity. Excursions are not compared. diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v0/meta.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v0/meta.json new file mode 100644 index 00000000..753d3d41 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v0/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM CHART diff v0 pooc0 coof0 slip0 pyr1", + "slug": "hm-chart-diff-v0", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:48:41.576Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "bf60e7ac44b995129ac51fb7b511ca9b7982aab3b970ffac74ff6d660366a5ef", + "tv_trades_sha256": "b2652feb01b83236315a92112acb5b108626d11d959ed4b17effc6087454ae2c", + "tv_trades_rows": 24 +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v0/metrics.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v0/metrics.json new file mode 100644 index 00000000..47593751 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v0/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-chart-diff-v0", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 12, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "bf60e7ac44b995129ac51fb7b511ca9b7982aab3b970ffac74ff6d660366a5ef", + "tvTradesCsvHash": "b2652feb01b83236315a92112acb5b108626d11d959ed4b17effc6087454ae2c", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74395", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "284ca1dd5c3f8fb1fda2e6146653abd933430861a234c1d3079096f44cfb1824", + "decodedReportSha256": "270e2f978ecee0e88bbba4a37f672d84d7b62a9002690187bf1f827c1f19eb3e", + "closedTrades": 12, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:48:41.575Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v0/strategy.pine b/tests/fixtures/magnified_aggregated/hm-chart-diff-v0/strategy.pine new file mode 100644 index 00000000..0b992ac5 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v0/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 0: +// process_orders_on_close=0, calc_on_order_fills=0, slippage=0, pyramiding=1, +// with the bar magnifier OFF (the chart-path control). NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM CHART diff v0 pooc0 coof0 slip0 pyr1", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v0/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-chart-diff-v0/tv_trades.csv new file mode 100644 index 00000000..a2900e0e --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v0/tv_trades.csv @@ -0,0 +1,25 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,2 +1,Entry long,2025-07-02 22:30,PL,11.47,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,2 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1158,7.7676,0.67,0.2324,7.8842,0.68,-0.1158,-0.01,13.5376,0.01,0 +2,Entry long,2025-07-02 23:15,ML,11.58,100,1158,7.7676,0.67,0.2324,7.8842,0.68,-0.1158,-0.01,13.5376,0.01,0 +3,Exit long,2025-07-03 01:45,Close position order,11.66,100,1167,-1.2333,-0.11,0.2333,0.8833,0.08,-3.1167,-0.27,12.3043,0.01,6 +3,Entry long,2025-07-03 00:15,PL,11.67,100,1167,-1.2333,-0.11,0.2333,0.8833,0.08,-3.1167,-0.27,12.3043,0.01,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1167,4.7661,0.41,0.2339,4.8833,0.42,0,0.00,17.0704,0.02,0 +4,Entry long,2025-07-03 02:15,PL,11.67,100,1167,4.7661,0.41,0.2339,4.8833,0.42,0,0.00,17.0704,0.02,0 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1171,7.765,0.66,0.235,7.8829,0.67,-0.1171,-0.01,24.8354,0.02,6 +5,Entry long,2025-07-03 02:30,ML,11.71,100,1171,7.765,0.66,0.235,7.8829,0.67,-0.1171,-0.01,24.8354,0.02,6 +6,Exit long,2025-07-03 23:30,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,22.5984,0.02,2 +6,Entry long,2025-07-03 23:00,PL,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,22.5984,0.02,2 +7,Exit short,2025-07-04 00:30,SX,11.8,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,30.3616,0.03,3 +7,Entry short,2025-07-03 23:45,PS,11.88,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,30.3616,0.03,3 +8,Exit long,2025-07-07 21:30,PX,11.62,100,1180,-18.2342,-1.55,0.2342,3.882,0.33,-18.118,-1.54,12.1274,0.01,1 +8,Entry long,2025-07-04 00:45,PL,11.8,100,1180,-18.2342,-1.55,0.2342,3.882,0.33,-18.118,-1.54,12.1274,0.01,1 +9,Exit short,2025-07-07 22:00,SX,11.75,100,1170,-5.2345,-0.45,0.2345,0,0.00,-5.117,-0.44,6.8929,0.01,1 +9,Entry short,2025-07-07 21:45,PS,11.7,100,1170,-5.2345,-0.45,0.2345,0,0.00,-5.117,-0.44,6.8929,0.01,1 +10,Exit long,2025-07-07 23:45,MX,11.58,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,-2.3396,0.00,3 +10,Entry long,2025-07-07 23:00,ML,11.67,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,-2.3396,0.00,3 +11,Exit long,2025-07-08 02:15,PX,11.51,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-8.5704,-0.01,6 +11,Entry long,2025-07-08 00:45,PL,11.57,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-8.5704,-0.01,6 +12,Exit long,2025-07-08 02:45,PX,11.58,100,1154,3.7688,0.33,0.2312,3.8846,0.34,0,0.00,-4.8016,0.00,1 +12,Entry long,2025-07-08 02:30,PL,11.54,100,1154,3.7688,0.33,0.2312,3.8846,0.34,0,0.00,-4.8016,0.00,1 \ No newline at end of file diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v4/meta.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v4/meta.json new file mode 100644 index 00000000..d0b08122 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v4/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM CHART diff v4 pooc0 coof0 slip2 pyr3", + "slug": "hm-chart-diff-v4", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:49:28.141Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "73fa2e6034a3e52c39c61d477c3a3d056820d81475b2d6541d57b8f98494c99f", + "tv_trades_sha256": "5293cc0c2bc88dabba65ff73a024d88b28292845c537f0f4ccf58922e4d502a1", + "tv_trades_rows": 28 +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v4/metrics.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v4/metrics.json new file mode 100644 index 00000000..b564d56c --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v4/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-chart-diff-v4", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 14, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "73fa2e6034a3e52c39c61d477c3a3d056820d81475b2d6541d57b8f98494c99f", + "tvTradesCsvHash": "5293cc0c2bc88dabba65ff73a024d88b28292845c537f0f4ccf58922e4d502a1", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74403", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "eb0e3e06d7805fd0b772df6ecf8531ba9dbd6fdc720597b9f767026e8cb92730", + "decodedReportSha256": "8d895d47ce50b352efaef365ea90ca2d06a01425107512a26199b3d2d4277ecc", + "closedTrades": 14, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:49:28.139Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v4/strategy.pine b/tests/fixtures/magnified_aggregated/hm-chart-diff-v4/strategy.pine new file mode 100644 index 00000000..49d47f80 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v4/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 4: +// process_orders_on_close=0, calc_on_order_fills=0, slippage=2, pyramiding=3, +// with the bar magnifier OFF (the chart-path control). NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM CHART diff v4 pooc0 coof0 slip2 pyr3", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=3, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=2, process_orders_on_close=false, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v4/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-chart-diff-v4/tv_trades.csv new file mode 100644 index 00000000..bf216a2b --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v4/tv_trades.csv @@ -0,0 +1,29 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1149,3.7698,0.33,0.2302,3.8851,0.34,-5.1149,-0.45,3.7698,0.00,2 +1,Entry long,2025-07-02 22:30,PL,11.49,100,1149,3.7698,0.33,0.2302,3.8851,0.34,-5.1149,-0.45,3.7698,0.00,2 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1160,5.7674,0.50,0.2326,5.884,0.51,-2.116,-0.18,9.5372,0.01,0 +2,Entry long,2025-07-02 23:15,ML,11.6,100,1160,5.7674,0.50,0.2326,5.884,0.51,-2.116,-0.18,9.5372,0.01,0 +3,Exit long,2025-07-03 01:45,Close position order,11.64,100,1169,-5.2333,-0.45,0.2333,0,0.00,-5.1169,-0.44,4.3039,0.00,6 +3,Entry long,2025-07-03 00:15,PL,11.69,100,1169,-5.2333,-0.45,0.2333,0,0.00,-5.1169,-0.44,4.3039,0.00,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1169,2.7659,0.24,0.2341,2.8831,0.25,0,0.00,7.0698,0.01,0 +4,Entry long,2025-07-03 02:15,PL,11.69,100,1169,2.7659,0.24,0.2341,2.8831,0.25,0,0.00,7.0698,0.01,0 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1173,5.7648,0.49,0.2352,5.8827,0.50,-2.1173,-0.18,12.7171,0.01,6 +5,Entry long,2025-07-03 02:30,ML,11.73,100,1173,5.7648,0.49,0.2352,5.8827,0.50,-2.1173,-0.18,12.7171,0.01,6 +6,Exit long,2025-07-03 21:30,MX,11.79,100,1175,3.7646,0.32,0.2354,3.8825,0.33,-3.1175,-0.27,16.5992,0.02,5 +6,Entry long,2025-07-03 02:45,ML,11.75,100,1175,3.7646,0.32,0.2354,3.8825,0.33,-3.1175,-0.27,16.5992,0.02,5 +7,Exit long,2025-07-03 23:30,Close position order,11.82,100,1188,-6.237,-0.52,0.237,0,0.00,-8.1188,-0.68,10.2436,0.01,2 +7,Entry long,2025-07-03 23:00,PL,11.88,100,1188,-6.237,-0.52,0.237,0,0.00,-8.1188,-0.68,10.2436,0.01,2 +8,Exit long,2025-07-03 23:45,PS,11.88,100,1186,1.7626,0.15,0.2374,1.8814,0.16,-4.1186,-0.35,12.1248,0.01,1 +8,Entry long,2025-07-03 23:30,ML,11.86,100,1186,1.7626,0.15,0.2374,1.8814,0.16,-4.1186,-0.35,12.1248,0.01,1 +9,Exit long,2025-07-07 21:30,PX,11.6,100,1182,-22.2342,-1.88,0.2342,1.8818,0.16,-22.1182,-1.87,-10.1094,-0.01,1 +9,Entry long,2025-07-04 00:45,PL,11.82,100,1182,-22.2342,-1.88,0.2342,1.8818,0.16,-22.1182,-1.87,-10.1094,-0.01,1 +10,Exit short,2025-07-07 22:00,SX,11.77,100,1170,-7.2347,-0.62,0.2347,0,0.00,-7.117,-0.61,-17.3441,-0.02,1 +10,Entry short,2025-07-07 21:45,PS,11.7,100,1170,-7.2347,-0.62,0.2347,0,0.00,-7.117,-0.61,-17.3441,-0.02,1 +11,Exit long,2025-07-07 23:45,MX,11.56,100,1169,-13.2325,-1.13,0.2325,0,0.00,-13.1169,-1.12,-30.693,-0.03,3 +11,Entry long,2025-07-07 23:00,ML,11.69,100,1169,-13.2325,-1.13,0.2325,0,0.00,-13.1169,-1.12,-30.693,-0.03,3 +12,Exit long,2025-07-07 23:45,MX,11.56,100,1164,-8.232,-0.71,0.232,0,0.00,-8.1164,-0.70,-38.8086,-0.04,2 +12,Entry long,2025-07-07 23:15,ML,11.64,100,1164,-8.232,-0.71,0.232,0,0.00,-8.1164,-0.70,-38.8086,-0.04,2 +13,Exit long,2025-07-08 02:15,PX,11.49,100,1159,-10.2308,-0.88,0.2308,1.8841,0.16,-10.1159,-0.87,-49.0394,-0.05,6 +13,Entry long,2025-07-08 00:45,PL,11.59,100,1159,-10.2308,-0.88,0.2308,1.8841,0.16,-10.1159,-0.87,-49.0394,-0.05,6 +14,Exit long,2025-07-08 02:45,PX,11.58,100,1156,1.7686,0.15,0.2314,1.8844,0.16,-1.1156,-0.10,-47.2708,-0.05,1 +14,Entry long,2025-07-08 02:30,PL,11.56,100,1156,1.7686,0.15,0.2314,1.8844,0.16,-1.1156,-0.10,-47.2708,-0.05,1 \ No newline at end of file diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v0/meta.json b/tests/fixtures/magnified_aggregated/hm-mag-diff-v0/meta.json new file mode 100644 index 00000000..253fb4cb --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v0/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM MAG diff v0 pooc0 coof0 slip0 pyr1", + "slug": "hm-mag-diff-v0", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:48:22.147Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "c95e1af263acfc999df840efa0f9c08da1f8eb2c1304d2c3c0dc01a902c287db", + "tv_trades_sha256": "53c28aaf8caa6ed1708a9aa42f2965db37724c87d95691fb16bbb56dbb69906e", + "tv_trades_rows": 24 +} diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v0/metrics.json b/tests/fixtures/magnified_aggregated/hm-mag-diff-v0/metrics.json new file mode 100644 index 00000000..0fc1ab6c --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v0/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-mag-diff-v0", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 12, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "c95e1af263acfc999df840efa0f9c08da1f8eb2c1304d2c3c0dc01a902c287db", + "tvTradesCsvHash": "53c28aaf8caa6ed1708a9aa42f2965db37724c87d95691fb16bbb56dbb69906e", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74391", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "3d20e62df368d1af7dd94c9e478f686df2e255c43b60785b10fc531cb5ffb6f8", + "decodedReportSha256": "44475dc5b7e81844563583f2c4dd11344e73017dfb70c919fa2d366b27653586", + "closedTrades": 12, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:48:22.145Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v0/strategy.pine b/tests/fixtures/magnified_aggregated/hm-mag-diff-v0/strategy.pine new file mode 100644 index 00000000..05c44365 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v0/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 0: +// process_orders_on_close=0, calc_on_order_fills=0, slippage=0, pyramiding=1, +// with the bar magnifier ON. NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM MAG diff v0 pooc0 coof0 slip0 pyr1", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=true) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v0/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-mag-diff-v0/tv_trades.csv new file mode 100644 index 00000000..f6254aca --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v0/tv_trades.csv @@ -0,0 +1,25 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,2 +1,Entry long,2025-07-02 22:30,PL,11.47,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,2 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1158,7.7676,0.67,0.2324,7.8842,0.68,-1.1158,-0.10,13.5376,0.01,0 +2,Entry long,2025-07-02 23:15,ML,11.58,100,1158,7.7676,0.67,0.2324,7.8842,0.68,-1.1158,-0.10,13.5376,0.01,0 +3,Exit long,2025-07-03 01:45,Close position order,11.66,100,1167,-1.2333,-0.11,0.2333,0.8833,0.08,-3.1167,-0.27,12.3043,0.01,6 +3,Entry long,2025-07-03 00:15,PL,11.67,100,1167,-1.2333,-0.11,0.2333,0.8833,0.08,-3.1167,-0.27,12.3043,0.01,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1167,4.7661,0.41,0.2339,4.8833,0.42,0,0.00,17.0704,0.02,0 +4,Entry long,2025-07-03 02:15,PL,11.67,100,1167,4.7661,0.41,0.2339,4.8833,0.42,0,0.00,17.0704,0.02,0 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1171,7.765,0.66,0.235,7.8829,0.67,-0.1171,-0.01,24.8354,0.02,6 +5,Entry long,2025-07-03 02:30,ML,11.71,100,1171,7.765,0.66,0.235,7.8829,0.67,-0.1171,-0.01,24.8354,0.02,6 +6,Exit long,2025-07-03 23:30,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,22.5984,0.02,2 +6,Entry long,2025-07-03 23:00,PL,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,22.5984,0.02,2 +7,Exit short,2025-07-04 00:30,SX,11.8,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,30.3616,0.03,3 +7,Entry short,2025-07-03 23:45,PS,11.88,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,30.3616,0.03,3 +8,Exit long,2025-07-07 21:30,PX,11.62,100,1180,-18.2342,-1.55,0.2342,3.882,0.33,-18.118,-1.54,12.1274,0.01,1 +8,Entry long,2025-07-04 00:45,PL,11.8,100,1180,-18.2342,-1.55,0.2342,3.882,0.33,-18.118,-1.54,12.1274,0.01,1 +9,Exit short,2025-07-07 22:00,SX,11.75,100,1170,-5.2345,-0.45,0.2345,2.883,0.25,-5.117,-0.44,6.8929,0.01,1 +9,Entry short,2025-07-07 21:45,PS,11.7,100,1170,-5.2345,-0.45,0.2345,2.883,0.25,-5.117,-0.44,6.8929,0.01,1 +10,Exit long,2025-07-07 23:45,MX,11.58,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,-2.3396,0.00,3 +10,Entry long,2025-07-07 23:00,ML,11.67,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,-2.3396,0.00,3 +11,Exit long,2025-07-08 02:15,PX,11.51,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-8.5704,-0.01,6 +11,Entry long,2025-07-08 00:45,PL,11.57,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-8.5704,-0.01,6 +12,Exit long,2025-07-08 02:45,PX,11.58,100,1154,3.7688,0.33,0.2312,3.8846,0.34,-0.1154,-0.01,-4.8016,0.00,1 +12,Entry long,2025-07-08 02:30,PL,11.54,100,1154,3.7688,0.33,0.2312,3.8846,0.34,-0.1154,-0.01,-4.8016,0.00,1 \ No newline at end of file diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v2/meta.json b/tests/fixtures/magnified_aggregated/hm-mag-diff-v2/meta.json new file mode 100644 index 00000000..7b3a5da9 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v2/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM MAG diff v2 pooc0 coof1 slip0 pyr1", + "slug": "hm-mag-diff-v2", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:48:32.395Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "05e229d0ab47a651444793edcce6f7739c4349e1121a830cbb41451a49842be9", + "tv_trades_sha256": "444e7bc4eaac5b16e224007225bc55ed1c36fecd4453b8eab95e6c1ea70c6656", + "tv_trades_rows": 24 +} diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v2/metrics.json b/tests/fixtures/magnified_aggregated/hm-mag-diff-v2/metrics.json new file mode 100644 index 00000000..673aa3cb --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v2/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-mag-diff-v2", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 12, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "05e229d0ab47a651444793edcce6f7739c4349e1121a830cbb41451a49842be9", + "tvTradesCsvHash": "444e7bc4eaac5b16e224007225bc55ed1c36fecd4453b8eab95e6c1ea70c6656", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74393", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "6c663c3d1cdd9cc2feae3b548789bc41fc30bb529838099d453b292fc9012d8e", + "decodedReportSha256": "77addba910941aea7cdffd2b9366bf1b173f1d855144331c8b27bc261e5a1fe1", + "closedTrades": 12, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:48:32.391Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v2/strategy.pine b/tests/fixtures/magnified_aggregated/hm-mag-diff-v2/strategy.pine new file mode 100644 index 00000000..fc39e801 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v2/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 2: +// process_orders_on_close=0, calc_on_order_fills=1, slippage=0, pyramiding=1, +// with the bar magnifier ON. NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM MAG diff v2 pooc0 coof1 slip0 pyr1", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=true) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v2/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-mag-diff-v2/tv_trades.csv new file mode 100644 index 00000000..c6cdb064 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v2/tv_trades.csv @@ -0,0 +1,25 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,2 +1,Entry long,2025-07-02 22:30,PL,11.47,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,2 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1154,11.768,1.02,0.232,11.8846,1.03,-1.1154,-0.10,17.538,0.02,1 +2,Entry long,2025-07-02 23:00,ML,11.54,100,1154,11.768,1.02,0.232,11.8846,1.03,-1.1154,-0.10,17.538,0.02,1 +3,Exit long,2025-07-03 01:45,Close position order,11.66,100,1167,-1.2333,-0.11,0.2333,0.8833,0.08,-3.1167,-0.27,16.3047,0.02,6 +3,Entry long,2025-07-03 00:15,PL,11.67,100,1167,-1.2333,-0.11,0.2333,0.8833,0.08,-3.1167,-0.27,16.3047,0.02,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1167,4.7661,0.41,0.2339,4.8833,0.42,-3.1167,-0.27,21.0708,0.02,2 +4,Entry long,2025-07-03 01:45,PL,11.67,100,1167,4.7661,0.41,0.2339,4.8833,0.42,-3.1167,-0.27,21.0708,0.02,2 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1172,6.7649,0.58,0.2351,6.8828,0.59,-1.1172,-0.10,27.8357,0.03,7 +5,Entry long,2025-07-03 02:15,ML,11.72,100,1172,6.7649,0.58,0.2351,6.8828,0.59,-1.1172,-0.10,27.8357,0.03,7 +6,Exit long,2025-07-03 23:30,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,25.5987,0.03,2 +6,Entry long,2025-07-03 23:00,PL,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,25.5987,0.03,2 +7,Exit short,2025-07-04 00:30,SX,11.8,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,33.3619,0.03,3 +7,Entry short,2025-07-03 23:45,PS,11.88,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,33.3619,0.03,3 +8,Exit long,2025-07-07 21:30,PX,11.62,100,1180,-18.2342,-1.55,0.2342,3.882,0.33,-18.118,-1.54,15.1277,0.02,2 +8,Entry long,2025-07-04 00:30,PL,11.8,100,1180,-18.2342,-1.55,0.2342,3.882,0.33,-18.118,-1.54,15.1277,0.02,2 +9,Exit short,2025-07-07 22:00,SX,11.75,100,1170,-5.2345,-0.45,0.2345,4.883,0.42,-5.117,-0.44,9.8932,0.01,2 +9,Entry short,2025-07-07 21:30,PS,11.7,100,1170,-5.2345,-0.45,0.2345,4.883,0.42,-5.117,-0.44,9.8932,0.01,2 +10,Exit long,2025-07-07 23:45,MX,11.58,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,0.6607,0.00,3 +10,Entry long,2025-07-07 23:00,ML,11.67,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,0.6607,0.00,3 +11,Exit long,2025-07-08 02:15,PX,11.51,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-5.5701,-0.01,6 +11,Entry long,2025-07-08 00:45,PL,11.57,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-5.5701,-0.01,6 +12,Exit long,2025-07-08 02:45,PX,11.58,100,1154,3.7688,0.33,0.2312,3.8846,0.34,-3.1154,-0.27,-1.8013,0.00,2 +12,Entry long,2025-07-08 02:15,PL,11.54,100,1154,3.7688,0.33,0.2312,3.8846,0.34,-3.1154,-0.27,-1.8013,0.00,2 \ No newline at end of file diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v4/meta.json b/tests/fixtures/magnified_aggregated/hm-mag-diff-v4/meta.json new file mode 100644 index 00000000..b3314c68 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v4/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM MAG diff v4 pooc0 coof0 slip2 pyr3", + "slug": "hm-mag-diff-v4", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:49:06.980Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "aa3d8391b3a7022bb998cdbdc755db1c1d59cc2bda1462edc86c1f2b877395a2", + "tv_trades_sha256": "d18ca4ad316311a4aad91503800402f1873c927c3cadc575b3b1a1ab669d196e", + "tv_trades_rows": 28 +} diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v4/metrics.json b/tests/fixtures/magnified_aggregated/hm-mag-diff-v4/metrics.json new file mode 100644 index 00000000..9c7d4968 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v4/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-mag-diff-v4", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 14, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "aa3d8391b3a7022bb998cdbdc755db1c1d59cc2bda1462edc86c1f2b877395a2", + "tvTradesCsvHash": "d18ca4ad316311a4aad91503800402f1873c927c3cadc575b3b1a1ab669d196e", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74399", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "7c362a237f495f8834dcec155c549fde2c609fd7caf18668b6c92328167128d7", + "decodedReportSha256": "701dea92c92494cbc8414471a6a6df339123a99af52e6199f87925f1919cd806", + "closedTrades": 14, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:49:06.976Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v4/strategy.pine b/tests/fixtures/magnified_aggregated/hm-mag-diff-v4/strategy.pine new file mode 100644 index 00000000..3b08c369 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v4/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 4: +// process_orders_on_close=0, calc_on_order_fills=0, slippage=2, pyramiding=3, +// with the bar magnifier ON. NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM MAG diff v4 pooc0 coof0 slip2 pyr3", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=3, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=2, process_orders_on_close=false, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=true) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v4/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-mag-diff-v4/tv_trades.csv new file mode 100644 index 00000000..61a2412c --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v4/tv_trades.csv @@ -0,0 +1,29 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1149,3.7698,0.33,0.2302,3.8851,0.34,-5.1149,-0.45,3.7698,0.00,2 +1,Entry long,2025-07-02 22:30,PL,11.49,100,1149,3.7698,0.33,0.2302,3.8851,0.34,-5.1149,-0.45,3.7698,0.00,2 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1160,5.7674,0.50,0.2326,5.884,0.51,-3.116,-0.27,9.5372,0.01,0 +2,Entry long,2025-07-02 23:15,ML,11.6,100,1160,5.7674,0.50,0.2326,5.884,0.51,-3.116,-0.27,9.5372,0.01,0 +3,Exit long,2025-07-03 01:45,Close position order,11.64,100,1169,-5.2333,-0.45,0.2333,0,0.00,-5.1169,-0.44,4.3039,0.00,6 +3,Entry long,2025-07-03 00:15,PL,11.69,100,1169,-5.2333,-0.45,0.2333,0,0.00,-5.1169,-0.44,4.3039,0.00,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1169,2.7659,0.24,0.2341,2.8831,0.25,-1.1169,-0.10,7.0698,0.01,0 +4,Entry long,2025-07-03 02:15,PL,11.69,100,1169,2.7659,0.24,0.2341,2.8831,0.25,-1.1169,-0.10,7.0698,0.01,0 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1173,5.7648,0.49,0.2352,5.8827,0.50,-2.1173,-0.18,12.7171,0.01,6 +5,Entry long,2025-07-03 02:30,ML,11.73,100,1173,5.7648,0.49,0.2352,5.8827,0.50,-2.1173,-0.18,12.7171,0.01,6 +6,Exit long,2025-07-03 21:30,MX,11.79,100,1175,3.7646,0.32,0.2354,3.8825,0.33,-3.1175,-0.27,16.5992,0.02,5 +6,Entry long,2025-07-03 02:45,ML,11.75,100,1175,3.7646,0.32,0.2354,3.8825,0.33,-3.1175,-0.27,16.5992,0.02,5 +7,Exit long,2025-07-03 23:30,Close position order,11.82,100,1188,-6.237,-0.52,0.237,0,0.00,-8.1188,-0.68,10.2436,0.01,2 +7,Entry long,2025-07-03 23:00,PL,11.88,100,1188,-6.237,-0.52,0.237,0,0.00,-8.1188,-0.68,10.2436,0.01,2 +8,Exit long,2025-07-03 23:45,PS,11.88,100,1186,1.7626,0.15,0.2374,1.8814,0.16,-4.1186,-0.35,12.1248,0.01,1 +8,Entry long,2025-07-03 23:30,ML,11.86,100,1186,1.7626,0.15,0.2374,1.8814,0.16,-4.1186,-0.35,12.1248,0.01,1 +9,Exit long,2025-07-07 21:30,PX,11.6,100,1182,-22.2342,-1.88,0.2342,1.8818,0.16,-22.1182,-1.87,-10.1094,-0.01,1 +9,Entry long,2025-07-04 00:45,PL,11.82,100,1182,-22.2342,-1.88,0.2342,1.8818,0.16,-22.1182,-1.87,-10.1094,-0.01,1 +10,Exit short,2025-07-07 22:00,SX,11.77,100,1170,-7.2347,-0.62,0.2347,2.883,0.25,-7.117,-0.61,-17.3441,-0.02,1 +10,Entry short,2025-07-07 21:45,PS,11.7,100,1170,-7.2347,-0.62,0.2347,2.883,0.25,-7.117,-0.61,-17.3441,-0.02,1 +11,Exit long,2025-07-07 23:45,MX,11.56,100,1169,-13.2325,-1.13,0.2325,0,0.00,-13.1169,-1.12,-30.693,-0.03,3 +11,Entry long,2025-07-07 23:00,ML,11.69,100,1169,-13.2325,-1.13,0.2325,0,0.00,-13.1169,-1.12,-30.693,-0.03,3 +12,Exit long,2025-07-07 23:45,MX,11.56,100,1164,-8.232,-0.71,0.232,0,0.00,-8.1164,-0.70,-38.8086,-0.04,2 +12,Entry long,2025-07-07 23:15,ML,11.64,100,1164,-8.232,-0.71,0.232,0,0.00,-8.1164,-0.70,-38.8086,-0.04,2 +13,Exit long,2025-07-08 02:15,PX,11.49,100,1159,-10.2308,-0.88,0.2308,1.8841,0.16,-10.1159,-0.87,-49.0394,-0.05,6 +13,Entry long,2025-07-08 00:45,PL,11.59,100,1159,-10.2308,-0.88,0.2308,1.8841,0.16,-10.1159,-0.87,-49.0394,-0.05,6 +14,Exit long,2025-07-08 02:45,PX,11.58,100,1156,1.7686,0.15,0.2314,1.8844,0.16,-2.1156,-0.18,-47.2708,-0.05,1 +14,Entry long,2025-07-08 02:30,PL,11.56,100,1156,1.7686,0.15,0.2314,1.8844,0.16,-2.1156,-0.18,-47.2708,-0.05,1 \ No newline at end of file diff --git a/tests/test_adapter_security_route_conditions.cpp b/tests/test_adapter_security_route_conditions.cpp new file mode 100644 index 00000000..799bca0c --- /dev/null +++ b/tests/test_adapter_security_route_conditions.cpp @@ -0,0 +1,435 @@ +// test_adapter_security_route_conditions.cpp — R5 lane H-MEASURE (AUDIT4 §6 M20 / G2-23, X14). +// +// The Pine adapter declares its request.security sites to the kernel as +// NativeRunSpec::subscriptions +// (PineStrategyHost::declare_security_sites_to_kernel, lane R3b) unless one of +// the conditions of that predicate holds; then EVERY site of the run stays on +// the source evaluator. This unit takes each reachable condition once, on a +// site the predicate would otherwise route, and holds two facts: +// +// 1. the route: the site stays on the evaluator (the running spec names no +// subscription and native_series_bar never answers), and the control +// site without the condition is the kernel's; +// 2. where a NativeTimeframeSubscription can spell the same site, the paired +// differential: the same site on the same bars through a bare kernel host +// declaring it (native_series_bar(0).close on every script bar) against +// the value the generated body reads on every chart bar. The chart bars +// where the two differ are pinned exactly: none where the condition is a +// conservative exclusion on these feeds, the Pine rule's own bars where +// it is not. +// +// Conditions the predicate names that no run can trip on its own -- subsumed +// by an earlier test or behind a validate-time throw -- are listed in the +// ADR-0001 `subscriptions` row, not here. The stream (tick input) and the auxiliary +// feed are pinned by route only: the kernel refuses ticks while a series is +// declared (tests/test_native_htf_subscriptions_stream.cpp) and the auxiliary +// drive's differential is tests/test_native_auxiliary_feed_twin.cpp. + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; + +namespace { + +int checks = 0; +int failures = 0; +const char* scenario = "initialization"; + +#define CHECK(expression) \ + do { \ + ++checks; \ + if (!(expression)) { \ + ++failures; \ + std::printf("FAIL [%s] line %d: %s\n", scenario, __LINE__, \ + #expression); \ + } \ + } while (false) + +std::int64_t utc_ms(int y, int m, int d, int h = 0, int mi = 0) { + y -= (m <= 2); + long era = (y >= 0 ? y : y - 399) / 400; + unsigned yoe = static_cast(y - era * 400); + unsigned doy = (153u * static_cast(m + (m > 2 ? -3 : 9)) + 2) / 5 + + static_cast(d) - 1; + unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + long days = era * 146097L + static_cast(doe) - 719468L; + return (static_cast(days) * 86400 + h * 3600 + mi * 60) * 1000; +} + +constexpr std::int64_t kMinute = 60000; +constexpr std::int64_t kQuarter = 15 * kMinute; + +// A 24x7 feed of `step`-minute bars from 2024-01-01 00:00Z + offset quarters. +std::vector grid_bars(int n, int offset_quarters = 0, std::int64_t step = kQuarter) { + const std::int64_t origin = utc_ms(2024, 1, 1) + offset_quarters * kQuarter; + std::vector bars; + for (int i = 0; i < n; ++i) { + const double open = 100.0 + i; + Bar bar{}; + bar.open = open; bar.high = open + 2.0; bar.low = open - 1.0; bar.close = open + 0.5; + bar.volume = static_cast(i + 1); + bar.timestamp = origin + static_cast(i) * step; + bars.push_back(bar); + } + return bars; +} + +// Regular-hours 15m bars, 09:30..15:45 America/New_York (EST), five weekdays. +std::vector rth_bars(int days) { + std::vector bars; + int day = 2, made = 0, i = 0; + while (made < days) { + const std::int64_t d0 = utc_ms(2024, 1, day); + const int weekday = static_cast(((d0 / 86400000) + 4) % 7); // 0 = Sunday + ++day; + if (weekday == 0 || weekday == 6) continue; + for (int k = 0; k < 26; ++k, ++i) { + const double open = 100.0 + i; + Bar bar{}; + bar.open = open; bar.high = open + 2.0; bar.low = open - 1.0; bar.close = open + 0.5; + bar.volume = static_cast(i + 1); + bar.timestamp = d0 + (14 * 60 + 30) * kMinute + k * kQuarter; + bars.push_back(bar); + } + ++made; + } + return bars; +} + +struct Site { + std::string tf; + bool lookahead = false; + bool gaps = false; + bool heikinashi = false; + bool lower_tf = false; + int sec_id = 0; +}; + +// The adapter in the generated shape (tests/test_adapter_htf_relower.cpp): +// _req_sec_0 is the last evaluated bar's close; the body reads it per bar. +class GeneratedShape final : public source::PineStrategyHost { +public: + Site site; + double value = na(); + std::vector visible; + bool series_seen = false; + void configure_security_evaluators() override { + security_eval_states_.clear(); + value = na(); + visible.clear(); + series_seen = false; + if (site.lower_tf) { + register_security_lower_tf_eval(site.sec_id, site.tf, input_tf_); + } else { + register_security_eval(site.sec_id, site.tf, input_tf_, site.lookahead, site.gaps, + site.heikinashi); + } + } + void evaluate_security(int, const Bar& bar, bool) override { value = bar.close; } + void clear_security(int) override { value = na(); } + void on_source_bar(const Bar&) override { + visible.push_back(value); + if (native_series_bar(0).has_value()) series_seen = true; + } + bool kernel_routed() const { + const auto view = native_state(); + return view.spec != nullptr && !view.spec->subscriptions.empty(); + } +}; + +// The kernel alone, declaring the same series. +class KernelSeries final : public NativeStrategyHost { +public: + std::vector visible; + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + const auto bar = native_series_bar(0); + visible.push_back(bar ? bar->close : na()); + } +}; + +struct Shape { + const char* input_tf = "15"; + const char* script_tf = "15"; + bool magnifier = false; + const char* type = "crypto"; + const char* session = "24x7"; + const char* timezone = "UTC"; + double range_start_ms = 0.0; // security_range_start_na_warmup + bool lookahead_projection = false; // historical_security_lookahead_projection +}; + +void prime(GeneratedShape& pine, const Shape& shape) { + pine.set_syminfo_timezone(shape.timezone); + pine.set_syminfo_session(shape.session); + pine.set_syminfo_type(shape.type); + if (shape.range_start_ms > 0.0) + pine.set_syminfo_metadata("security_range_start_na_warmup", shape.range_start_ms); + if (shape.lookahead_projection) + pine.set_syminfo_metadata("historical_security_lookahead_projection", 1.0); +} + +void run_pine(GeneratedShape& pine, const std::vector& bars, const Shape& shape) { + pine.run(bars.data(), static_cast(bars.size()), shape.input_tf, shape.script_tf, + shape.magnifier, 4, MagnifierDistribution::ENDPOINTS); + CHECK(pine.last_error().empty()); +} + +NativeRunSpec kernel_spec(const std::vector& bars, const Site& site, const Shape& shape) { + NativeRunSpec spec; + spec.identity = {std::string(shape.session) + "@" + shape.timezone, 1}; + spec.input_tf = shape.input_tf; + spec.script_tf = shape.script_tf; + spec.ticker = "SERIES"; + spec.tickerid = "TEST:SERIES"; + spec.type = shape.type; + spec.currency = "USD"; + spec.basecurrency = "USD"; + spec.description = "security route differential"; + spec.volumetype = "base"; + spec.timezone = shape.timezone; + spec.session = shape.session; + spec.initial_capital = 10000.0; + spec.point_value = 1.0; + spec.account_fx = 1.0; + spec.price_tick = 0.01; + spec.fee_kind = NativeFeeKind::CashPerExecution; + spec.fee_value = 0.0; + NativeTimeframeSubscription series; + series.tf = site.tf; + series.lookahead = site.lookahead; + series.gaps = site.gaps; + spec.subscriptions.push_back(series); + if (shape.magnifier) { + // The adapter's own projection of the magnifier (PineExecutionAdapter::project). + if (std::string(shape.input_tf) == shape.script_tf) { + IntrabarPath::synthesized path; + spec.intrabar.value = path; + } else { + IntrabarPath::lower_tf path; + path.bars = bars; + path.tf = shape.input_tf; + path.sample_eligibility = IntrabarPath::SampleEligibility::ContinuousSegments; + spec.intrabar.value = path; + } + } + return spec; +} + +using Ranges = std::vector>; + +Ranges differing(const std::vector& a, const std::vector& b) { + Ranges out; + const std::size_t n = std::min(a.size(), b.size()); + const auto differs = [&](std::size_t k) { + return !((std::isnan(a[k]) && std::isnan(b[k])) || a[k] == b[k]); + }; + for (std::size_t i = 0; i < n;) { + if (!differs(i)) { ++i; continue; } + std::size_t j = i; + while (j + 1 < n && differs(j + 1)) ++j; + out.push_back({static_cast(i), static_cast(j)}); + i = j + 1; + } + return out; +} + +void print_ranges(const Ranges& ranges) { + std::printf(" differing chart bars:"); + if (ranges.empty()) std::printf(" none"); + for (const auto& r : ranges) std::printf(" %d..%d", r.first, r.second); + std::printf("\n"); +} + +// Route + differential for one condition. +void condition(const char* name, const std::vector& bars, const Site& site, + const Shape& shape, bool kernel_route, bool kernel_expressible, + const Ranges& expected) { + scenario = name; + GeneratedShape pine; + pine.site = site; + prime(pine, shape); + run_pine(pine, bars, shape); + CHECK(pine.kernel_routed() == kernel_route); + CHECK(pine.series_seen == kernel_route); + if (!kernel_expressible) return; + KernelSeries kernel; + const auto applied = kernel.configure_native(kernel_spec(bars, site, shape)); + CHECK(applied.status == NativeSetupStatus::Applied); + kernel.run(bars.data(), static_cast(bars.size()), shape.input_tf, shape.script_tf, + shape.magnifier, 4, MagnifierDistribution::ENDPOINTS); + CHECK(kernel.last_error().empty()); + CHECK(kernel.visible.size() == pine.visible.size()); + const Ranges got = differing(pine.visible, kernel.visible); + CHECK(got == expected); + if (got != expected) { + std::printf(" [%s]\n", name); + print_ranges(got); + } +} + +// ---- 1. the control: a plain site on the chart timeframe is the kernel's ---- + +void test_controls() { + condition("control: plain 60 on 15/15", grid_bars(64), Site{"60"}, Shape{}, true, true, {}); + condition("control: gaps_on 60 on 15/15", grid_bars(64), Site{"60", false, true}, Shape{}, + true, true, {}); +} + +// ---- 2. run-shape conditions ------------------------------------------------ + +void test_run_shape_conditions() { + Shape aggregated; + aggregated.script_tf = "60"; + // input_tf != script_tf (pine_strategy_host.cpp:1551): conservative on + // every feed below -- the kernel's series is the evaluator's, bar for bar. + condition("aggregated: 240 on 15 -> 60", grid_bars(128), Site{"240"}, aggregated, false, true, {}); + condition("aggregated: 60 on 15 -> 60", grid_bars(128), Site{"60"}, aggregated, false, true, {}); + Shape rth_aggregated = aggregated; + rth_aggregated.type = "stock"; + rth_aggregated.session = "0930-1600"; + rth_aggregated.timezone = "America/New_York"; + condition("aggregated: D on RTH 15 -> 60", rth_bars(5), Site{"D"}, rth_aggregated, false, true, {}); + condition("aggregated: 120 on RTH 15 -> 60", rth_bars(5), Site{"120"}, rth_aggregated, false, + true, {}); + + // the bar magnifier (:1545): conservative on the same feeds. + Shape magnified = aggregated; + magnified.magnifier = true; + condition("magnifier: 240 on 15 -> 60", grid_bars(128), Site{"240"}, magnified, false, true, {}); + Shape magnified_chart; + magnified_chart.magnifier = true; + condition("magnifier: 60 on 15 -> 15", grid_bars(64), Site{"60"}, magnified_chart, false, true, {}); + Shape rth_magnified = rth_aggregated; + rth_magnified.magnifier = true; + condition("magnifier: D on RTH 15 -> 60", rth_bars(5), Site{"D"}, rth_magnified, false, true, {}); + condition("magnifier: 120 on RTH 15 -> 60", rth_bars(5), Site{"120"}, rth_magnified, false, + true, {}); + + // KI-55 range-start cut (:1546): a range start at bar 6 (01:30Z) cuts the + // hour in progress; the kernel, which has no cut, reads it from bar 3. + const std::vector hourly = grid_bars(64); + Shape range_start; + range_start.range_start_ms = static_cast(hourly[6].timestamp); + condition("KI-55 range start at bar 6", hourly, Site{"60"}, range_start, false, true, {{3, 10}}); + // ... and a cut at the first bar cuts nothing, route included. + Shape range_start_first; + range_start_first.range_start_ms = static_cast(hourly[0].timestamp); + condition("KI-55 range start at bar 0", hourly, Site{"60"}, range_start_first, false, true, {}); + + // historical lookahead projection (:1546): a lookahead_off site is the + // kernel's plain series; a lookahead_on site is the kernel's lookahead + // rule except on the run's incomplete tail hour (bars 64, 65), which the + // projection shows progressively and the kernel never completes. + Shape projection; + projection.lookahead_projection = true; + condition("historical projection, lookahead_off", grid_bars(66), Site{"60"}, projection, false, + true, {}); + condition("historical projection, lookahead_on", grid_bars(66), Site{"60", true}, projection, + false, true, {{64, 65}}); +} + +// ---- 3. per-site conditions ------------------------------------------------- + +void test_site_conditions() { + // lookahead_on (:1572) without the projection: the evaluator reads the + // hour in progress on every bar that completes none (index % 4 != 3), the + // kernel's lookahead rule the completed hour. + Ranges progressive; + for (int k = 0; k < 16; ++k) progressive.push_back({4 * k, 4 * k + 2}); + progressive.push_back({64, 65}); + condition("lookahead_on", grid_bars(66), Site{"60", true}, Shape{}, false, true, progressive); + // ticker.heikinashi and request.security_lower_tf: no subscription spells them. + condition("heikinashi", grid_bars(64), Site{"60", false, false, true}, Shape{}, false, false, {}); + condition("security_lower_tf", grid_bars(64), Site{"1", false, false, false, true}, Shape{}, + false, false, {}); + // OTC daily pins (:1578): a forex intraday run that starts mid-day has no + // partial first D bar; the kernel delivers it at bar 71 and holds it until + // the first full day completes at bar 167. From midnight nothing differs. + Shape forex; + forex.type = "forex"; + condition("forex D from 06:00Z", grid_bars(480, 24), Site{"D"}, forex, false, true, {{71, 166}}); + condition("forex D from 00:00Z", grid_bars(480), Site{"D"}, forex, false, true, {}); + condition("crypto D from 06:00Z (no pins: the kernel's)", grid_bars(480, 24), Site{"D"}, Shape{}, + true, true, {}); + // A sec_id that is not the site's index (:1567): structural only. + Site second; + second.tf = "60"; + second.sec_id = 1; + condition("sec_id 1 without sec_id 0", grid_bars(64), second, Shape{}, false, true, {}); +} + +// ---- 4. the kernel's own refusal (:1584) -------------------------------------- + +void test_kernel_refusal() { + scenario = "kernel refuses a non-divisible series"; + GeneratedShape pine; + pine.site = Site{"25"}; + const std::vector bars = grid_bars(64); + prime(pine, Shape{}); + run_pine(pine, bars, Shape{}); + CHECK(!pine.kernel_routed()); + CHECK(!pine.series_seen); + KernelSeries kernel; + const auto applied = kernel.configure_native(kernel_spec(bars, Site{"25"}, Shape{})); + CHECK(applied.status != NativeSetupStatus::Applied); +} + +// ---- 5. route-only conditions ------------------------------------------------- + +void test_route_only_conditions() { + // A stream (:1545): the kernel takes no tick while a series is declared. + scenario = "stream"; + { + GeneratedShape pine; + pine.site = Site{"60"}; + prime(pine, Shape{}); + const std::vector bars = grid_bars(32); + CHECK(pine.stream_begin(bars.data(), 19, "15", "15")); + TradeTick tick{}; + tick.timestamp = bars[19].timestamp; + tick.sequence = 1; + tick.price = bars[19].close; + tick.quantity = 1.0; + CHECK(pine.stream_push_tick(tick)); + CHECK(pine.stream_end(true)); + CHECK(pine.last_error().empty()); + CHECK(!pine.kernel_routed()); + CHECK(!pine.series_seen); + } + // The auxiliary finer feed (:1549). + scenario = "auxiliary feed"; + { + GeneratedShape pine; + pine.site = Site{"60"}; + prime(pine, Shape{}); + const std::vector chart = grid_bars(64); + const std::vector finer = grid_bars(192, 0, 5 * kMinute); + CHECK(pine.set_aux_security_feed(finer.data(), static_cast(finer.size()), "5")); + run_pine(pine, chart, Shape{}); + CHECK(!pine.kernel_routed()); + CHECK(!pine.series_seen); + } +} + +} // namespace + +int main() { + test_controls(); + test_run_shape_conditions(); + test_site_conditions(); + test_kernel_refusal(); + test_route_only_conditions(); + std::printf("adapter security route conditions: %d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} diff --git a/tests/test_aggregated_entry_bar_index_tape.cpp b/tests/test_aggregated_entry_bar_index_tape.cpp new file mode 100644 index 00000000..e9375a85 --- /dev/null +++ b/tests/test_aggregated_entry_bar_index_tape.cpp @@ -0,0 +1,431 @@ +/* + * test_aggregated_entry_bar_index_tape.cpp — R5 lane H-MEASURE (AUDIT4 X14, F1(e)). + * + * The parity-visible half of lane F1 / K-IDX: a process_orders_on_close script + * that READS the bar indices of its trades, replayed on an aggregated chart + * against TradingView's own tape. + * + * AUDIT4 §3.2 (F1 e): the aggregated-index regressions were invisible to + * parity because no aggregated corpus probe reads a bar index and none runs + * process_orders_on_close (the only two aggregated probes, + * ltf-bool-array-bull-majority-01 and ltf-numeric-float-ratio15-01, read + * neither). tests/test_aggregated_path_regressions.cpp pins the pre-R4 engine + * ab9714be; this row pins TradingView. + * + * The tape (tests/fixtures/aggregated_entry_bar_index/hm-f1e-ebi-pooc-eth, + * `lab tv`, ws-report-v1, rangeProof covered, BINANCE:ETHUSDT.P 15m, + * process_orders_on_close=true) enters at three fixed bar opens and closes + * every position on a bar-index difference: + * + * A long 01:00 UTC, closed once bar_index - strategy.opentrades.entry_bar_index(0) >= 1 + * B short 05:00 UTC, closed once held >= 3 + * C long 09:00 UTC, closed once held >= (last closed exit_bar_index - entry_bar_index) + 2 + * D short entered when bar_index - strategy.closedtrades.exit_bar_index(2) == 4, + * closed once held >= 2 + * + * and a three-hour "timeout" guard that TradingView never reaches. Each exit's + * comment is "held", the count TradingView's script computed, and the + * tape's "Duration (bars)" is exit bar - entry bar: 1, 3, 5, 2. + * + * The same chart reaches the script three ways: the 15m bars fed as they are + * (the control, input == script timeframe), and the corpus 1m bars they + * aggregate, plain and under the bar magnifier. The chart and plain aggregated + * paths answer the tape above field for field, excursions included; the + * magnified path answers hm-f1e-ebi-pooc-eth-mag, the same script exported + * with use_bar_magnifier=true, on every field but the excursions (TradingView's + * magnifier moves trade 2's favorable excursion 0.40 -> 1.86, which the + * engine's excursion model does not reproduce on any path; not this row's + * subject: tests/test_e19_excursion_tape.cpp measures that model). Since K-IDX (ADR-0001 rule 2, + * Option A) the kernel books lots, trade rows and NativeCoordinate:: + * interval_index in SCRIPT-bar space, so all three read chart bars. With the + * kernel's pre-K-IDX input-slot booking (and no adapter re-stamp) the + * aggregated paths read held = chart bar - input slot < 0: no held exit ever + * fires, the guard closes A, B and C at their timeouts and D never enters. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; + +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +#ifndef PINEFORGE_HM_F1E_FIXTURE_DIR +#error "PINEFORGE_HM_F1E_FIXTURE_DIR must name tests/fixtures/aggregated_entry_bar_index" +#endif + +namespace { + +struct FeedBar { + std::int64_t ts; + double open, high, low, close, volume; +}; + +#include "fixtures/aggregated_entry_bar_index/bars.inc" + +constexpr std::int64_t kMinute = 60'000; +constexpr std::int64_t kHour = 60 * kMinute; +constexpr const char* kSlug = "hm-f1e-ebi-pooc-eth"; // use_bar_magnifier=false +constexpr const char* kSlugMag = "hm-f1e-ebi-pooc-eth-mag"; // use_bar_magnifier=true + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +std::int64_t utc_ms(int y, int mo, int d, int h, int mi) { + const std::int64_t days = days_from_civil(y, static_cast(mo), + static_cast(d)); + return ((days * 24 + h) * 60 + mi) * kMinute; +} + +// "YYYY-MM-DD HH:MM" in the tape's UTC+8 -> UTC milliseconds. +std::int64_t tape_ms(const std::string& text) { + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + if (std::sscanf(text.c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) return -1; + return utc_ms(y, mo, d, h, mi) - 8 * kHour; +} + +// ── the tape ────────────────────────────────────────────────────────── + +struct TapeTrade { + bool is_long = true; + std::int64_t entry_ms = 0; + std::int64_t exit_ms = 0; + double entry_price = 0.0; + double exit_price = 0.0; + double qty = 0.0; + double net_pnl = 0.0; + double favorable = 0.0; + double adverse = 0.0; + std::string entry_signal; + std::string exit_signal; + int duration_bars = -1; +}; + +std::vector read_tape(const char* slug) { + std::ifstream in(std::string(PINEFORGE_HM_F1E_FIXTURE_DIR) + "/" + slug + "/tv_trades.csv"); + std::vector trades; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 17) continue; + const std::size_t n = static_cast(std::stoi(cell[0])); + if (trades.size() < n) trades.resize(n); + TapeTrade& t = trades[n - 1]; + const bool entry = cell[1].rfind("Entry", 0) == 0; + t.is_long = cell[1].find("long") != std::string::npos; + (entry ? t.entry_ms : t.exit_ms) = tape_ms(cell[2]); + (entry ? t.entry_signal : t.exit_signal) = cell[3]; + (entry ? t.entry_price : t.exit_price) = std::stod(cell[4]); + t.qty = std::stod(cell[5]); + t.net_pnl = std::stod(cell[7]); + t.favorable = std::stod(cell[10]); + t.adverse = std::stod(cell[12]); + t.duration_bars = std::stoi(cell[16]); + } + return trades; +} + +// ── the strategy, as the probe's Pine source runs it ───────────────── + +struct Held { + int bar = 0; + std::string id; + int held = 0; +}; + +class F1eHost final : public source::PineStrategyHost { +public: + F1eHost() { + set_syminfo_session("24x7"); + set_syminfo_timezone("UTC"); + source::PineStrategyConfig config; + config.initial_capital = 100000; + config.default_qty_type = static_cast(QtyType::FIXED); + config.default_qty_value = 1; + config.pyramiding = 1; + config.commission_type = static_cast(CommissionType::PERCENT); + config.commission_value = 0.0; + config.slippage = 0; + config.process_orders_on_close = true; + config.margin_long = 100; + config.margin_short = 100; + configure_pine_strategy(config); + syminfo_mintick_ = 0.01; + } + + void on_source_bar(const Bar&) override { + // int lastDur = na + // if strategy.closedtrades > 0 + // lastDur := strategy.closedtrades.exit_bar_index(n - 1) + // - strategy.closedtrades.entry_bar_index(n - 1) + const int closed = trade_count(); + const int last_duration = closed > 0 + ? closed_trade_exit_bar_index(closed - 1) - closed_trade_entry_bar_index(closed - 1) + : 0; + const std::int64_t t = current_bar_.timestamp; + if (position_entry_count_ > 0) { + // held = bar_index - strategy.opentrades.entry_bar_index(0) + const int held = bar_index_ - open_trade_entry_bar_index(0); + const std::string id = open_trade_entry_id(0); + const int need = id == "A" ? 1 : id == "B" ? 3 : id == "D" ? 2 : last_duration + 2; + held_.push_back({bar_index_, id, held}); + if (held >= need) { + strategy_close(id, "held" + std::to_string(held)); + } else if (t - open_trade_entry_time(0) >= 3 * kHour) { + strategy_close(id, "timeout"); + } + } else if (t == utc_ms(2025, 6, 12, 1, 0)) { + strategy_entry("A", true); + } else if (t == utc_ms(2025, 6, 12, 5, 0)) { + strategy_entry("B", false); + } else if (t == utc_ms(2025, 6, 12, 9, 0)) { + strategy_entry("C", true); + } else if (closed == 3 && bar_index_ - closed_trade_exit_bar_index(2) == 4) { + strategy_entry("D", false); + } + } + + // One booked row, as Pine's strategy.closedtrades.* accessors read it. + struct Row { + bool is_long = true; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = 0.0, exit_price = 0.0, qty = 0.0; + double profit = 0.0, runup = 0.0, drawdown = 0.0; + int entry_bar = 0, exit_bar = 0; + std::string entry_id, exit_comment; + }; + struct Run { + std::vector closed; + int open_count = 0; + std::string open_id; + std::int64_t open_entry_ms = 0; + int open_entry_bar = 0; + std::string error; + }; + Run read() const { + Run run; + run.error = last_error(); + for (int i = 0; i < trade_count(); ++i) { + Row row; + row.is_long = trades_[static_cast(i)].is_long; + row.entry_ms = closed_trade_entry_time(i); + row.exit_ms = closed_trade_exit_time(i); + row.entry_price = closed_trade_entry_price(i); + row.exit_price = closed_trade_exit_price(i); + row.qty = closed_trade_size(i); + row.profit = closed_trade_profit(i); + row.runup = closed_trade_max_runup(i); + row.drawdown = closed_trade_max_drawdown(i); + row.entry_bar = closed_trade_entry_bar_index(i); + row.exit_bar = closed_trade_exit_bar_index(i); + row.entry_id = closed_trade_entry_id(i); + row.exit_comment = closed_trade_exit_comment(i); + run.closed.push_back(row); + } + run.open_count = position_entry_count_; + if (run.open_count > 0) { + run.open_id = open_trade_entry_id(0); + run.open_entry_ms = open_trade_entry_time(0); + run.open_entry_bar = open_trade_entry_bar_index(0); + } + return run; + } + + std::vector held_; +}; + +enum class Path { Chart, Aggregated, Magnified }; +const Path kPaths[] = {Path::Chart, Path::Aggregated, Path::Magnified}; + +const char* path_name(Path path) { + switch (path) { + case Path::Chart: return "chart 15 -> 15 (control)"; + case Path::Aggregated: return "aggregated 1 -> 15"; + case Path::Magnified: return "aggregated 1 -> 15, magnifier"; + } + return "?"; +} + +template +std::vector feed(const FeedBar (&rows)[N]) { + std::vector bars; + for (const FeedBar& row : rows) { + Bar b{}; + b.timestamp = row.ts; + b.open = row.open; b.high = row.high; b.low = row.low; b.close = row.close; + b.volume = row.volume; + bars.push_back(b); + } + return bars; +} + +void run_path(F1eHost& host, Path path) { + static const std::vector fifteen = feed(kEth15); + static const std::vector one = feed(kEth1m); + if (path == Path::Chart) { + host.run(fifteen.data(), static_cast(fifteen.size()), "15", "15", false); + } else { + host.run(one.data(), static_cast(one.size()), "1", "15", path == Path::Magnified); + } +} + +// The chart bar a UTC bar open is, counted from the fixture's first 15m bar +// (the fixture has no hole, so bar k opens k * 15 minutes after it). +int chart_bar_of(std::int64_t ms) { + return static_cast((ms - kEth15[0].ts) / (15 * kMinute)); +} + +std::string hhmm(std::int64_t ms) { + const std::int64_t minutes = ms / kMinute; + char text[16]; + std::snprintf(text, sizeof(text), "%02lld:%02lld", + static_cast(minutes % 1440 / 60), + static_cast(minutes % 60)); + return text; +} + +// TradingView prints money to the cent. +bool cents(double engine, double tape) { return std::fabs(engine - tape) < 0.005 + 1e-9; } +bool same_price(double a, double b) { return std::fabs(a - b) <= 1e-9; } + +void show(const char* tag, const F1eHost::Run& run) { + std::printf(" %s: closed=%zu open=%d err='%s'\n", tag, run.closed.size(), + run.open_count, run.error.c_str()); + for (std::size_t i = 0; i < run.closed.size(); ++i) { + const F1eHost::Row& r = run.closed[i]; + std::printf(" [%zu] %s %s in %s @%.2f bar %d out %s @%.2f bar %d '%s' dur %d\n", i, + r.entry_id.c_str(), r.is_long ? "L" : "S", hhmm(r.entry_ms).c_str(), + r.entry_price, r.entry_bar, hhmm(r.exit_ms).c_str(), r.exit_price, + r.exit_bar, r.exit_comment.c_str(), r.exit_bar - r.entry_bar); + } + if (run.open_count > 0) { + std::printf(" open %s in %s bar %d\n", run.open_id.c_str(), + hhmm(run.open_entry_ms).c_str(), run.open_entry_bar); + } +} + +// ── 1. every tape row, on every path ────────────────────────────────── + +void test_tape_rows() { + const std::vector plain = read_tape(kSlug); + const std::vector magnified = read_tape(kSlugMag); + std::printf("-- %s: %zu TradingView trades; %s: %zu --\n", kSlug, plain.size(), kSlugMag, + magnified.size()); + CHECK(plain.size() == 4); + CHECK(magnified.size() == 4); + if (plain.size() != 4 || magnified.size() != 4) return; + for (const Path path : kPaths) { + const bool mag = path == Path::Magnified; + const std::vector& tape = mag ? magnified : plain; + std::printf("test_tape_rows [%s] vs %s\n", path_name(path), mag ? kSlugMag : kSlug); + F1eHost host; + run_path(host, path); + const F1eHost::Run run = host.read(); + CHECK(run.error.empty()); + CHECK(run.closed.size() == tape.size()); + CHECK(run.open_count == 0); + bool path_ok = run.error.empty() && run.closed.size() == tape.size() + && run.open_count == 0; + const std::size_t rows = std::min(run.closed.size(), tape.size()); + for (std::size_t i = 0; i < rows; ++i) { + const TapeTrade& tv = tape[i]; + const F1eHost::Row& r = run.closed[i]; + const bool ok = r.is_long == tv.is_long + && r.entry_ms == tv.entry_ms && r.exit_ms == tv.exit_ms + && same_price(r.entry_price, tv.entry_price) + && same_price(r.exit_price, tv.exit_price) + && r.qty == tv.qty + && r.entry_id == tv.entry_signal && r.exit_comment == tv.exit_signal + && cents(r.profit, tv.net_pnl) + && (mag || (cents(r.runup, tv.favorable) && cents(-r.drawdown, tv.adverse))) + // "Duration (bars)": the row counts chart bars (design AG1) ... + && r.exit_bar - r.entry_bar == tv.duration_bars + // ... and they are the chart bars the tape's instants open. + && r.entry_bar == chart_bar_of(tv.entry_ms) + && r.exit_bar == chart_bar_of(tv.exit_ms); + CHECK(ok); + if (!ok) { + path_ok = false; + std::printf(" row %zu: TradingView %s %s %s @%.2f bar %d -> %s @%.2f bar %d" + " '%s' dur %d pnl %.2f fe %.2f ae %.2f\n" + " engine %s %s %s @%.2f bar %d -> %s @%.2f bar %d" + " '%s' dur %d pnl %.4f fe %.4f ae %.4f\n", + i + 1, tv.entry_signal.c_str(), tv.is_long ? "L" : "S", + hhmm(tv.entry_ms).c_str(), tv.entry_price, chart_bar_of(tv.entry_ms), + hhmm(tv.exit_ms).c_str(), tv.exit_price, chart_bar_of(tv.exit_ms), + tv.exit_signal.c_str(), tv.duration_bars, tv.net_pnl, tv.favorable, + tv.adverse, r.entry_id.c_str(), r.is_long ? "L" : "S", + hhmm(r.entry_ms).c_str(), r.entry_price, r.entry_bar, + hhmm(r.exit_ms).c_str(), r.exit_price, r.exit_bar, + r.exit_comment.c_str(), r.exit_bar - r.entry_bar, r.profit, r.runup, + -r.drawdown); + } + } + if (!path_ok) show(path_name(path), run); + } +} + +// ── 2. what the script read: held = bar_index - entry_bar_index(0) ──── + +// Every callback with a position open reads 1, 2, ... chart bars, the same +// sequence on every path: A 1; B 1..3; C 1..5; D 1..2. +void test_held_sequence() { + const std::vector expected = {1, 1, 2, 3, 1, 2, 3, 4, 5, 1, 2}; + for (const Path path : kPaths) { + std::printf("test_held_sequence [%s]\n", path_name(path)); + F1eHost host; + run_path(host, path); + std::vector held; + for (const Held& h : host.held_) held.push_back(h.held); + CHECK(held == expected); + if (held != expected) { + std::printf(" held:"); + for (std::size_t i = 0; i < host.held_.size() && i < 24; ++i) + std::printf(" %s@%d=%d", host.held_[i].id.c_str(), host.held_[i].bar, + host.held_[i].held); + std::printf("%s\n", host.held_.size() > 24 ? " ..." : ""); + } + } +} + +} // namespace + +int main() { + test_tape_rows(); + test_held_sequence(); + std::printf("\n%s aggregated entry_bar_index tape: %d checks, %d failures\n", + tests_failed == 0 ? "PASS" : "FAIL", tests_passed + tests_failed, tests_failed); + return tests_failed == 0 ? 0 : 1; +} diff --git a/tests/test_magnified_aggregated_tape.cpp b/tests/test_magnified_aggregated_tape.cpp new file mode 100644 index 00000000..6aeaaf3d --- /dev/null +++ b/tests/test_magnified_aggregated_tape.cpp @@ -0,0 +1,311 @@ +/* + * test_magnified_aggregated_tape.cpp — R5 lane H-MEASURE (AUDIT4 X14, "F1's magnified residual"). + * + * Lane F1 recorded two open items for a 1m feed aggregated to a 15m chart + * under the bar magnifier: three entry-bar tests compared the chart bar with + * the kernel's input slot, and lane F1's differential script booked 12 rows + * where the pre-R4 engine ab9714be booked 9. TradingView settles the second, + * and replays the first's inputs: + * + * - Lane F1's differential probe (its eight variants v0..v7) as Pine, + * tests/fixtures/magnified_aggregated/hm-{mag,chart}-diff-v (`lab tv`, + * ws-report-v1, rangeProof covered), NYSE:F 15m over the three days of + * tests/fixtures/session_islastbar, use_bar_magnifier=true (hm-mag-*) and false + * (hm-chart-*). TradingView books 12 rows (v0, v2) and 14 (v4), as this engine + * does on every path: ab9714be's 9 was its own divergence. + * - The rows are asserted at chart-bar granularity: TradingView dates a magnified + * fill at its chart bar's open, where the adapter keeps a magnified sub-bar fill's + * own instant (design AG2); that presentation difference is a separate finding. + * Excursions are not asserted (the E19 excursion model differs on every path, + * the chart timeframe's included). + * - Each row's bar indices are the chart bars its instants open, and its "Duration + * (bars)" is exit bar - entry bar, on the magnified path as on the chart path: + * since K-IDX the kernel books lots and rows in script-bar space. + * + * Variants kept here are the ones the magnified run books exactly as TradingView: + * v0 (plain), v2 (calc_on_order_fills), v4 (slippage 2, pyramiding 3); controls: + * the chart and plain-aggregated paths against hm-chart-diff-v0 / v4. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; + +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +#ifndef PINEFORGE_HM_MAG_FIXTURE_DIR +#error "PINEFORGE_HM_MAG_FIXTURE_DIR must name tests/fixtures/magnified_aggregated" +#endif + +namespace { + +struct FeedBar { + std::int64_t ts; + double open, high, low, close, volume; +}; + +#include "fixtures/session_islastbar/bars.inc" + +constexpr std::int64_t kMinute = 60'000; +constexpr std::int64_t kBar = 15 * kMinute; +const double kNaN = std::numeric_limits::quiet_NaN(); + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +// "YYYY-MM-DD HH:MM" in the tape's UTC+8 -> UTC milliseconds. +std::int64_t tape_ms(const std::string& text) { + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + if (std::sscanf(text.c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) return -1; + const std::int64_t days = days_from_civil(y, static_cast(mo), + static_cast(d)); + return ((days * 24 + h - 8) * 60 + mi) * kMinute; +} + +// 2025-07-07 16:00 ET: no order is placed after the replayed days; the tape +// closes whatever is open on the next session's first bar ("after-window"). +constexpr std::int64_t kWindowEnd = 1751918400000LL; + +struct TapeTrade { + bool is_long = true; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = 0.0, exit_price = 0.0, qty = 0.0, net_pnl = 0.0; + std::string entry_signal, exit_signal; + int duration_bars = -1; +}; + +std::vector read_tape(const std::string& slug) { + std::ifstream in(std::string(PINEFORGE_HM_MAG_FIXTURE_DIR) + "/" + slug + "/tv_trades.csv"); + std::vector trades; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 17) continue; + const std::size_t n = static_cast(std::stoi(cell[0])); + if (trades.size() < n) trades.resize(n); + TapeTrade& t = trades[n - 1]; + const bool entry = cell[1].rfind("Entry", 0) == 0; + t.is_long = cell[1].find("long") != std::string::npos; + (entry ? t.entry_ms : t.exit_ms) = tape_ms(cell[2]); + (entry ? t.entry_signal : t.exit_signal) = cell[3]; + (entry ? t.entry_price : t.exit_price) = std::stod(cell[4]); + t.qty = std::stod(cell[5]); + t.net_pnl = std::stod(cell[7]); + t.duration_bars = std::stoi(cell[16]); + } + return trades; +} + +struct Variant { const char* tag; bool pooc; bool coof; int slippage; int pyramiding; }; + +// The Pine port's body, as generated code reads it: units = strategy.position_size. +class MagDiffHost final : public source::PineStrategyHost { +public: + explicit MagDiffHost(const Variant& v) { + set_syminfo_session("0930-1600"); + set_syminfo_timezone("America/New_York"); + source::PineStrategyConfig c; + c.initial_capital = 100000; + c.default_qty_type = static_cast(QtyType::FIXED); + c.default_qty_value = 100; + c.pyramiding = v.pyramiding; + c.process_orders_on_close = v.pooc; + c.calc_on_order_fills = v.coof; + c.slippage = v.slippage; + c.commission_type = static_cast(CommissionType::PERCENT); + c.commission_value = 0.01; + c.margin_long = 100; + c.margin_short = 100; + configure_pine_strategy(c); + syminfo_mintick_ = 0.01; + } + void on_source_bar(const Bar& b) override { + const int k = bar_index_; // the replay's first bar is the Pine counter's origin + const double units = signed_position_size(); + if (k % 7 == 3 && units == 0.0) { + strategy_entry("PL", true, kNaN, b.high - 0.02); + strategy_exit("PX", "PL", b.close + 0.06, b.close - 0.06); + } + if (k % 9 == 4 && units == 0.0) { + strategy_entry("PS", false, b.close + 0.02); + strategy_exit("SX", "PS", b.close - 0.05, b.close + 0.07); + } + if (k % 13 == 6 && units == 0.0) { + strategy_entry("ML", true); + strategy_exit("MX", "ML", b.close + 0.08, b.close - 0.08); + } + if (k % 13 == 7 && units > 0.0) strategy_entry("ML", true); + if (k % 17 == 16 && units != 0.0) strategy_close_all(); + } + + struct Row { + bool is_long = true; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = 0.0, exit_price = 0.0, qty = 0.0, profit = 0.0; + int entry_bar = 0, exit_bar = 0; + std::string entry_id, exit_id; + }; + std::vector rows() const { + std::vector out; + for (int i = 0; i < trade_count(); ++i) { + Row r; + r.is_long = trades_[static_cast(i)].is_long; + r.entry_ms = closed_trade_entry_time(i); + r.exit_ms = closed_trade_exit_time(i); + r.entry_price = closed_trade_entry_price(i); + r.exit_price = closed_trade_exit_price(i); + r.qty = closed_trade_size(i); + r.profit = closed_trade_profit(i); + r.entry_bar = closed_trade_entry_bar_index(i); + r.exit_bar = closed_trade_exit_bar_index(i); + r.entry_id = closed_trade_entry_id(i); + r.exit_id = closed_trade_exit_id(i); + out.push_back(r); + } + return out; + } + double open_units() const { return physical_position().signed_units; } +}; + +enum class Path { Chart, Aggregated, Magnified }; + +const char* path_name(Path path) { + switch (path) { + case Path::Chart: return "chart 15 -> 15"; + case Path::Aggregated: return "aggregated 1 -> 15"; + case Path::Magnified: return "aggregated 1 -> 15, magnifier"; + } + return "?"; +} + +template +std::vector feed(const FeedBar (&rows)[N]) { + std::vector bars; + for (const FeedBar& row : rows) { + Bar b{}; + b.timestamp = row.ts; + b.open = row.open; b.high = row.high; b.low = row.low; b.close = row.close; + b.volume = row.volume; + bars.push_back(b); + } + return bars; +} + +// The chart bar (fixture order) whose open is `ms`; -1 when none is. +int chart_bar_of(std::int64_t ms) { + for (std::size_t i = 0; i < sizeof(kFord15) / sizeof(kFord15[0]); ++i) + if (kFord15[i].ts == ms) return static_cast(i); + return -1; +} + +std::int64_t bar_open(std::int64_t ms) { return ms - ms % kBar; } + +// TradingView's exit signal for strategy.close_all is "Close position order". +std::string exit_signal(const MagDiffHost::Row& r) { + return r.exit_id.rfind("__", 0) == 0 ? "Close position order" : r.exit_id; +} + +bool near(double a, double b, double tol) { return std::fabs(a - b) <= tol; } + +void replay(const char* slug, const Variant& v, Path path) { + static const std::vector fifteen = feed(kFord15); + static const std::vector one = feed(kFord1m); + std::printf("-- %s [%s, %s]\n", slug, v.tag, path_name(path)); + std::vector tape; + for (const TapeTrade& t : read_tape(slug)) + if (t.exit_ms < kWindowEnd) tape.push_back(t); + const bool after_window = read_tape(slug).size() != tape.size(); + MagDiffHost host(v); + if (path == Path::Chart) { + host.run(fifteen.data(), static_cast(fifteen.size()), "15", "15", false); + } else { + host.run(one.data(), static_cast(one.size()), "1", "15", path == Path::Magnified); + } + CHECK(host.last_error().empty()); + const auto rows = host.rows(); + CHECK(!tape.empty()); + CHECK(rows.size() == tape.size()); + // The tape closes nothing after the window, and the replay ends flat. + CHECK(!after_window); + CHECK(host.open_units() == 0.0); + const std::size_t n = std::min(rows.size(), tape.size()); + for (std::size_t i = 0; i < n; ++i) { + const TapeTrade& tv = tape[i]; + const MagDiffHost::Row& r = rows[i]; + const bool ok = r.is_long == tv.is_long + && bar_open(r.entry_ms) == tv.entry_ms && bar_open(r.exit_ms) == tv.exit_ms + && near(r.entry_price, tv.entry_price, 1e-9) + && near(r.exit_price, tv.exit_price, 1e-9) + && r.qty == tv.qty && near(r.profit, tv.net_pnl, 0.00006) + && r.entry_id == tv.entry_signal && exit_signal(r) == tv.exit_signal + && r.entry_bar == chart_bar_of(tv.entry_ms) + && r.exit_bar == chart_bar_of(tv.exit_ms) + && r.exit_bar - r.entry_bar == tv.duration_bars; + CHECK(ok); + if (!ok) { + std::printf(" row %zu: TradingView %s %s bar %d @%.2f -> bar %d @%.2f %s dur %d pnl %.4f\n" + " engine %s %s bar %d @%.2f -> bar %d @%.2f %s dur %d pnl %.4f" + " (instants %lld -> %lld)\n", + i + 1, tv.entry_signal.c_str(), tv.is_long ? "L" : "S", + chart_bar_of(tv.entry_ms), tv.entry_price, chart_bar_of(tv.exit_ms), + tv.exit_price, tv.exit_signal.c_str(), tv.duration_bars, tv.net_pnl, + r.entry_id.c_str(), r.is_long ? "L" : "S", r.entry_bar, r.entry_price, + r.exit_bar, r.exit_price, exit_signal(r).c_str(), r.exit_bar - r.entry_bar, + r.profit, static_cast(r.entry_ms), + static_cast(r.exit_ms)); + } + } +} + +} // namespace + +int main() { + const Variant v0{"v0 plain", false, false, 0, 1}; + const Variant v2{"v2 calc_on_order_fills", false, true, 0, 1}; + const Variant v4{"v4 slippage 2, pyramiding 3", false, false, 2, 3}; + // 1. the magnified run against TradingView's magnifier tapes + replay("hm-mag-diff-v0", v0, Path::Magnified); + replay("hm-mag-diff-v2", v2, Path::Magnified); + replay("hm-mag-diff-v4", v4, Path::Magnified); + // 2. controls: the chart and plain-aggregated paths against the magnifier-off tapes + for (const Path path : {Path::Chart, Path::Aggregated}) { + replay("hm-chart-diff-v0", v0, path); + replay("hm-chart-diff-v4", v4, path); + } + std::printf("\n%s magnified aggregated tape: %d checks, %d failures\n", + tests_failed == 0 ? "PASS" : "FAIL", tests_passed + tests_failed, tests_failed); + return tests_failed == 0 ? 0 : 1; +} From e0510534e2b6b6e6d2ae67ea807985415e57b449 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:22:39 +0800 Subject: [PATCH 05/83] H-MEASURE: the G1 acid composite as a C++/C witness row AUDIT4-codex A4-ACID-COVERAGE. tests/test_native_acid_composite.cpp is one source-free strategy on NativeStrategyHost (kernel headers only) that exercises every feature of the G1 acid test and prints PASS per feature: F01 fee-reserved Sized entry, F02 anchored bracket legs on a price grid, F03 a keep_binding re-issue, F04 a trail in ticks, F05 a maintenance-only margin model and its liquidation seen through the open lots, F06 an FX step, F07 a risk limit and an opening refused with RiskLimit, F08 calc-on-fills, F09 an hourly subscription plus an auxiliary 5m->30m series that is actually delivered (44 buckets; the audit's copy wiped its own series with an empty begin-time declare and so read aux=0), F10 session-day flags, F11 a FIFO partial close across lots and an exact-sum close that leaves no dust lot (K-ULP3), F12 typed refusals, F13 magnifier counters, F14 the kernel-recorded report with per-bar hashes. The C half (tests/test_native_acid_composite_c.c, native_c_api.h only) configures through strategy_configure_native_ext_result_v1 on a handle it first refuses (GRID_REQUIRES_PRICE_TICK, stays UNCONFIGURED, then Ready, then WRONG_PHASE), runs the same commands in batch and stream, and prints the same FIELD lines: the C++ half compares the two sets in process, 915 of 915 identical, every C++-only family named by an EXCLUDED line (the four C-SURFACE-2 items ruled to 1.1.0, closes_session_day_open_ended, the protected broker fold, quote_origin_ordinal and driver_statistics). The batch/stream comparison is explicit: trades, fills, curve, series deliveries, check points and the broker half agree; the per-bar hash rows, the final hashes and the forming bar's closes_session_day are driving-mode keyed, each rule printed with its documented source. Findings printed, not asserted: under IntrabarPath::lower_tf the margin model is checked at the bar's first sample and after fills only, never per sample, contrary to native-engine.md's "re-evaluates at each delivered sample". Source-free: registered like test_native_c_api, so it runs in the kernel profile and in release. native-engine.md records the C port's measured exclusions beside the COVERAGE block. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 58e0f16275ea57148f3385d564df88ae7cc2e70c) --- docs/pages/native-engine.md | 26 +- tests/CMakeLists.txt | 21 + tests/native_acid_composite.h | 282 ++++ tests/test_native_acid_composite.cpp | 2082 ++++++++++++++++++++++++++ tests/test_native_acid_composite_c.c | 1270 ++++++++++++++++ 5 files changed, 3680 insertions(+), 1 deletion(-) create mode 100644 tests/native_acid_composite.h create mode 100644 tests/test_native_acid_composite.cpp create mode 100644 tests/test_native_acid_composite_c.c diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 1d3649a5..995413ee 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -4076,7 +4076,31 @@ notification; the same request is `strategy_native_submit_v1` with `strategy_native_sized_units_v1`. A table that leaves `on_bar_open` or `on_precommit` out declares that hook absent when the host is created (`declare_native_bar_open_hook` / `declare_native_precommit_hook`, R5 lane -D2-A), which moves no value. `scripts/check_native_c_api_surface.py` +D2-A), which moves no value. + +**The same strategy through both surfaces (R5 lane H-MEASURE).** +`tests/test_native_acid_composite.cpp` drives one Pine-free strategy through +every feature of the G1 acid test — a fee-reserved `Sized` entry, anchored +bracket legs on a price grid, a `keep_binding` re-issue, a trail in ticks, a +maintenance-only margin model and its liquidation seen through the open lots, +an FX step, a risk limit and its refusal, calc-on-fills, an hourly and an +auxiliary subscription, session-day flags, a partial close across lots and an +exact-sum close, a typed refusal, the magnifier counters, the kernel-recorded +report — in batch and in stream, and runs its C port +(`tests/test_native_acid_composite_c.c`, configured through the typed +`strategy_configure_native_ext_result_v1`) in the same process: every shared +ledger fact is identical, per-bar broker-state hash rows included, within one +driving mode (a batch and a stream record the same trades, fills, equity +curve, series deliveries and check points, and different hash rows, as the +driving-mode rules above say). What the C port cannot read is named family by +family: the four items ruled to 1.1.0 — a non-mutating execution preview +(`inspect_current_execution`), the applied event's origin and label, a closed +trade's entry comment, and a C `ReplaceOptions` word (`keep_handle` / +`keep_binding` / `retain_trigger_state`) — plus `closes_session_day_open_ended` +(the asymmetry the COVERAGE block records), the protected +`broker_state_hash_from_execution_hash` fold, and two C++ readouts with no C +field: `NativeCurrentPointView::quote_origin_ordinal` and +`NativeDecisionContext::driver_statistics`. `scripts/check_native_c_api_surface.py` proves the block is exactly that class's public surface and runs as a source guard in every `ci_verify.py` profile, so the list cannot silently go stale; `scripts/test_check_native_c_api_surface.py` proves the guard can fail. A diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f77ce540..bfe0989d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2244,6 +2244,27 @@ if(PINEFORGE_ENABLE_COVERAGE) endif() pineforge_add_twin_test(test_native_c_api_c99 test_native_c_api_c99) +# R5 lane H-MEASURE (AUDIT4-codex A4-ACID-COVERAGE): the G1 composite witness. +# One ctest row, two translation units, the test_native_c_api pattern. The C++ +# half drives one Pine-free NativeStrategyHost through every feature the G1 +# acid test names (a PASS line per feature) in batch and in stream, runs the +# keep_binding variant and a lower_tf-magnified batch, and accounts for the +# driving-mode hash rules line by line. The C half drives the same commands +# through alone, and the C++ half compares the two +# FIELD sets in process: every shared ledger fact identical, every C++-only +# family named by an EXCLUDED line. Neither TU reaches a source or compat +# header, so the row runs in the kernel-only profile too. +add_executable(test_native_acid_composite + test_native_acid_composite.cpp + test_native_acid_composite_c.c) +target_compile_definitions(test_native_acid_composite PRIVATE ACID_WITH_C=1 ACID_C_NO_MAIN=1) +target_link_libraries(test_native_acid_composite PRIVATE pineforge) +target_compile_options(test_native_acid_composite PRIVATE -UNDEBUG -ffp-contract=off) +if(PINEFORGE_ENABLE_COVERAGE) + target_compile_options(test_native_acid_composite PRIVATE ${_pf_cov_test_flags}) +endif() +pineforge_add_twin_test(test_native_acid_composite test_native_acid_composite) + add_test(NAME test_market_admission_schema COMMAND ${Python3_EXECUTABLE} ${PROJECT_SOURCE_DIR}/scripts/check_market_admission_schema.py) add_test(NAME test_market_admission_schema_mutations COMMAND ${Python3_EXECUTABLE} diff --git a/tests/native_acid_composite.h b/tests/native_acid_composite.h new file mode 100644 index 00000000..5ba14e76 --- /dev/null +++ b/tests/native_acid_composite.h @@ -0,0 +1,282 @@ +/* + * native_acid_composite.h -- what the two halves of the acid composite share. + * + * Lane H-MEASURE, item A4-ACID-COVERAGE. tests/test_native_acid_composite.cpp drives the + * scenario through NativeStrategyHost, tests/test_native_acid_composite_c.c drives the very same + * commands through ; both print one `FIELD key=value` + * line per shared ledger fact and the two sets must be identical, run by run. + * This header is plain C (C99/C11 and C++17 alike) and includes only the two + * public C headers, so the C half really is compiled by a C compiler. + * + * What lives here, so neither half can drift from the other: + * - the tape: 88 fifteen-minute input bars (Mon 5 .. Thu 8 Oct 2026, UTC, + * session 0900-1500 Mon-Fri; Thursday stops at 12:45) and the 264 + * five-minute auxiliary bars that aggregate exactly to them; + * - the run specification's numbers and the FX curve; + * - the scenario's bar indices (every command is issued at one of them); + * - the event projection: pf_native_event_v1 is what C reads back, and the + * C++ half flattens native_events() into the same POD (the translation + * src/native_c_host.cpp:1918-2033 performs) before the same digest. + */ +#ifndef PINEFORGE_TEST_NATIVE_ACID_COMPOSITE_H +#define PINEFORGE_TEST_NATIVE_ACID_COMPOSITE_H + +#include +#include + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── Calendar ──────────────────────────────────────────────────────── */ +#define ACID_DAYS 4 +#define ACID_SLOTS 24 /* 09:00 .. 14:45 */ +#define ACID_LAST_DAY_SLOTS 16 /* Thursday stops at 12:45: mid-session */ +#define ACID_BARS (3 * ACID_SLOTS + ACID_LAST_DAY_SLOTS) /* 88 */ +#define ACID_FEED (3 * ACID_BARS) /* 264 */ +#define ACID_WARMUP ACID_SLOTS /* a stream warms up on Monday */ +#define ACID_DAY0_ORDINAL 20731 /* days_from_civil(2026, 10, 5), a Monday */ +#define ACID_MINUTE_MS 60000LL + +/* Monday 5 Oct 2026 00:00 UTC. */ +#define ACID_DAY0_MS ((int64_t)ACID_DAY0_ORDINAL * 1440LL * ACID_MINUTE_MS) + +static inline int64_t acid_day_ms(int day) { return ACID_DAY0_MS + (int64_t)day * 1440LL * ACID_MINUTE_MS; } +static inline int acid_day_of(int gi) { return gi / ACID_SLOTS; } +static inline int acid_slot_of(int gi) { return gi % ACID_SLOTS; } +static inline int64_t acid_bar_ms(int gi) { + return acid_day_ms(acid_day_of(gi)) + (540LL + 15LL * acid_slot_of(gi)) * ACID_MINUTE_MS; +} + +/* ── The scenario: every command is issued at one of these bars ───────── */ +enum { + /* Monday: Sized entry + anchored bracket, take-profit, re-entry, re-issued exit. */ + G_E1 = 0, G_E1FILL = 1, G_TP = 5, G_E2FILL = 6, G_X_FIRST = 7, G_X_LAST = 14, G_XFILL = 15, + /* Tuesday: entry + trail in ticks. */ + G_E3 = 24, G_E3FILL = 25, G_T3X = 28, + /* Wednesday: two short lots, the FX step, the kernel liquidation, the risk block. */ + G_E4A = 48, G_E4B = 49, G_E4BFILL = 50, G_PRESTEP = 54, G_STEP = 56, G_PRESPIKE = 59, + G_SPIKE = 60, G_E5REJ = 61, G_XFFILL = 62, + /* Thursday: four lots, a FIFO partial close across lots, an exact-sum close. */ + G_D4 = 72, G_D4FILL = 73, G_P1 = 74, G_P1FILL = 75, G_P2 = 76, G_P2FILL = 77, + G_LAST = ACID_BARS - 1 +}; + +/* ── The run specification's numbers (both halves spell the same spec) ── */ +#define ACID_CAPITAL 100000.0 +#define ACID_POINT_VALUE 5.0 +#define ACID_ACCOUNT_FX 1.20 +#define ACID_TICK 0.25 +#define ACID_SLIPPAGE 1u +#define ACID_FEE_PCT 0.1 +#define ACID_QTY_GRID 0.01 +#define ACID_INITIAL_LONG 0.20 +#define ACID_MAINT_SHORT 0.25 +#define ACID_RISK_LOSS 2500.0 +#define ACID_MAX_RECALCS 2u +#define ACID_LIQ_LABEL "ACID-desk" +#define ACID_LIQ_COMMENT "maintenance 25%" +/* The FX curve (EUR -> USD): a point that RESTATES the scalar at Wed 10:00, + * which is no step, and one real step at Wed 11:00. */ +#define ACID_FX_T0 (acid_day_ms(2) + 600LL * ACID_MINUTE_MS) +#define ACID_FX_T1 (acid_day_ms(2) + 660LL * ACID_MINUTE_MS) +#define ACID_FX_R0 1.20 +#define ACID_FX_R1 1.25 + +/* Scenario quantities. */ +#define ACID_E1_CASH 24000.0 +#define ACID_TP_OFFSET 3.1 /* price units, off the 0.25 ladder on purpose */ +#define ACID_SL_OFFSET (-2.3) +#define ACID_E2_UNITS 10.0 +#define ACID_X_LEVEL0 201.00 +#define ACID_E3_UNITS 8.0 +#define ACID_T3_TICKS 6.0 +#define ACID_E4A_UNITS (-5.0) +#define ACID_E4B_UNITS (-285.0) +#define ACID_E5_UNITS (-10.0) +#define ACID_P1_UNITS 1.5 + +static const double acid_d4_units[4] = {1.1, 0.7, 0.2, 0.1}; + +/* ── The tape (prices in hundredths, so every aggregate is exact) ─────── */ +typedef struct acid_t4 { int o, h, l, c; } acid_t4; + +static inline acid_t4 acid_mk(int o, int h, int l, int c) { + acid_t4 t; + t.o = o; t.h = h; t.l = l; t.c = c; + return t; +} +static inline acid_t4 acid_quiet(int o, int c) { + return acid_mk(o, (o > c ? o : c) + 10, (o < c ? o : c) - 10, c); +} +static inline double acid_px(int hundredths) { return (double)hundredths / 100.0; } + +/* The 88 input bars as hundredths. Every scripted bar is written out; the + * quiet bars between them open at the previous close. */ +static inline void acid_tape_t4(acid_t4* t) { + static const acid_t4 mon[7] = { + {19990, 20015, 19980, 20002}, /* 09:00 E1 + bracket submitted */ + {20010, 20040, 20000, 20030}, /* 09:15 E1 fills at the open 200.10 */ + {20030, 20090, 20020, 20080}, + {20080, 20190, 20070, 20170}, + {20170, 20290, 20160, 20280}, + {20290, 20410, 20270, 20360}, /* 10:15 the take-profit at 203.75 */ + {20355, 20420, 20330, 20350} /* 10:30 E2 fills; X submitted */ + }; + static const int mon_x[8] = {20360, 20375, 20365, 20380, 20370, 20385, 20375, 20390}; + static const int mon_tail[8] = {20280, 20265, 20275, 20260, 20270, 20285, 20275, 20280}; + static const acid_t4 tue[5] = { + {20480, 20500, 20470, 20495}, /* 09:00 E3 + trail submitted */ + {20505, 20630, 20500, 20610}, /* 09:15 E3 fills at the open 205.05 */ + {20610, 20720, 20590, 20700}, + {20700, 20760, 20660, 20740}, + {20735, 20745, 20580, 20600} /* 10:00 the trail exits */ + }; + static const int tue_tail[8] = {20610, 20595, 20605, 20590, 20600, 20615, 20605, 20600}; + static const acid_t4 wed[3] = { + {20980, 21000, 20960, 20990}, /* 09:00 E4a submitted */ + {21010, 21030, 20990, 21000}, /* 09:15 E4a fills; E4b submitted */ + {20995, 21010, 20970, 20980} /* 09:30 E4b fills */ + }; + static const int wed_quiet[9] = {20990, 20975, 20985, 20970, 20980, 20995, 20985, 20990, 20980}; + static const acid_t4 wed_spike[3] = { + {21010, 21300, 20990, 21240}, /* 12:00 the spike: the kernel liquidation */ + {21230, 21250, 21200, 21220}, /* 12:15 E5 refused (RiskLimit); flatten submitted */ + {21220, 21240, 21190, 21210} /* 12:30 the flatten fills */ + }; + static const int wed_tail[9] = {21215, 21200, 21210, 21195, 21205, 21220, 21210, 21200, 21205}; + static const acid_t4 thu[6] = { + {21100, 21120, 21080, 21110}, /* 09:00 E6..E9 submitted */ + {21115, 21140, 21100, 21130}, /* 09:15 E6..E9 fill at the open */ + {21130, 21150, 21110, 21140}, /* 09:30 P1 submitted */ + {21145, 21160, 21120, 21150}, /* 09:45 P1 fills */ + {21150, 21170, 21140, 21160}, /* 10:00 P2 (the exact sum) submitted */ + {21165, 21180, 21150, 21170} /* 10:15 P2 fills */ + }; + static const int thu_tail[10] = {21200, 21230, 21260, 21290, 21320, 21350, 21380, 21410, + 21440, 21460}; + int gi = 0, i, prev; + for (i = 0; i < 7; ++i) t[gi++] = mon[i]; + prev = mon[6].c; + for (i = 0; i < 8; ++i) { t[gi] = acid_quiet(prev, mon_x[i]); prev = t[gi++].c; } + t[gi] = acid_mk(prev, prev + 10, 20250, 20270); prev = t[gi++].c; /* 12:45 X fills */ + for (i = 0; i < 8; ++i) { t[gi] = acid_quiet(prev, mon_tail[i]); prev = t[gi++].c; } + for (i = 0; i < 5; ++i) t[gi++] = tue[i]; + prev = tue[4].c; + for (i = 0; i < 19; ++i) { t[gi] = acid_quiet(prev, tue_tail[i % 8]); prev = t[gi++].c; } + for (i = 0; i < 3; ++i) t[gi++] = wed[i]; + prev = wed[2].c; + for (i = 0; i < 9; ++i) { t[gi] = acid_quiet(prev, wed_quiet[i]); prev = t[gi++].c; } + for (i = 0; i < 3; ++i) t[gi++] = wed_spike[i]; + prev = wed_spike[2].c; + for (i = 0; i < 9; ++i) { t[gi] = acid_quiet(prev, wed_tail[i]); prev = t[gi++].c; } + for (i = 0; i < 6; ++i) t[gi++] = thu[i]; + prev = thu[5].c; + for (i = 0; i < 10; ++i) { t[gi] = acid_quiet(prev, thu_tail[i]); prev = t[gi++].c; } +} + +/* Three five-minute bars whose aggregate is exactly the input bar, walking its + * extremes in the order the kernel's AUTO path walks them (the high first when + * |H - O| < |O - L|). */ +static inline void acid_split(acid_t4 b, acid_t4 f[3]) { + const int high_first = (b.h - b.o < 0 ? b.o - b.h : b.h - b.o) < + (b.o - b.l < 0 ? b.l - b.o : b.o - b.l); + int x1, x2; + if (high_first) { + x1 = (b.h + b.l) / 2; x2 = (b.l + b.c) / 2; + f[0] = acid_mk(b.o, b.h, b.o < x1 ? b.o : x1, x1); + f[1] = acid_mk(x1, x1 > x2 ? x1 : x2, b.l, x2); + } else { + x1 = (b.h + b.l) / 2; x2 = (b.h + b.c) / 2; + f[0] = acid_mk(b.o, b.o > x1 ? b.o : x1, b.l, x1); + f[1] = acid_mk(x1, b.h, x1 < x2 ? x1 : x2, x2); + } + f[2] = acid_mk(x2, x2 > b.c ? x2 : b.c, x2 < b.c ? x2 : b.c, b.c); +} + +static inline void acid_build_tape(pf_bar_t* b15, pf_bar_t* b5) { + acid_t4 t[ACID_BARS]; + int gi, k; + acid_tape_t4(t); + for (gi = 0; gi < ACID_BARS; ++gi) { + acid_t4 f[3]; + double vol = 0.0; + const int64_t t0 = acid_bar_ms(gi); + acid_split(t[gi], f); + for (k = 0; k < 3; ++k) { + pf_bar_t* s = &b5[3 * gi + k]; + const double v5 = (double)(100 + (gi * 7 + k * 13) % 50); + vol += v5; + s->open = acid_px(f[k].o); s->high = acid_px(f[k].h); + s->low = acid_px(f[k].l); s->close = acid_px(f[k].c); + s->volume = v5; + s->timestamp = t0 + (int64_t)k * 5LL * ACID_MINUTE_MS; + } + b15[gi].open = acid_px(t[gi].o); b15[gi].high = acid_px(t[gi].h); + b15[gi].low = acid_px(t[gi].l); b15[gi].close = acid_px(t[gi].c); + b15[gi].volume = vol; + b15[gi].timestamp = t0; + } +} + +/* ── Digest (FNV-1a, 64-bit) ────────────────────────────────────────── */ +#define ACID_FNV_OFFSET 14695981039346656037ull +static inline uint64_t acid_fnv_u64(uint64_t h, uint64_t v) { + int i; + for (i = 0; i < 8; ++i) { + h ^= (v >> (8 * i)) & 0xffu; + h *= 1099511628211ull; + } + return h; +} +static inline uint64_t acid_fnv_d(uint64_t h, double d) { + uint64_t u; + memcpy(&u, &d, sizeof u); + return acid_fnv_u64(h, u); +} +/* Every field of the flattened event, in declaration order. */ +static inline uint64_t acid_event_fold(uint64_t h, const pf_native_event_v1* e) { + h = acid_fnv_u64(h, e->kind); + h = acid_fnv_u64(h, e->reason); + h = acid_fnv_u64(h, e->ordinal); + h = acid_fnv_u64(h, e->incarnation); + h = acid_fnv_u64(h, e->successor); + h = acid_fnv_d(h, e->raw_price); + h = acid_fnv_d(h, e->resolved_price); + h = acid_fnv_d(h, e->price); + h = acid_fnv_d(h, e->closed_units); + h = acid_fnv_d(h, e->opened_units); + h = acid_fnv_u64(h, (uint64_t)e->cycle_before); + h = acid_fnv_u64(h, (uint64_t)e->cycle_after); + h = acid_fnv_u64(h, (uint64_t)e->effective_time_ms); + h = acid_fnv_u64(h, (uint64_t)(int64_t)e->interval_index); + h = acid_fnv_u64(h, e->provenance); + h = acid_fnv_u64(h, e->path_phase); + h = acid_fnv_u64(h, e->terminal); + return h; +} + +/* ── Formatting: %.17g round-trips every binary64 ─────────────────────── */ +static inline const char* acid_d(char* buf, size_t n, double v) { + if (v != v) snprintf(buf, n, "nan"); + else snprintf(buf, n, "%.17g", v); + return buf; +} + +/* ── The line sink the C half writes through ────────────────────────── */ +typedef void (*acid_sink_fn)(void* user, const char* line); + +/* The C half: runs the C port and hands every output line to `sink`. + * Answers the number of C-side check failures. */ +int acid_c_main(acid_sink_fn sink, void* user); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* PINEFORGE_TEST_NATIVE_ACID_COMPOSITE_H */ diff --git a/tests/test_native_acid_composite.cpp b/tests/test_native_acid_composite.cpp new file mode 100644 index 00000000..12260435 --- /dev/null +++ b/tests/test_native_acid_composite.cpp @@ -0,0 +1,2082 @@ +// test_native_acid_composite.cpp -- the G1 acid composite, C++ half (lane H-MEASURE, +// item A4-ACID-COVERAGE; extends the fourth audit's codex composite witness). +// +// ONE Pine-free strategy written against NativeStrategyHost that uses every +// feature the G1 acid test asks for (the fourth audit's G1 feature list): +// +// F01 a Sized entry with a fee reserve +// F02 a bracket with anchored legs on a price grid +// F03 a re-issued exit with keep_binding +// F04 a trail in ticks +// F05 a maintenance-only margin model, kernel liquidation seen through open lots +// F06 an FX curve with a step inside the run +// F07 a risk limit (and its refusal: an opening refused with RiskLimit) +// F08 calc-on-fills +// F09 a 60-minute subscription over 15-minute bars + an auxiliary finer feed +// with the interval query +// F10 session-day first/last flags +// F11 a partial close across lots and an exact-sum close +// F12 a typed refusal path +// F13 magnifier counters on the bare host +// F14 the kernel-recorded report with per-bar broker-state hashes +// +// Source-free: it includes only kernel headers and links +// libpineforge_kernel.a. It runs the plain scenario (the C twin) in batch and +// stream, the keep_binding scenario (C++ only: C has no ReplaceOptions word) in +// batch and stream, a lower_tf-magnified batch, and a batch cut after bar 60 +// (prefix closure). It prints +// PASS|FAIL one per requested feature +// FIELD .= every shared ledger fact (the C half prints the +// identical set: test_native_acid_composite_c.c) +// XFIELD = excl= +// C++-only facts, each family named by the C half +// in an EXCLUDED line +// DRIVING ... the batch/stream accounting, per documented rule +// and exits non-zero on any FAIL. Built with -DACID_WITH_C and linked with the +// C half, it also runs the C port in-process and compares the FIELD sets. +#include "native_acid_composite.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; +namespace tk = pineforge::native_toolkit; + +static_assert(sizeof(Bar) == sizeof(pf_bar_t), "Bar is layout-compatible with pf_bar_t"); + +namespace { + +// ============================================================ output +std::vector g_fields; // FIELD lines, for the in-process C comparison + +std::string fmt(const char* f, ...) { + char buf[4096]; + va_list ap; + va_start(ap, f); + std::vsnprintf(buf, sizeof buf, f, ap); + va_end(ap); + return buf; +} +std::string D(double v) { + char b[64]; + return acid_d(b, sizeof b, v); +} +using ull = unsigned long long; +using ll = long long; + +void out(const std::string& s) { std::printf("%s\n", s.c_str()); } +void field(const std::string& key, const std::string& value) { + const std::string l = "FIELD " + key + "=" + value; + g_fields.push_back(l); + out(l); +} +void xfield(const std::string& family, const std::string& key, const std::string& value) { + out("XFIELD " + key + "=" + value + " excl=" + family); +} + +// A feature is PASS when every one of its checks held. +struct Feature { + std::string id, name; + int total = 0, failed = 0; + std::vector failures; +}; +std::vector g_features; +Feature& feature(const std::string& id, const std::string& name) { + for (auto& f : g_features) + if (f.id == id) return f; + g_features.push_back(Feature{id, name, 0, 0, {}}); + return g_features.back(); +} +void fcheck(const std::string& id, const std::string& name, bool ok, const std::string& what) { + Feature& f = feature(id, name); + ++f.total; + if (!ok) { + ++f.failed; + f.failures.push_back(what); + } +} +bool same(double a, double b) { return std::memcmp(&a, &b, sizeof(double)) == 0; } +bool near_rel(double a, double b, double rel) { + return std::fabs(a - b) <= rel * std::max(1.0, std::max(std::fabs(a), std::fabs(b))); +} + +// ============================================================ tape +std::vector g_b15, g_b5; +void build_tape() { + pf_bar_t b15[ACID_BARS], b5[ACID_FEED]; + acid_build_tape(b15, b5); + g_b15.resize(ACID_BARS); + g_b5.resize(ACID_FEED); + std::memcpy(g_b15.data(), b15, sizeof b15); + std::memcpy(g_b5.data(), b5, sizeof b5); +} + +// ============================================================ hand arithmetic +// The 0.25 ladder (native-engine.md:336-338): every tape price is a multiple of +// 0.05 and every level below is an exact binary64 multiple of 0.25. +double ceil_tick(double p) { return std::ceil(p / ACID_TICK) * ACID_TICK; } +double floor_tick(double p) { return std::floor(p / ACID_TICK) * ACID_TICK; } +double nearest_tick(double p) { return std::round(p / ACID_TICK) * ACID_TICK; } +// A Directional market fill: the adverse tick, then one tick of slippage +// (native-engine.md:183-186, :336-338). +double buy_fill(double open) { return ceil_tick(open) + ACID_TICK; } +double sell_fill(double open) { return floor_tick(open) - ACID_TICK; } +// The quantity-grid floor: the largest grid multiple at or below the quotient +// (native-engine.md:3119-3126), n * step. +double floor_grid(double q) { + double n = std::floor(q / ACID_QTY_GRID); + while ((n + 1.0) * ACID_QTY_GRID <= q) n += 1.0; + while (n * ACID_QTY_GRID > q) n -= 1.0; + return n * ACID_QTY_GRID; +} +// A percent fee: abs(units) x price x point_value x fx x fee / 100 (native-engine.md:188-189). +double fee(double u, double p, double fx) { + return std::fabs(u) * p * ACID_POINT_VALUE * fx * ACID_FEE_PCT / 100.0; +} + +// ============================================================ records +struct FillRec { + int gi = 0; + no::ExecutionAppliedEvent ev; + std::string label; + no::RequestOrigin origin = no::RequestOrigin::Host; + uint64_t at_ord = 0; + int at_ii = 0; + int64_t at_eff = 0; + double at_px = NAN; + int at_qk = 0; +}; +struct BarRec { + NativeDecisionContext ctx; + double px = NAN; + int qk = 0; + uint64_t quote_origin = 0; + int64_t sd = -1, nsd = -1; + uint64_t broker_half = 0; + NativeRiskState risk; +}; +struct TfRec { + std::size_t sub = 0; + Bar bar{}; + NativeCompletionKind completion{}; + int64_t delivered_at = 0; + native_calendar::NativeInterval iv{}; + bool pulled = false; + int gi = 0; +}; +struct RecalcRec { + uint64_t cause_ord = 0, cause_inc = 0; + int gi = 0; + std::optional partial; + double px = NAN; + int qk = 0; + uint64_t ord = 0; + NativeRiskState risk; +}; +struct CheckRec { + NativeMarginCheckPoint p; +}; +struct ReqRec { + NativeMarginRequirementView v; +}; +struct AnchorRec { + NativeAnchoredLevelView v; +}; +struct Refusal { + std::string id, word; + bool unchanged = false; +}; +struct Snap { + double units = 0, avg = 0, eq = 0; + std::size_t lots = 0, working = 0; + bool operator==(const Snap& o) const { + return same(units, o.units) && same(avg, o.avg) && same(eq, o.eq) && lots == o.lots + && working == o.working; + } +}; + +const char* reject_word(no::RequestRejectReason r) { + switch (r) { + case no::RequestRejectReason::InvalidQuantity: return "InvalidQuantity"; + case no::RequestRejectReason::OffGrid: return "OffGrid"; + case no::RequestRejectReason::InvalidTrigger: return "InvalidTrigger"; + case no::RequestRejectReason::InvalidCapacity: return "InvalidCapacity"; + case no::RequestRejectReason::InvalidOwner: return "InvalidOwner"; + case no::RequestRejectReason::InvalidQuantityBasis: return "InvalidQuantityBasis"; + case no::RequestRejectReason::InvalidGroup: return "InvalidGroup"; + case no::RequestRejectReason::PlacementAdmission: return "PlacementAdmission"; + } + return "?"; +} +std::string submit_word(const no::SubmitResult& r) { + if (r.status == no::SubmitStatus::Accepted) return "Accepted"; + return std::string("Rejected/") + (r.reason ? reject_word(*r.reason) : "none"); +} +const char* replace_word(no::ReplaceStatus s) { + switch (s) { + case no::ReplaceStatus::Replaced: return "Replaced"; + case no::ReplaceStatus::ReplaceRejected: return "ReplaceRejected"; + case no::ReplaceStatus::NotWorking: return "NotWorking"; + case no::ReplaceStatus::InvalidHandle: return "InvalidHandle"; + } + return "?"; +} +const char* cancel_word(no::CancelStatus s) { + switch (s) { + case no::CancelStatus::Cancelled: return "Cancelled"; + case no::CancelStatus::NotWorking: return "NotWorking"; + case no::CancelStatus::InvalidHandle: return "InvalidHandle"; + } + return "?"; +} +const char* append_word(NativeAuxiliaryAppendError e) { + switch (e) { + case NativeAuxiliaryAppendError::None: return "None"; + case NativeAuxiliaryAppendError::HostFailed: return "HostFailed"; + case NativeAuxiliaryAppendError::Reentrant: return "Reentrant"; + case NativeAuxiliaryAppendError::NotRealtime: return "NotRealtime"; + case NativeAuxiliaryAppendError::NoAuxiliaryFeed: return "NoAuxiliaryFeed"; + case NativeAuxiliaryAppendError::InvalidBarArray: return "InvalidBarArray"; + case NativeAuxiliaryAppendError::InvalidBar: return "InvalidBar"; + case NativeAuxiliaryAppendError::UnorderedBars: return "UnorderedBars"; + case NativeAuxiliaryAppendError::InputPeriodAlreadyAccepted: return "InputPeriodAlreadyAccepted"; + case NativeAuxiliaryAppendError::AllocationFailure: return "AllocationFailure"; + } + return "?"; +} + +// ============================================================ the host +struct Opts { + std::string tag = "batch"; + bool stream = false; + bool keep = false; // re-issue X with ReplaceOptions{keep_binding} + bool magnify = false; // IntrabarPath::lower_tf over the 5-minute feed + int bars = ACID_BARS; // a batch cut short (prefix closure) +}; + +class AcidHost final : public NativeStrategyHost { +public: + explicit AcidHost(Opts o) : opt_(std::move(o)) {} + + std::vector fills; + std::vector bars; + std::vector tfs; + std::vector recalcs; + mutable std::vector checks; + mutable std::vector reqs; + mutable std::vector callviews; + mutable std::vector lots_before_call; + mutable bool lots_before_taken = false; + mutable std::vector anchors; + std::vector calls; + std::vector lots_after_call; + std::vector>> lots; + std::vector>> working; + std::vector>> liq; + std::vector>> trails; + std::vector> risks; + std::vector refusals; + std::vector> sized; + std::vector x_replaces; + std::optional preview; + tk::BracketReceipt bracket{}; + int inputs = 0, sub_bars = 0, begins = 0; + std::optional h_e1, h_tp, h_sl, h_e2, h_x, h_e3, h_t3, h_e4a, h_e4b, + h_e5, h_xf, h_p1, h_p2; + std::optional h_d4[4]; + bool x_done = false; + double p2_units = 0.0; + std::vector p2_book; + + // A test-only reach for the protected broker-state fold (engine.hpp:413): + // the continuation factored out at a fixed execution hash. + uint64_t broker_half() const { return broker_state_hash_from_execution_hash(0); } + double net() const { return net_profit(); } + +private: + Opts opt_; + std::optional cal_; + + Snap snap(double mark) const { + const auto p = physical_position(); + return Snap{p.signed_units, p.average_price, native_marked_equity(mark), p.lot_count, + native_working_requests().size()}; + } + void refusal(const std::string& id, const std::string& word, const Snap& before, double mark) { + refusals.push_back(Refusal{id, word, snap(mark) == before}); + } + int64_t day_of(int64_t ms) const { + if (!cal_) return -1; + const auto d = native_calendar::session_day_ordinal(*cal_, ms); + return d ? *d : -1; + } + + // ------------------------------------------------------------ Monday 09:00 + void monday_open(const Bar& bar) { + const double mark = bar.close; + const Snap s0 = snap(mark); + refusal("R01.transact_zero", submit_word(submit({no::Transact{0.0}, "R01", "zero"})), s0, mark); + refusal("R02.off_grid", submit_word(submit({no::Transact{0.005}, "R02", "off grid"})), s0, mark); + { + no::Sized bad; + bad.side = no::Side::Long; + bad.basis = no::CashValue{-5.0}; + refusal("R03.bad_basis", submit_word(submit({bad, "R03", "negative cash"})), s0, mark); + } + { + no::Sized big; + big.side = no::Side::Long; + big.basis = no::CashValue{5000000.0}; + big.time = no::SizeTime::AtAcceptance; + refusal("R04.placement", submit_word(submit({big, "R04", "too big"})), s0, mark); + } + { + const auto r = declare_timeframe_subscriptions_result({}); + refusal("R05.declare_outside_begin", + fmt("%s/%d/%d", r.status == NativeSetupStatus::Applied ? "Applied" : "Failed", + static_cast(r.validation.error), static_cast(r.validation.field)), + s0, mark); + } + { + NativeFxCurve late{{bar.timestamp + 1}, {2.0}}; + const auto r = configure_native_fx_curve(late); + refusal("R06.fx_curve_while_running", + fmt("%s/%d/%zu", r.status == NativeSetupStatus::Applied ? "Applied" : "Failed", + static_cast(r.validation.error), r.validation.index), + s0, mark); + } + // F01: the kernel's own sizing as a pure query, at the price the + // candidate will resolve (Directional + 1 tick: native-engine.md:3242-3252). + no::Sized sz; + sz.side = no::Side::Long; + sz.basis = no::CashValue{ACID_E1_CASH}; + sz.time = no::SizeTime::AtMatch; + sz.price = no::SizePrice::Resolved; + sz.grid_policy = no::ExecutionGridPolicy::SnapToGrid; + sz.reserve_percent_fee = true; + const double fill_px = buy_fill(g_b15[G_E1FILL].open); + sized.emplace_back("E1.reserve", native_sized_units(sz, fill_px, 0.0, ACID_ACCOUNT_FX).value_or(NAN)); + no::Sized plain = sz; + plain.reserve_percent_fee = false; + sized.emplace_back("E1.no_reserve", + native_sized_units(plain, fill_px, 0.0, ACID_ACCOUNT_FX).value_or(NAN)); + h_e1 = submit({sz, "E1", "sized entry"}).handle; + if (!h_e1) return; + no::Request take{no::Reduce{no::OwnerOpenedUnits{}}, "TP", "bracket"}; + take.trigger = no::Limit{0.0}; + take.anchor = no::FromOwnerFill{ACID_TP_OFFSET, false, no::NativeAnchorRounding::Raw}; + no::Request stop{no::Reduce{no::OwnerOpenedUnits{}}, "SL", "bracket"}; + stop.trigger = no::Stop{0.0}; + stop.anchor = no::FromOwnerFill{ACID_SL_OFFSET, false, no::NativeAnchorRounding::Raw}; + tk::BracketSpec spec; + spec.parent = *h_e1; + spec.take_profit = take; + spec.stop_loss = stop; + spec.anchor_rounding = no::NativeAnchorRounding::Directional; + spec.visibility = no::NativeArmVisibility::PendingUntilArmed; + bracket = tk::submit_bracket(*this, spec); + h_tp = bracket.take_profit; + h_sl = bracket.stop_loss; + working.emplace_back("E1", native_working_requests()); + const Snap s1 = snap(mark); + { + no::Request bad = take; + bad.label = "R07"; + bad.trigger = no::Limit{203.0}; + bad.owner = no::WaitForApplied{*h_e1}; + refusal("R07.written_anchor", submit_word(submit(bad)), s1, mark); + } + { + no::Sized child; + child.side = no::Side::Long; + child.basis = no::CashValue{1000.0}; + no::Request bad{child, "R08", "sized child"}; + bad.owner = no::WaitForApplied{*h_e1}; + refusal("R08.sized_child", submit_word(submit(bad)), s1, mark); + } + } + + void reissue_x(int gi) { + if (!h_x || x_done) return; + const double level = ACID_X_LEVEL0 + ACID_TICK * static_cast(gi - G_E2FILL); + no::Request x{no::Flatten{}, "X", "protect"}; + x.trigger = no::Stop{level}; + no::ReplaceResult r; + if (opt_.keep) { + no::ReplaceOptions options; + options.keep_binding = true; + r = replace(*h_x, x, options); + } else { + r = replace(*h_x, x); + } + x_replaces.push_back(r); + if (r.status == no::ReplaceStatus::Replaced && r.successor) h_x = r.successor; + } + + // ------------------------------------------------------------ callbacks + void on_native_run_begin() override { + ++begins; + if (const auto* s = native_state().spec) { + cal_ = native_calendar::parse_session(s->session, s->timezone); + } + } + void on_native_input(const Bar&, const NativeInputContext&) override { ++inputs; } + void on_native_sub_bar(const Bar&, const NativeDecisionContext&) override { ++sub_bars; } + + void on_native_timeframe_bar(const Bar& b, const NativeTimeframeBarContext& c) override { + TfRec t; + t.sub = c.subscription; + t.bar = b; + t.completion = c.completion; + t.delivered_at = c.delivered_at_ms; + t.iv = c.interval; + t.gi = static_cast(bars.size()); + const auto pulled = native_series_bar(c.subscription); + t.pulled = pulled && same(pulled->open, b.open) && same(pulled->high, b.high) + && same(pulled->low, b.low) && same(pulled->close, b.close) + && same(pulled->volume, b.volume) && pulled->timestamp == b.timestamp; + tfs.push_back(t); + } + + void on_native_recalculate(const Bar& bar, const NativeDecisionContext& ctx, + NativeCalculationReason reason, + const no::ExecutionAppliedEvent* cause) override { + if (reason == NativeCalculationReason::BarClose) { + on_native_bar(bar, ctx); + return; + } + if (reason != NativeCalculationReason::OrderFill) return; + RecalcRec r; + r.gi = static_cast(bars.size()); + if (cause) { + r.cause_ord = cause->ordinal; + r.cause_inc = cause->handle().incarnation; + } + r.partial = current_partial_bar(); + if (const auto pt = current_execution_point()) { + r.px = pt->price; + r.qk = static_cast(pt->quote_kind); + } + r.ord = ctx.coordinate.ordinal; + r.risk = native_risk_state(); + recalcs.push_back(r); + // F08: a request born in the take-profit's fill recalculation. + if (cause && h_tp && cause->handle() == *h_tp && !h_e2) { + h_e2 = submit({no::Transact{ACID_E2_UNITS}, "E2", "re-entry on the fill"}).handle; + } + } + + void on_native_bar(const Bar& bar, const NativeDecisionContext& ctx) override { + const int gi = static_cast(bars.size()); + BarRec rec; + rec.ctx = ctx; + if (const auto pt = current_execution_point()) { + rec.px = pt->price; + rec.qk = static_cast(pt->quote_kind); + rec.quote_origin = pt->quote_origin_ordinal; + } + rec.sd = day_of(ctx.script_interval.open_ms); + rec.nsd = day_of(ctx.script_interval.next_input_open_ms); + rec.broker_half = broker_half(); + rec.risk = native_risk_state(); + bars.push_back(rec); + const double mark = bar.close; + + if (gi == G_E1) monday_open(bar); + if (gi == G_E1FILL) { + lots.emplace_back("E1FILL", native_open_lots(mark)); + working.emplace_back("E1FILL", native_working_requests()); + } + if (gi == G_E2FILL) { + const Snap s0 = snap(mark); + if (h_e1) { + refusal("R09.replace_terminal", + replace_word(replace(*h_e1, no::Request{no::Transact{1.0}, "R09", ""}).status), + s0, mark); + } + if (h_sl) refusal("R10.cancel_cancelled_leg", cancel_word(cancel(*h_sl).status), s0, mark); + lots.emplace_back("E2FILL", native_open_lots(mark)); + no::Request x{no::Flatten{}, "X", "protect"}; + x.trigger = no::Stop{ACID_X_LEVEL0}; + h_x = submit(x).handle; + } + if (gi >= G_X_FIRST && gi <= G_X_LAST) reissue_x(gi); + if (gi == G_XFILL) lots.emplace_back("XFILL", native_open_lots(mark)); + if (gi == G_E3) { + h_e3 = submit({no::Transact{ACID_E3_UNITS}, "E3", "trail entry"}).handle; + if (h_e3) { + no::Request t{no::Reduce{no::OwnerOpenedUnits{}}, "T3", "trail"}; + t.trigger = no::Trail{0.0, std::nullopt, no::TrailTicks{ACID_T3_TICKS}}; + t.owner = no::WaitForApplied{*h_e3}; + h_t3 = submit(t).handle; + } + working.emplace_back("E3", native_working_requests()); + } + if (gi == G_E3FILL) lots.emplace_back("E3FILL", native_open_lots(mark)); + if (h_t3 && gi >= G_E3FILL && gi <= G_T3X) trails.emplace_back(gi, trail_state(*h_t3)); + if (gi == G_E4A) h_e4a = submit({no::Transact{ACID_E4A_UNITS}, "E4a", "short"}).handle; + if (gi == G_E4B) h_e4b = submit({no::Transact{ACID_E4B_UNITS}, "E4b", "short"}).handle; + if (gi == G_E4BFILL) { + lots.emplace_back("E4BFILL", native_open_lots(mark)); + liq.emplace_back("E4BFILL", native_liquidation_price()); + } + if (gi == G_PRESTEP) liq.emplace_back("PRESTEP", native_liquidation_price()); + if (gi == G_STEP) liq.emplace_back("STEP", native_liquidation_price()); + if (gi == G_PRESPIKE) liq.emplace_back("PRESPIKE", native_liquidation_price()); + if (gi == G_SPIKE) { + liq.emplace_back("SPIKE", native_liquidation_price()); + lots.emplace_back("SPIKE", native_open_lots(mark)); + risks.emplace_back("SPIKE", native_risk_state()); + // F07: an opening while the intraday-loss block stands. + h_e5 = submit({no::Transact{ACID_E5_UNITS}, "E5", "add while blocked"}).handle; + } + if (gi == G_E5REJ) { + risks.emplace_back("E5REJ", native_risk_state()); + h_xf = submit({no::Flatten{}, "XF", "flatten while blocked"}).handle; + } + if (gi == G_XFFILL) { + liq.emplace_back("XFFILL", native_liquidation_price()); + lots.emplace_back("XFFILL", native_open_lots(mark)); + } + if (gi == G_D4) { + risks.emplace_back("D4", native_risk_state()); + for (int k = 0; k < 4; ++k) { + h_d4[k] = submit({no::Transact{acid_d4_units[k]}, fmt("E%d", 6 + k), "thursday"}).handle; + } + // C++ only (C-SURFACE-2): a current-execution preview, observed and + // not executed. It must move no shared field. + if (h_d4[0]) { + preview = inspect_current_execution( + NativeCurrentExecution{*h_d4[0], NativeCurrentPriceRule::AsPresented}); + } + working.emplace_back("D4", native_working_requests()); + } + if (gi == G_D4FILL) lots.emplace_back("D4FILL", native_open_lots(mark)); + if (gi == G_P1) h_p1 = submit({no::Reduce{no::ExplicitUnits{ACID_P1_UNITS}}, "P1", "fifo partial"}).handle; + if (gi == G_P1FILL) lots.emplace_back("P1FILL", native_open_lots(mark)); + if (gi == G_P2) { + const auto book = native_open_lots(mark); + p2_book.clear(); + for (const auto& lot : book) p2_book.push_back(lot.signed_units); + if (book.size() >= 2) { + p2_units = p2_book[0] + p2_book[1]; // the binary64 sum of the first two lots + h_p2 = submit({no::Reduce{no::ExplicitUnits{p2_units}}, "P2", "exact sum"}).handle; + } + } + if (gi == G_P2FILL) lots.emplace_back("P2FILL", native_open_lots(mark)); + if (gi == opt_.bars - 1) { + lots.emplace_back("LAST", native_open_lots(mark)); + risks.emplace_back("LAST", native_risk_state()); + } + } + + void on_native_applied(const no::ExecutionAppliedEvent& ev, + const NativeDecisionContext& ctx) override { + FillRec f; + f.gi = static_cast(bars.size()); + f.ev = ev; + f.label = ev.request().label; + f.origin = ev.definition->origin; + f.at_ord = ctx.coordinate.ordinal; + f.at_ii = ctx.coordinate.interval_index; + f.at_eff = ctx.coordinate.effective_time_ms; + if (const auto pt = current_execution_point()) { + f.at_px = pt->price; + f.at_qk = static_cast(pt->quote_kind); + } + fills.push_back(f); + if (h_x && ev.handle() == *h_x) x_done = true; + } + + bool margin_check_allowed(const NativeMarginCheckPoint& p) const override { + checks.push_back(CheckRec{p}); + return true; + } + std::optional resolve_margin_requirement( + const NativeMarginRequirementView& v) const override { + reqs.push_back(ReqRec{v}); + return std::nullopt; + } + std::optional resolve_margin_call_units(const NativeMarginCallView& v) const override { + callviews.push_back(v); + if (!lots_before_taken) { + lots_before_call = native_open_lots(v.mark); + lots_before_taken = true; + } + return std::nullopt; + } + void on_native_margin_call(const no::MarginCallEvent& e) override { + calls.push_back(e); + lots_after_call = native_open_lots(e.mark); + } + std::optional resolve_anchored_level(const NativeAnchoredLevelView& v) const override { + anchors.push_back(AnchorRec{v}); + return std::nullopt; + } +}; + +// ============================================================ spec +NativeRunSpec make_spec(const Opts& o) { + NativeRunSpec s; + s.identity = {"acid-composite", 1}; + s.input_tf = "15"; + s.script_tf = "15"; + s.ticker = "ACIDX"; + s.tickerid = "ACID:ACIDX"; + s.type = "cfd"; + s.currency = "EUR"; + s.basecurrency = ""; + s.description = "acid composite"; + s.volumetype = ""; + s.timezone = "UTC"; + s.session = "0900-1500:23456"; + s.chart_timezone = ""; + s.initial_capital = ACID_CAPITAL; + s.point_value = ACID_POINT_VALUE; + s.account_fx = ACID_ACCOUNT_FX; + s.price_tick = ACID_TICK; + s.slippage_ticks = ACID_SLIPPAGE; + s.price_grid = NativePriceGrid::QuantizeFillsAndTriggers; + s.grid_rounding = NativeGridRounding::Directional; + s.fee_kind = NativeFeeKind::Percent; + s.fee_value = ACID_FEE_PCT; + s.quantity_grid = ACID_QTY_GRID; + NativeMarginModel m; + m.initial_long = ACID_INITIAL_LONG; // the long side: an opening gate, no liquidation + m.initial_short = 0.0; // the short side: MAINTENANCE-ONLY + m.maintenance_short = ACID_MAINT_SHORT; + m.sizing = NativeLiquidationSizing::RestoreMinimum; + m.shortfall_multiple = 1.0; + m.check = NativeLiquidationCheck::PathAdverseExtreme; + m.liquidation_label = ACID_LIQ_LABEL; + m.liquidation_comment = ACID_LIQ_COMMENT; + s.margin = m; + NativeRiskLimits r; + r.max_intraday_loss = NativeLossLimit{ACID_RISK_LOSS, false}; + r.day_basis = NativeRiskDay::SessionDay; + r.action = NativeRiskAction::BlockOpenings; + s.risk = r; + s.report_policy = NativeReportPolicy::KernelRecorded; + s.report_open_position_at_end = true; + s.calculation = NativeCalculationTrigger::BarCloseAndFills; + s.max_recalculations_per_point = ACID_MAX_RECALCS; + NativeTimeframeSubscription hour; + hour.tf = "60"; + NativeTimeframeSubscription half; + half.tf = "30"; + half.source = NativeSeriesSource::AuxiliaryFeed; + s.subscriptions = {hour, half}; + NativeAuxiliaryFeed feed; + feed.tf = "5"; + // A stream declares what it knows at begin (the warmup's feed bars) and + // appends the rest live, one input at a time (native-engine.md:2655-2668). + // A batch cut short keeps the whole spec: prefix closure compares two runs + // of the SAME spec (native-engine.md:2024-2027). + const int feed_bars = o.stream ? 3 * ACID_WARMUP : ACID_FEED; + feed.bars.assign(g_b5.begin(), g_b5.begin() + feed_bars); + s.auxiliary_feed = feed; + s.event_retention = NativeEventRetention::Full; + if (o.magnify) { + IntrabarPath::lower_tf lower; + lower.bars = g_b5; + lower.tf = "5"; + lower.samples = 4; + s.intrabar.value = lower; + } + return s; +} + +NativeFxCurve make_curve() { + NativeFxCurve c; + c.effective_from_ms = {ACID_FX_T0, ACID_FX_T1}; + c.account_per_quote = {ACID_FX_R0, ACID_FX_R1}; + return c; +} + +// ============================================================ a run +struct Run { + Opts opt; + std::unique_ptr host; + ReportC report{}; + bool report_filled = false; + NativeSetupResult setup{}; + NativeFxCurveSetupResult bad_curve{}, good_curve{}; + std::vector> appends; + bool drive_ok = true; + std::string last_error; + uint64_t scalar = 0, continuation = 0, stream_hash = 0; + NativeStateView state{}; + std::vector events; + ~Run() { + if (report_filled) BacktestEngine::free_report(&report); + } +}; + +std::unique_ptr execute(const Opts& o) { + auto run = std::make_unique(); + run->opt = o; + run->host = std::make_unique(o); + AcidHost& h = *run->host; + run->setup = h.configure_native(make_spec(o)); + if (run->setup.status != NativeSetupStatus::Applied) { + std::printf("configure refused: %d/%d %s\n", static_cast(run->setup.validation.error), + static_cast(run->setup.validation.field), h.last_error().c_str()); + run->drive_ok = false; + return run; + } + // A curve refused while Ready changes nothing (F12); then the real curve. + NativeFxCurve bad = make_curve(); + std::swap(bad.effective_from_ms[0], bad.effective_from_ms[1]); + run->bad_curve = h.configure_native_fx_curve(bad); + run->good_curve = h.configure_native_fx_curve(make_curve()); + h.set_broker_state_hash_recording(true); + if (!o.stream) { + h.run(g_b15.data(), o.bars); + run->appends.emplace_back("after_batch", h.append_auxiliary_bars_result(&g_b5[0], 1)); + } else { + run->drive_ok = h.stream_begin(g_b15.data(), ACID_WARMUP, "15", "15"); + for (int i = ACID_WARMUP; i < o.bars && run->drive_ok; ++i) { + if (i == ACID_WARMUP + 2) { + // Typed append refusals between inputs, changing nothing. + Bar pair[2] = {g_b5[3 * i + 1], g_b5[3 * i]}; + run->appends.emplace_back("unordered_pair", h.append_auxiliary_bars_result(pair, 2)); + Bar stale = g_b5[3 * (i - 1)]; + run->appends.emplace_back("stale_bar", h.append_auxiliary_bars_result(&stale, 1)); + } + const auto a = h.append_auxiliary_bars_result(&g_b5[3 * i], 3); + if (a.status != NativeSetupStatus::Applied) { + std::printf("append refused at %d: %s\n", i, append_word(a.error)); + run->drive_ok = false; + } + run->drive_ok = run->drive_ok && h.stream_push_bar(g_b15[i]); + } + run->drive_ok = run->drive_ok && h.stream_end(false); + run->appends.emplace_back("after_end", h.append_auxiliary_bars_result(&g_b5[0], 1)); + run->stream_hash = h.stream_state_hash(); + } + run->last_error = h.last_error(); + run->state = h.native_state(); + h.fill_report(&run->report); + run->report_filled = true; + run->scalar = h.broker_state_hash(); + run->continuation = h.native_continuation_hash(); + run->events = h.native_events(0); + return run; +} + +// ============================================================ event projection +// The flattening src/native_c_host.cpp:1918-2033 performs for +// strategy_native_events_v1, over the C++ rows (every C word is the C++ +// enumerator's integer, pinned by static_asserts in native_c_host.cpp). +template uint32_t w(E e) { return static_cast(e); } +void fill_cursor(pf_native_event_v1& o, const no::MatchCursor& c) { + o.effective_time_ms = c.point.effective_time_ms; + o.interval_index = c.point.interval_index; + o.provenance = static_cast(c.point.provenance); + o.path_phase = static_cast(c.point.path_phase); +} +pf_native_event_v1 blank(uint32_t kind, uint64_t ordinal) { + pf_native_event_v1 o; + std::memset(&o, 0, sizeof o); + o.struct_size = sizeof o; + o.version = PF_NATIVE_API_VERSION; + o.kind = kind; + o.ordinal = ordinal; + return o; +} +pf_native_event_v1 project(const NativeMarketEvent& e) { + if (e.kind == NativeEventKind::Driver && e.driver) { + auto o = blank(PF_NATIVE_EVENT_DRIVER_POINT, e.ordinal); + o.raw_price = e.driver->raw_price; + o.price = e.driver->raw_price; + o.effective_time_ms = e.driver->coordinate.effective_time_ms; + o.interval_index = e.driver->coordinate.interval_index; + o.provenance = static_cast(e.driver->coordinate.provenance); + o.path_phase = static_cast(e.driver->coordinate.path_phase); + return o; + } + if (e.kind == NativeEventKind::Account && e.account) { + auto o = blank(PF_NATIVE_EVENT_ACCOUNT, e.ordinal); + o.price = e.account->marked_equity; + o.raw_price = e.account->realized_balance; + o.opened_units = e.account->signed_units; + o.effective_time_ms = e.account->effective_time_ms; + return o; + } + const uint64_t ord = e.ordinal; + return std::visit([&](const auto& p) -> pf_native_event_v1 { + using T = std::decay_t; + pf_native_event_v1 o{}; + if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_ACCEPTED, ord); + o.incarnation = p.handle().incarnation; + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_REJECTED, ord); + o.reason = w(p.reason); + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_REPLACED, ord); + o.incarnation = p.predecessor().incarnation; + o.successor = p.successor().incarnation; + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_REPLACE_REJECTED, ord); + o.incarnation = p.target().incarnation; + o.reason = w(p.reason); + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_CANCELLED, ord); + o.incarnation = p.handle().incarnation; + o.reason = w(p.reason); + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_NOT_WORKING, ord); + o.incarnation = p.target.incarnation; + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_INVALID_HANDLE, ord); + o.incarnation = p.target.incarnation; + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_NO_EFFECT, ord); + o.incarnation = p.handle().incarnation; + fill_cursor(o, p.cursor); + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_MATCH_REJECTED, ord); + o.incarnation = p.handle().incarnation; + o.reason = w(p.reason); + fill_cursor(o, p.cursor); + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_APPLIED, ord); + o.incarnation = p.handle().incarnation; + o.raw_price = p.raw_price; + o.resolved_price = p.resolved_price; + o.price = p.resolved_price; + o.closed_units = p.closed_units; + o.opened_units = p.opened_units; + o.cycle_before = p.cycle_before; + o.cycle_after = p.cycle_after; + o.terminal = p.terminal ? 1 : 0; + if (p.terminal_reason) o.reason = w(*p.terminal_reason); + fill_cursor(o, p.cursor); + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_CLOSE_BOUND, ord); + o.incarnation = p.definition->handle.incarnation; + o.reason = w(p.side); + o.cycle_after = p.cycle; + fill_cursor(o, p.cursor); + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_ACTIVATED, ord); + o.incarnation = p.definition->handle.incarnation; + o.reason = w(p.kind); + o.price = p.reached_price; + o.raw_price = p.reached_price; + fill_cursor(o, p.cursor); + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_RESERVATION_REDUCED, ord); + o.incarnation = p.recipient.incarnation; + o.reason = w(p.effect); + o.closed_units = p.actual_deduction; + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_DEFERRED_GROUP, ord); + o.incarnation = p.recipient.incarnation; + o.reason = w(p.effect); + o.closed_units = p.deferred_delta; + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_QUANTITY_BOUND, ord); + o.incarnation = p.definition->handle.incarnation; + o.opened_units = p.source_units; + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_ARMED, ord); + o.incarnation = p.definition->handle.incarnation; + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_TERMS_RESOLVED, ord); + o.incarnation = p.handle().incarnation; + o.raw_price = p.input.raw_price; + o.resolved_price = p.input.terms.resolved_price; + o.price = p.input.terms.resolved_price; + fill_cursor(o, p.cursor); + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_MARGIN_CALL, ord); + o.incarnation = p.handle().incarnation; + o.reason = w(p.side); + o.price = p.mark; + o.resolved_price = p.mark; + o.raw_price = p.liquidation_price; + o.closed_units = p.units; + o.opened_units = p.position_after; + fill_cursor(o, p.cursor); + } else if constexpr (std::is_same_v) { + o = blank(PF_NATIVE_EVENT_RISK, ord); + o.reason = w(p.kind); + o.price = p.observed; + o.raw_price = p.limit; + o.cycle_before = p.day_ordinal; + o.successor = p.cursor.point.ordinal; + fill_cursor(o, p.cursor); + } + return o; + }, *e.command); +} + +// ============================================================ C words of C++ values +uint32_t intent_word(const no::OrderIntent& i, double& value) { + value = 0.0; + return std::visit([&](const auto& a) -> uint32_t { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return PF_NATIVE_INTENT_FLATTEN; + } else if constexpr (std::is_same_v) { + if (const auto* u = std::get_if(&a.size)) value = u->units; + else if (const auto* f = std::get_if(&a.size)) value = f->fraction; + return PF_NATIVE_INTENT_REDUCE; + } else if constexpr (std::is_same_v) { + value = a.signed_units; + return PF_NATIVE_INTENT_TRANSACT; + } else if constexpr (std::is_same_v) { + value = a.signed_units; + return PF_NATIVE_INTENT_REVERSE_TO; + } else if constexpr (std::is_same_v) { + return PF_NATIVE_INTENT_HOST_SIZED; + } else { + if (const auto* c = std::get_if(&a.basis)) value = c->cash; + else value = std::get(a.basis).fraction; + return PF_NATIVE_INTENT_SIZED; + } + }, i); +} +uint32_t trigger_word(const no::Trigger& t, double& p1, double& p2) { + p1 = p2 = 0.0; + return std::visit([&](const auto& a) -> uint32_t { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return PF_NATIVE_TRIGGER_MARKET; + } else if constexpr (std::is_same_v) { + p1 = a.price; + return PF_NATIVE_TRIGGER_LIMIT; + } else if constexpr (std::is_same_v) { + p1 = a.price; + return PF_NATIVE_TRIGGER_STOP; + } else if constexpr (std::is_same_v) { + p1 = a.stop; + p2 = a.limit; + return PF_NATIVE_TRIGGER_STOP_LIMIT; + } else { + p1 = a.offset; + if (a.arm_price) p2 = *a.arm_price; + return PF_NATIVE_TRIGGER_TRAIL; + } + }, t); +} + +// ============================================================ FIELD emission +std::string lot_line(const NativeOpenLot& l) { + return fmt("ord:%zu;inc:%llu;cyc:%lld;side:%d;eb:%d;et:%lld;ep:%s;u:%s;com:%s;mark:%s;upnl:%s;" + "fav:%s;adv:%s;lab:%s;cmt:%s", + l.ordinal, (ull)l.entry_incarnation, (ll)l.cycle, static_cast(l.side), + l.entry_bar_index, (ll)l.entry_time_ms, D(l.entry_price).c_str(), + D(l.signed_units).c_str(), D(l.entry_commission).c_str(), D(l.mark).c_str(), + D(l.unrealized_pnl).c_str(), D(l.favorable_excursion).c_str(), + D(l.adverse_excursion).c_str(), l.entry_label.c_str(), l.entry_comment.c_str()); +} +void emit_lots(const std::string& key, const std::vector& lots) { + field(key, fmt("n:%zu", lots.size())); + for (std::size_t i = 0; i < lots.size(); ++i) field(fmt("%s.%02zu", key.c_str(), i), lot_line(lots[i])); +} +std::string risk_str(const NativeRiskState& r) { + return fmt("%d/%d/%d/%d/%lld/%llu/%u/%s/%s", r.blocked ? 1 : 0, r.reason ? 1 : 0, + r.reason ? static_cast(*r.reason) : 0, r.has_day ? 1 : 0, (ll)r.day_ordinal, + (ull)r.fills_today, r.consecutive_loss_days, D(r.peak_equity).c_str(), + D(r.day_open_equity).c_str()); +} +std::string cursor_str(const no::MatchCursor& c) { + return fmt("%llu/%lld/%s/%d/%d/%d", (ull)c.point.ordinal, (ll)c.point.effective_time_ms, + D(c.t).c_str(), c.point.interval_index, static_cast(c.point.provenance), + static_cast(c.point.path_phase)); +} + +void emit_run(const Run& run) { + const AcidHost& h = *run.host; + const std::string p = run.opt.tag; + const ReportC& r = run.report; + field(p + ".setup", fmt("status:%d;err:%d;field:%d", static_cast(run.setup.status), + static_cast(run.setup.validation.error), + static_cast(run.setup.validation.field))); + field(p + ".fx_curve", fmt("bad:%d/%d/%zu;good:%d/%d/%zu", static_cast(run.bad_curve.status), + static_cast(run.bad_curve.validation.error), + run.bad_curve.validation.index, static_cast(run.good_curve.status), + static_cast(run.good_curve.validation.error), + run.good_curve.validation.index)); + field(p + ".report", + fmt("trades:%d;trades_len:%d;net:%s;inputs:%lld;scripts:%lld;mag_sub:%lld;mag_ticks:%lld;" + "itf:%d;stf:%d;ratio:%d;agg:%d;mag:%d;eq_len:%lld;hash_len:%lld", + r.total_trades, r.trades_len, D(r.net_profit).c_str(), (ll)r.input_bars_processed, + (ll)r.script_bars_processed, (ll)r.magnifier_sub_bars_total, + (ll)r.magnifier_sample_ticks_total, r.input_tf_seconds, r.script_tf_seconds, + r.script_tf_ratio, r.needs_aggregation, r.bar_magnifier_enabled, + (ll)r.equity_curve_len, (ll)r.broker_state_hash_len)); + for (int i = 0; i < r.trades_len; ++i) { + const TradeC& t = r.trades[i]; + const Trade& row = h.get_report_trade(i); + field(fmt("%s.trade.%02d", p.c_str(), i), + fmt("et:%lld;xt:%lld;ep:%s;xp:%s;pnl:%s;pct:%s;long:%d;ru:%s;dd:%s;qty:%s;com:%s;" + "eb:%d;xb:%d;end:%d;eid:%s;xid:%s;xc:%s;cause:%d;inc:%llu", + (ll)t.entry_time, (ll)t.exit_time, D(t.entry_price).c_str(), D(t.exit_price).c_str(), + D(t.pnl).c_str(), D(t.pnl_pct).c_str(), t.is_long, D(t.max_runup).c_str(), + D(t.max_drawdown).c_str(), D(t.qty).c_str(), D(t.commission).c_str(), + t.entry_bar_index, t.exit_bar_index, t.open_at_end, row.entry_id.c_str(), + row.exit_id.c_str(), row.exit_comment.c_str(), h.closed_trade_close_cause(i), + (ull)row.entry_incarnation)); + xfield("C-SURFACE-2:closed-trade-entry-comment", fmt("%s.trade.%02d.entry_comment", p.c_str(), i), + row.entry_comment); + } + // Per bar: the decision the calculation was handed, and the report row. + std::string open_ended, drv; + uint64_t half_digest = ACID_FNV_OFFSET, qo_digest = ACID_FNV_OFFSET; + for (std::size_t i = 0; i < h.bars.size(); ++i) { + const BarRec& b = h.bars[i]; + const auto& c = b.ctx; + const auto& iv = c.script_interval; + const uint64_t row = i < static_cast(r.broker_state_hash_len) ? r.broker_state_hash[i] : 0; + const double eq = i < static_cast(r.equity_curve_len) ? r.equity_curve[i].equity : NAN; + const double op = i < static_cast(r.equity_curve_len) ? r.equity_curve[i].open_profit : NAN; + field(fmt("%s.bar.%02zu", p.c_str(), i), + fmt("ord:%llu;ii:%d;eff:%lld;sbo:%lld;floor:%lld;px:%s;qk:%d;prov:%d;ph:%d;comp:%d;" + "sub:%d/%d/%d;sess:%d%d%d;iv:%lld/%lld/%lld/%lld/%lld;sd:%lld/%lld;row:%llu;eq:%s;op:%s;" + "risk:%s", + (ull)c.coordinate.ordinal, c.coordinate.interval_index, (ll)c.coordinate.effective_time_ms, + (ll)c.script_bar_open_ms, (ll)c.decision_floor_ms, D(b.px).c_str(), b.qk, + static_cast(c.coordinate.provenance), static_cast(c.coordinate.path_phase), + static_cast(c.coordinate.completion), c.sub_index, c.sub_count, + c.is_terminal_sub_bar ? 1 : 0, c.in_session ? 1 : 0, c.opens_session_day ? 1 : 0, + c.closes_session_day ? 1 : 0, (ll)iv.open_ms, (ll)iv.eligible_open_ms, + (ll)iv.last_traded_close_ms, (ll)iv.next_period_open_ms, (ll)iv.next_input_open_ms, + (ll)b.sd, (ll)b.nsd, (ull)row, D(eq).c_str(), D(op).c_str(), risk_str(b.risk).c_str())); + open_ended += c.closes_session_day_open_ended ? '1' : '0'; + half_digest = acid_fnv_u64(half_digest, b.broker_half); + qo_digest = acid_fnv_u64(qo_digest, b.quote_origin); + if (i == 0 || i + 1 == h.bars.size()) { + const auto& s = c.driver_statistics; + drv += fmt("%s%zu:%d/%d/%d/%llu/%llu", drv.empty() ? "" : ",", i, s.intrabar_path_enabled ? 1 : 0, + s.sub_bars_per_script_bar, s.samples_per_sub_bar, (ull)s.sub_bars_processed, + (ull)s.sample_ticks_processed); + } + } + xfield("COVERAGE:closes_session_day_open_ended", p + ".bars.open_ended", open_ended); + xfield("PROTECTED:broker_state_hash_from_execution_hash", p + ".bars.broker_half_digest", + fmt("%llu", (ull)half_digest)); + xfield("NOC:quote_origin_ordinal", p + ".bars.quote_origin_digest", fmt("%llu", (ull)qo_digest)); + xfield("NOC:driver_statistics", p + ".bars.driver_statistics", drv); + for (std::size_t i = 0; i < h.fills.size(); ++i) { + const FillRec& f = h.fills[i]; + const auto& e = f.ev; + field(fmt("%s.fill.%02zu", p.c_str(), i), + fmt("gi:%d;ord:%llu;inc:%llu;lot:%llu;raw:%s;res:%s;tkt:%s;cu:%s;ou:%s;fw:%s;cb:%lld;ca:%lld;" + "term:%d;tr:%d;at:%llu/%d/%lld/%s/%d", + f.gi, (ull)e.ordinal, (ull)e.handle().incarnation, (ull)e.opened_lot_incarnation, + D(e.raw_price).c_str(), D(e.resolved_price).c_str(), D(e.current_ticket).c_str(), + D(e.closed_units).c_str(), D(e.opened_units).c_str(), D(e.filled_working).c_str(), + (ll)e.cycle_before, (ll)e.cycle_after, e.terminal ? 1 : 0, + e.terminal_reason ? static_cast(*e.terminal_reason) : -1, (ull)f.at_ord, f.at_ii, + (ll)f.at_eff, D(f.at_px).c_str(), f.at_qk)); + xfield("C-SURFACE-2:applied-origin-label", fmt("%s.fill.%02zu.who", p.c_str(), i), + fmt("label:%s;origin:%d", f.label.c_str(), static_cast(f.origin))); + } + for (std::size_t i = 0; i < h.tfs.size(); ++i) { + const TfRec& t = h.tfs[i]; + field(fmt("%s.tf.%02zu", p.c_str(), i), + fmt("sub:%zu;gi:%d;t:%lld;o:%s;h:%s;l:%s;c:%s;v:%s;comp:%d;at:%lld;iv:%lld/%lld/%lld/%lld/%lld;" + "pull:%d", + t.sub, t.gi, (ll)t.bar.timestamp, D(t.bar.open).c_str(), D(t.bar.high).c_str(), + D(t.bar.low).c_str(), D(t.bar.close).c_str(), D(t.bar.volume).c_str(), + static_cast(t.completion), (ll)t.delivered_at, (ll)t.iv.open_ms, + (ll)t.iv.eligible_open_ms, (ll)t.iv.last_traded_close_ms, (ll)t.iv.next_period_open_ms, + (ll)t.iv.next_input_open_ms, t.pulled ? 1 : 0)); + } + for (std::size_t i = 0; i < h.recalcs.size(); ++i) { + const RecalcRec& c = h.recalcs[i]; + const Bar pb = c.partial.value_or(Bar{NAN, NAN, NAN, NAN, NAN, 0}); + field(fmt("%s.recalc.%02zu", p.c_str(), i), + fmt("cause:%llu/%llu;gi:%d;part:%d/%s/%s/%s/%s/%s;px:%s;qk:%d;ord:%llu;risk:%s", + (ull)c.cause_ord, (ull)c.cause_inc, c.gi, c.partial ? 1 : 0, D(pb.open).c_str(), + D(pb.high).c_str(), D(pb.low).c_str(), D(pb.close).c_str(), D(pb.volume).c_str(), + D(c.px).c_str(), c.qk, (ull)c.ord, risk_str(c.risk).c_str())); + } + field(p + ".recalc.totals", fmt("driven:%llu;skipped:%llu", (ull)h.native_recalculation_count(), + (ull)h.native_recalculations_skipped())); + // Margin: every check point and requirement view, digested; the FX-roll + // points and every breached view in full. + int kinds[4] = {0, 0, 0, 0}; + uint64_t cd = ACID_FNV_OFFSET; + int fx_n = 0; + for (const auto& c : h.checks) { + const auto& q = c.p; + ++kinds[static_cast(q.kind) & 3]; + cd = acid_fnv_u64(cd, w(q.kind)); + cd = acid_fnv_d(cd, q.position.signed_units); + cd = acid_fnv_d(cd, q.position.average_price); + cd = acid_fnv_u64(cd, q.position.lot_count); + cd = acid_fnv_d(cd, q.mark); + cd = acid_fnv_u64(cd, q.liquidation_resting ? 1 : 0); + cd = acid_fnv_u64(cd, q.cursor.point.ordinal); + cd = acid_fnv_u64(cd, (uint64_t)q.cursor.point.effective_time_ms); + cd = acid_fnv_d(cd, q.cursor.t); + if (q.kind == NativeMarginCheckKind::FxRoll) { + field(fmt("%s.margin.fxroll.%02d", p.c_str(), fx_n++), + fmt("units:%s;avg:%s;lots:%zu;mark:%s;rest:%d;cur:%s", D(q.position.signed_units).c_str(), + D(q.position.average_price).c_str(), q.position.lot_count, D(q.mark).c_str(), + q.liquidation_resting ? 1 : 0, cursor_str(q.cursor).c_str())); + } + } + field(p + ".margin.checks", fmt("n:%zu;bar_open:%d;after_applied:%d;calc:%d;fx_roll:%d;digest:%llu", + h.checks.size(), kinds[0], kinds[1], kinds[2], kinds[3], (ull)cd)); + uint64_t rd = ACID_FNV_OFFSET; + int rq_n = 0; + for (const auto& rq : h.reqs) { + const auto& v = rq.v; + rd = acid_fnv_u64(rd, w(v.kind)); + rd = acid_fnv_d(rd, v.position.signed_units); + rd = acid_fnv_d(rd, v.mark); + rd = acid_fnv_d(rd, v.equity); + rd = acid_fnv_d(rd, v.required); + rd = acid_fnv_u64(rd, v.cursor.point.ordinal); + if (v.kind == NativeMarginCheckKind::FxRoll || v.required > v.equity) { + field(fmt("%s.margin.req.%02d", p.c_str(), rq_n++), + fmt("kind:%d;units:%s;mark:%s;eq:%s;req:%s;cur:%s", static_cast(v.kind), + D(v.position.signed_units).c_str(), D(v.mark).c_str(), D(v.equity).c_str(), + D(v.required).c_str(), cursor_str(v.cursor).c_str())); + } + } + field(p + ".margin.reqs", fmt("n:%zu;digest:%llu", h.reqs.size(), (ull)rd)); + for (std::size_t i = 0; i < h.callviews.size(); ++i) { + const auto& v = h.callviews[i]; + field(fmt("%s.margin.callview.%02zu", p.c_str(), i), + fmt("units:%s;avg:%s;lots:%zu;mark:%s;eq:%s;req:%s;cur:%s", D(v.position.signed_units).c_str(), + D(v.position.average_price).c_str(), v.position.lot_count, D(v.mark).c_str(), + D(v.equity).c_str(), D(v.required).c_str(), cursor_str(v.cursor).c_str())); + } + for (std::size_t i = 0; i < h.calls.size(); ++i) { + const auto& c = h.calls[i]; + field(fmt("%s.margin.call.%02zu", p.c_str(), i), + fmt("ord:%llu;inc:%llu;app:%llu;side:%d;mark:%s;eq:%s;req:%s;liq:%s;u:%s;pb:%s;pa:%s;cur:%s", + (ull)c.ordinal, (ull)c.handle().incarnation, (ull)c.applied.ordinal, + static_cast(c.side), D(c.mark).c_str(), D(c.equity).c_str(), D(c.required).c_str(), + D(c.liquidation_price).c_str(), D(c.units).c_str(), D(c.position_before).c_str(), + D(c.position_after).c_str(), cursor_str(c.cursor).c_str())); + } + if (!h.calls.empty()) { + emit_lots(p + ".lots.CALLBEFORE", h.lots_before_call); + emit_lots(p + ".lots.CALLAFTER", h.lots_after_call); + } + for (const auto& [tag, lv] : h.liq) field(p + ".liq." + tag, D(lv.value_or(NAN))); + for (const auto& [tag, lots] : h.lots) emit_lots(p + ".lots." + tag, lots); + for (const auto& [tag, rows] : h.working) { + field(p + ".working." + tag, fmt("n:%zu", rows.size())); + for (std::size_t i = 0; i < rows.size(); ++i) { + const auto& d = *rows[i].definition; + double value = 0.0, p1 = 0.0, p2 = 0.0; + const uint32_t iw = intent_word(d.request.intent, value); + const uint32_t tw = trigger_word(d.request.trigger, p1, p2); + double rem = 0.0; + if (const auto* u = std::get_if(&rows[i].remaining)) rem = u->q; + const auto* anchor = std::get_if(&d.request.anchor); + const auto* wait = std::get_if(&d.request.owner); + field(fmt("%s.working.%s.%02zu", p.c_str(), tag.c_str(), i), + fmt("inc:%llu;intent:%u/%s;trig:%u;p1:%s;p2:%s;rem:%zu/%s;ts:%zu;org:%d;owner:%zu;anc:%d/%s;" + "vis:%d;grp:%zu;lab:%s", + (ull)d.handle.incarnation, iw, D(value).c_str(), tw, D(p1).c_str(), D(p2).c_str(), + rows[i].remaining.index(), D(rem).c_str(), rows[i].trigger_state.index(), + static_cast(d.origin), d.request.owner.index(), anchor ? 1 : 0, + D(anchor ? anchor->offset : 0.0).c_str(), + wait ? static_cast(wait->visibility) : 0, d.request.group.index(), + d.request.label.c_str())); + } + } + for (const auto& [gi, ts] : h.trails) { + field(fmt("%s.trail.%02d", p.c_str(), gi), + ts ? fmt("act:%d;best:%s;lvl:%s;aord:%llu", ts->activated ? 1 : 0, D(ts->best_price).c_str(), + D(ts->current_level).c_str(), (ull)ts->activation_ordinal) + : std::string("absent")); + } + for (std::size_t i = 0; i < h.anchors.size(); ++i) { + const auto& v = h.anchors[i].v; + field(fmt("%s.anchor.%02zu", p.c_str(), i), + fmt("owner:%llu;app:%llu;lot:%llu;fill:%s;leg:%llu;side:%d;trig:%d;off:%s;tick:%s;lvl:%s;cur:%s", + (ull)v.owner.incarnation, (ull)v.owner_applied_ordinal, (ull)v.owner_lot_incarnation, + D(v.owner_fill_price).c_str(), (ull)v.leg.incarnation, static_cast(v.leg_side), + static_cast(v.trigger), D(v.offset).c_str(), D(v.price_tick).c_str(), + D(v.kernel_level).c_str(), cursor_str(v.owner_cursor).c_str())); + } + for (const auto& [tag, rs] : h.risks) field(p + ".risk." + tag, risk_str(rs)); + for (const auto& rf : h.refusals) field(p + ".refusal." + rf.id, fmt("%s;same:%d", rf.word.c_str(), rf.unchanged ? 1 : 0)); + for (const auto& [tag, u] : h.sized) field(p + ".sized." + tag, D(u)); + for (const auto& [id, a] : run.appends) + field(p + ".append." + id, fmt("%s/%zu", append_word(a.error), a.index)); + field(p + ".bracket", fmt("tp:%d;sl:%d;trail:%d;all:%d", static_cast(h.bracket.take_profit_outcome.state), + static_cast(h.bracket.stop_loss_outcome.state), + static_cast(h.bracket.trail_outcome.state), + h.bracket.every_requested_leg_accepted() ? 1 : 0)); + // X re-issues: the status of each replace (the keep variant is C++ only). + std::string xs; + for (const auto& x : h.x_replaces) xs += fmt("%s%s", xs.empty() ? "" : ",", replace_word(x.status)); + field(p + ".x_replaces", xs.empty() ? "none" : xs); + // Events, flattened exactly as the C API flattens them. + std::map counts; + uint64_t ed = ACID_FNV_OFFSET; + for (const auto& e : run.events) { + const pf_native_event_v1 pe = project(e); + ++counts[pe.kind]; + ed = acid_event_fold(ed, &pe); + } + field(p + ".events.n", fmt("%zu", run.events.size())); + for (const auto& [k, n] : counts) field(fmt("%s.events.kind.%02u", p.c_str(), k), fmt("%d", n)); + field(p + ".events.digest", fmt("%llu", (ull)ed)); + field(p + ".counts", fmt("inputs:%d;subbars:%d;begins:%d", h.inputs, h.sub_bars, h.begins)); + const auto& st = run.state; + field(p + ".final", + fmt("hash:%llu;cont:%llu;shash:%llu;life:%d;phase:%d;comp:%d;fail:%d/%d;floor:%lld;hw:%llu;drive:%d", + (ull)run.scalar, (ull)run.continuation, (ull)run.stream_hash, static_cast(st.kind), + static_cast(st.phase), static_cast(st.completion), static_cast(st.failure.code), + static_cast(st.failure.operation), (ll)st.decision_floor_ms, (ull)st.consumed_high_water, + run.drive_ok ? 1 : 0)); + if (h.preview) { + const auto& pv = *h.preview; + xfield("C-SURFACE-2:inspect_current_execution", p + ".preview.E6", + fmt("refusal:%d;ready:%d;ticket:%s;rows:%zu", pv.refusal ? static_cast(*pv.refusal) : -1, + pv.settlement_readiness ? static_cast(*pv.settlement_readiness) : -1, + D(pv.account.current_ticket).c_str(), pv.closed_row_pnl.size())); + } +} + +// ============================================================ helpers over a run +const FillRec* fill_of(const AcidHost& h, const std::optional& handle, int nth = 0) { + if (!handle) return nullptr; + for (const auto& f : h.fills) + if (f.ev.handle() == *handle && nth-- == 0) return &f; + return nullptr; +} +const std::vector* lots_at(const AcidHost& h, const std::string& tag) { + for (const auto& [t, l] : h.lots) + if (t == tag) return &l; + return nullptr; +} +std::optional liq_at(const AcidHost& h, const std::string& tag) { + for (const auto& [t, l] : h.liq) + if (t == tag) return l; + return std::nullopt; +} +const NativeRiskState* risk_at(const AcidHost& h, const std::string& tag) { + for (const auto& [t, r] : h.risks) + if (t == tag) return &r; + return nullptr; +} +std::vector commands(const Run& r) { + std::vector out; + for (const auto& e : r.events) + if (e.kind == NativeEventKind::Command && e.command) out.push_back(&*e.command); + return out; +} +template std::vector of(const Run& r) { + std::vector out; + for (const auto* c : commands(r)) + if (const auto* t = std::get_if(c)) out.push_back(t); + return out; +} +int trade_index_by_incarnation(const Run& r, uint64_t inc, int nth = 0) { + for (int i = 0; i < r.report.trades_len; ++i) + if (r.host->get_report_trade(i).entry_incarnation == inc && nth-- == 0) return i; + return -1; +} + +// ============================================================ the feature checks +void check_run_features(const Run& run) { + const AcidHost& h = *run.host; + const ReportC& rep = run.report; + const std::string m = run.opt.tag; + // The magnified run exists for F13 (and F14's report identities): a + // lower_tf path moves fills onto sub-bar points, so the scenario's + // hand-derived prices are the unmagnified run's and are not re-asserted. + const bool scenario = !run.opt.magnify; + auto C = [&](const char* id, const char* name, bool ok, const std::string& what) { + if (!scenario && std::strcmp(id, "F13") != 0 && std::strcmp(id, "F14") != 0) return; + fcheck(id, name, ok, m + ": " + what); + }; + // ---------------- F01 Sized entry with a fee reserve + { + const char* id = "F01"; + const char* nm = "sized-entry-with-fee-reserve"; + const double px = buy_fill(g_b15[G_E1FILL].open); // 200.10 -> 200.25 + 0.25 + const double units = floor_grid((ACID_E1_CASH / (1.0 + ACID_FEE_PCT / 100.0)) / + (px * ACID_POINT_VALUE * ACID_ACCOUNT_FX)); + const double units_plain = floor_grid(ACID_E1_CASH / (px * ACID_POINT_VALUE * ACID_ACCOUNT_FX)); + const FillRec* f = fill_of(h, h.h_e1); + C(id, nm, f != nullptr, "E1 filled"); + if (f) { + C(id, nm, same(f->ev.resolved_price, px), fmt("E1 price %s == hand %s", D(f->ev.resolved_price).c_str(), D(px).c_str())); + C(id, nm, f->gi == G_E1FILL, "E1 fills on bar 1 (AtMatch, next eligible point)"); + C(id, nm, same(f->ev.opened_units, units), fmt("E1 units %s == hand floor_0.01(24000/1.001/(price*5*1.2)) %s", + D(f->ev.opened_units).c_str(), D(units).c_str())); + } + C(id, nm, units < units_plain, fmt("the reserve costs units: %s < %s", D(units).c_str(), D(units_plain).c_str())); + C(id, nm, !h.sized.empty() && same(h.sized[0].second, units), "native_sized_units(reserve) == the fill"); + C(id, nm, h.sized.size() > 1 && same(h.sized[1].second, units_plain), "native_sized_units(no reserve) == hand"); + const auto* lots = lots_at(h, "E1FILL"); + C(id, nm, lots && lots->size() == 1 && near_rel((*lots)[0].entry_commission, fee(units, px, ACID_ACCOUNT_FX), 1e-12), + "E1's lot carries the percent fee of its fill"); + } + // ---------------- F02 bracket with anchored legs on a price grid + { + const char* id = "F02"; + const char* nm = "bracket-anchored-legs-price-grid"; + C(id, nm, h.bracket.every_requested_leg_accepted() && h.h_tp && h.h_sl, "both legs accepted"); + const auto* w0 = [&]() -> const std::vector* { + for (const auto& [t, v] : h.working) if (t == "E1") return &v; + return nullptr; }(); + C(id, nm, w0 && w0->size() == 1, "PendingUntilArmed: before the fill only E1 is a working row"); + const double fill = buy_fill(g_b15[G_E1FILL].open); + const double tp = ceil_tick(fill + ACID_TP_OFFSET); // a sell limit rounds up + const double sl = floor_tick(fill + ACID_SL_OFFSET); // a sell stop rounds down + C(id, nm, h.anchors.size() == 2, "two anchored materializations"); + if (h.anchors.size() == 2) { + const auto& a = h.anchors[0].v; + const auto& b = h.anchors[1].v; + C(id, nm, same(a.kernel_level, tp) && a.trigger == NativeAnchoredTrigger::Limit + && a.leg_side == no::Side::Short && same(a.owner_fill_price, fill), + fmt("TP level %s == ceil_tick(%s + 3.1) %s", D(a.kernel_level).c_str(), D(fill).c_str(), D(tp).c_str())); + C(id, nm, same(b.kernel_level, sl) && b.trigger == NativeAnchoredTrigger::Stop, + fmt("SL level %s == floor_tick(%s - 2.3) %s", D(b.kernel_level).c_str(), D(fill).c_str(), D(sl).c_str())); + } + const auto* w1 = [&]() -> const std::vector* { + for (const auto& [t, v] : h.working) if (t == "E1FILL") return &v; + return nullptr; }(); + bool tp_ok = false, sl_ok = false; + if (w1) { + for (const auto& row : *w1) { + const auto& d = *row.definition; + const bool abs = std::holds_alternative(d.request.anchor); + if (h.h_tp && d.handle == *h.h_tp) + if (const auto* l = std::get_if(&d.request.trigger)) tp_ok = abs && same(l->price, tp); + if (h.h_sl && d.handle == *h.h_sl) + if (const auto* s = std::get_if(&d.request.trigger)) sl_ok = abs && same(s->price, sl); + } + } + C(id, nm, tp_ok && sl_ok, "after the arm the legs read Absolute at the installed levels"); + const FillRec* t = fill_of(h, h.h_tp); + C(id, nm, t && same(t->ev.resolved_price, tp) && t->gi == G_TP, "TP fills AT its level (slippage capped by the limit) on bar 5"); + const int ti = h.h_e1 ? trade_index_by_incarnation(run, h.h_e1->incarnation) : -1; + C(id, nm, ti >= 0 && h.closed_trade_close_cause(ti) == 2, "the TP row's close cause is BRACKET (2)"); + bool sl_group = false; + for (const auto* c : of(run)) + if (h.h_sl && c->handle() == *h.h_sl) sl_group = c->reason == no::CancelReason::Group; + C(id, nm, sl_group, "the SL leaves the book as the TP's OCA sibling (CancelReason::Group)"); + } + // ---------------- F04 trail in ticks + { + const char* id = "F04"; + const char* nm = "trail-in-ticks"; + const auto* w = [&]() -> const std::vector* { + for (const auto& [t, v] : h.working) if (t == "E3") return &v; + return nullptr; }(); + bool resolved = false; + if (w) + for (const auto& row : *w) + if (h.h_t3 && row.definition->handle == *h.h_t3) + if (const auto* tr = std::get_if(&row.definition->request.trigger)) + resolved = same(tr->offset, ACID_T3_TICKS * ACID_TICK) && !tr->ticks; + C(id, nm, resolved, "TrailTicks{6} resolved at acceptance to the price distance 1.5, spelling cleared"); + // The running best is the favourable enclosing tick of each print (a sell + // trail: ceil), the level best - 6 ticks (native-engine.md:384-387, :900-915). + const double best[3] = {ceil_tick(g_b15[G_E3FILL].high), ceil_tick(g_b15[G_E3FILL + 1].high), + ceil_tick(g_b15[G_E3FILL + 2].high)}; + for (int k = 0; k < 3; ++k) { + const int gi = G_E3FILL + k; + std::optional ts; + for (const auto& [g, s] : h.trails) if (g == gi) ts = s; + C(id, nm, ts && ts->activated && same(ts->best_price, best[k]) && same(ts->current_level, best[k] - 1.5), + fmt("bar %d trail best %s level %s", gi, ts ? D(ts->best_price).c_str() : "-", + ts ? D(ts->current_level).c_str() : "-")); + } + const FillRec* f = fill_of(h, h.h_t3); + const double level = best[2] - 1.5; + C(id, nm, f && f->gi == G_T3X && same(f->ev.resolved_price, floor_tick(level) - ACID_TICK), + fmt("the trail exits on bar %d at level %s less one tick", G_T3X, D(level).c_str())); + const int ti = h.h_e3 ? trade_index_by_incarnation(run, h.h_e3->incarnation) : -1; + C(id, nm, ti >= 0 && h.closed_trade_close_cause(ti) == 2, "the trail row's close cause is BRACKET (2)"); + } + // ---------------- F05 maintenance-only margin, kernel liquidation via open lots + { + const char* id = "F05"; + const char* nm = "maintenance-only-margin-kernel-liquidation-open-lots"; + const FillRec* a = fill_of(h, h.h_e4a); + const FillRec* b = fill_of(h, h.h_e4b); + C(id, nm, a && b, "the two short lots filled (no opening gate on the maintenance-only side)"); + if (a && b) { + C(id, nm, same(a->ev.resolved_price, sell_fill(g_b15[G_E4B].open)) + && same(b->ev.resolved_price, sell_fill(g_b15[G_E4BFILL].open)), + "E4a/E4b fill at floor_tick(open) - 1 tick"); + // L = (K + S pv fx)/(Q pv fx (1 + m)), K = the marked-equity intercept + // (native-engine.md:1201-1205); the lots' own booking gives K and S. + const auto* lots = lots_at(h, "E4BFILL"); + if (lots && lots->size() == 2) { + double realized_balance = ACID_CAPITAL; + for (const auto& f : h.fills) + if (f.gi < G_E4A) (void)f; // realized sums come from the closed rows below + for (int i = 0; i < rep.trades_len; ++i) + if (rep.trades[i].exit_time < acid_day_ms(2)) realized_balance += rep.trades[i].pnl; + double open_fees = 0, S = 0, Q = 0; + for (const auto& l : *lots) { + open_fees += l.entry_commission; + S += -l.signed_units * l.entry_price; + Q += -l.signed_units; + } + const double K = realized_balance - open_fees; + auto L = [&](double fx) { + return (K + S * ACID_POINT_VALUE * fx) / (Q * ACID_POINT_VALUE * fx * (1.0 + ACID_MAINT_SHORT)); + }; + const auto l12 = liq_at(h, "E4BFILL"); + const auto lpre = liq_at(h, "PRESTEP"); + const auto l125 = liq_at(h, "STEP"); + C(id, nm, lpre && near_rel(*lpre, L(ACID_FX_R0), 1e-12), + fmt("L at 10:45, before the 11:00 step, still at 1.20: %s", D(lpre.value_or(NAN)).c_str())); + C(id, nm, l12 && near_rel(*l12, L(ACID_FX_R0), 1e-12), + fmt("L(1.20) %s == hand %s", D(l12.value_or(NAN)).c_str(), D(L(ACID_FX_R0)).c_str())); + C(id, nm, l125 && near_rel(*l125, L(ACID_FX_R1), 1e-12), + fmt("L(1.25) %s == hand %s", D(l125.value_or(NAN)).c_str(), D(L(ACID_FX_R1)).c_str())); + const double H = g_b15[G_SPIKE].high; + C(id, nm, L(ACID_FX_R1) < H && H < L(ACID_FX_R0), + "the spike crosses L at the new rate only: the step is what liquidates"); + // RestoreMinimum at the adverse mark H (native-engine.md:1284-1294). + const double fx = ACID_FX_R1; + const double eqH = K + (S - Q * H) * ACID_POINT_VALUE * fx; + const double reqH = Q * H * ACID_POINT_VALUE * fx * ACID_MAINT_SHORT; + const double u = floor_grid((reqH - eqH) / (H * ACID_POINT_VALUE * fx * ACID_MAINT_SHORT)); + C(id, nm, h.calls.size() == 1, "exactly one kernel liquidation"); + C(id, nm, h.callviews.size() == 1 && same(h.callviews[0].mark, H), + "the call is sized at the path's adverse extreme (the spike high)"); + if (h.calls.size() == 1) { + const auto& c = h.calls[0]; + C(id, nm, same(c.units, u), fmt("RestoreMinimum units %s == hand %s", D(c.units).c_str(), D(u).c_str())); + C(id, nm, same(c.mark, ceil_tick(L(fx)) + ACID_TICK), + fmt("fills at ceil_tick(L) + 1 tick: %s", D(c.mark).c_str())); + C(id, nm, c.handle().incarnation != 0 && c.definition->origin == no::RequestOrigin::KernelLiquidation, + "origin KernelLiquidation"); + C(id, nm, same(c.position_before, -Q) && same(c.position_after, -(Q - u)), "book before/after"); + } + // The open-lots reader: [E4a, E4b] before; E4b alone, shrunk, same incarnation, after. + const auto& lb = h.lots_before_call; + const auto& la = h.lots_after_call; + C(id, nm, lb.size() == 2 && la.size() == 1 && h.h_e4b + && la[0].entry_incarnation == h.h_e4b->incarnation + && same(la[0].signed_units, -(Q - u)), + "open lots: FIFO across lots, the survivor keeps E4b's incarnation"); + int liq_rows = 0; + for (int i = 0; i < rep.trades_len; ++i) { + const Trade& row = h.get_report_trade(i); + if (row.exit_id == ACID_LIQ_LABEL && row.exit_comment == ACID_LIQ_COMMENT + && h.closed_trade_close_cause(i) == 3) + ++liq_rows; + } + C(id, nm, liq_rows == 2, "two closed rows under the broker ticket, cause MARGIN_CALL (3)"); + } + } + } + // ---------------- F06 FX curve with a step inside the run + { + const char* id = "F06"; + const char* nm = "fx-curve-step"; + int rolls = 0; + const NativeMarginCheckPoint* roll = nullptr; + for (const auto& c : h.checks) + if (c.p.kind == NativeMarginCheckKind::FxRoll) { + ++rolls; + roll = &c.p; + } + C(id, nm, rolls == 1, fmt("one FxRoll point for one step (the restating 10:00 point is none): %d", rolls)); + C(id, nm, roll && roll->cursor.point.effective_time_ms == ACID_FX_T1, "the roll is taken at the step instant"); + const ReqRec* rv = nullptr; + for (const auto& r : h.reqs) + if (r.v.kind == NativeMarginCheckKind::FxRoll) rv = &r; + C(id, nm, rv && near_rel(rv->v.required, + -rv->v.position.signed_units * rv->v.mark * ACID_POINT_VALUE * ACID_FX_R1 * ACID_MAINT_SHORT, 1e-12), + "the roll's requirement converts at the new rate"); + const FillRec* xf = fill_of(h, h.h_xf); + const FillRec* e4b = fill_of(h, h.h_e4b); + C(id, nm, xf && near_rel(xf->ev.current_ticket, fee(xf->ev.closed_units, xf->ev.resolved_price, ACID_FX_R1), 1e-12), + "a fill after the step pays its fee at 1.25"); + C(id, nm, e4b && near_rel(e4b->ev.current_ticket, fee(e4b->ev.opened_units, e4b->ev.resolved_price, ACID_FX_R0), 1e-12), + "a fill before the step pays its fee at 1.20"); + } + // ---------------- F07 risk limit, and the risk refusal + { + const char* id = "F07"; + const char* nm = "risk-limit-and-refusal"; + const auto risk_events = of(run); + C(id, nm, risk_events.size() == 1, fmt("one breach recorded: %zu", risk_events.size())); + if (risk_events.size() == 1 && h.calls.size() == 1) { + const auto& e = *risk_events[0]; + const auto* at_spike = risk_at(h, "SPIKE"); + C(id, nm, e.kind == no::RiskLimitKind::MaxIntradayLoss && same(e.limit, ACID_RISK_LOSS) + && e.day_ordinal == ACID_DAY0_ORDINAL + 2, + "MaxIntradayLoss, limit 2500, Wednesday's session day"); + C(id, nm, at_spike && near_rel(e.observed, at_spike->day_open_equity - h.calls[0].equity, 1e-12), + fmt("observed %s == day-open equity - equity at the liquidation fill", D(e.observed).c_str())); + C(id, nm, e.cursor.point.ordinal == h.calls[0].cursor.point.ordinal, "measured at the liquidation's drain"); + } + const auto* sp = risk_at(h, "SPIKE"); + C(id, nm, sp && sp->blocked && sp->reason && *sp->reason == no::RiskLimitKind::MaxIntradayLoss, + "blocked at the spike's close"); + bool refused = false; + for (const auto* mr : of(run)) + if (h.h_e5 && mr->handle() == *h.h_e5) + refused = mr->reason == no::MatchRejectReason::RiskLimit && mr->cursor.point.interval_index == G_E5REJ; + C(id, nm, refused, "RISK REFUSAL: the opening E5 is MatchRejected(RiskLimit) at the next open"); + C(id, nm, fill_of(h, h.h_e5) == nullptr, "E5 never fills"); + const FillRec* xf = fill_of(h, h.h_xf); + C(id, nm, xf && xf->gi == G_XFFILL, "a reduction (the flatten) still fills while blocked"); + const auto* d4 = risk_at(h, "D4"); + C(id, nm, d4 && !d4->blocked && d4->day_ordinal == ACID_DAY0_ORDINAL + 3, "Thursday opens unblocked"); + C(id, nm, fill_of(h, h.h_d4[0]) != nullptr, "and its opening fills"); + } + // ---------------- F08 calc-on-fills + { + const char* id = "F08"; + const char* nm = "calc-on-fills"; + const std::size_t fills = h.fills.size(); + C(id, nm, h.native_recalculation_count() + h.native_recalculations_skipped() == fills, + fmt("one OrderFill recalculation per applied execution: %llu + %llu == %zu", + (ull)h.native_recalculation_count(), (ull)h.native_recalculations_skipped(), fills)); + C(id, nm, h.native_recalculations_skipped() == 2, + "four fills at Thursday's open, a budget of 2: two dropped"); + C(id, nm, h.recalcs.size() == h.native_recalculation_count(), "every driven recalculation reached the host"); + bool causes_ok = true; + std::set fill_ords; + for (const auto& f : h.fills) fill_ords.insert(f.ev.ordinal); + for (const auto& r : h.recalcs) causes_ok = causes_ok && fill_ords.count(r.cause_ord) == 1; + C(id, nm, causes_ok, "each recalculation's cause is an applied execution"); + const FillRec* tp = fill_of(h, h.h_tp); + const FillRec* e2 = fill_of(h, h.h_e2); + C(id, nm, tp && e2 && e2->gi == G_E2FILL, "E2, born in the take-profit's recalculation, fills at the next open"); + } + // ---------------- F09 60-minute series + auxiliary feed + interval query + { + const char* id = "F09"; + const char* nm = "htf-subscription-aux-feed-interval"; + int n0 = 0, n1 = 0, bad = 0, pulls = 0; + for (const auto& t : h.tfs) { + const bool hour = t.sub == 0; + (hour ? n0 : n1)++; + pulls += t.pulled ? 1 : 0; + const int64_t span = (hour ? 60 : 30) * ACID_MINUTE_MS; + // Hand aggregation over [open, open + span) of the input (hourly) or feed (half-hour). + const std::vector& src = hour ? g_b15 : g_b5; + double o = NAN, hi = -1e300, lo = 1e300, c = NAN, v = 0; + int64_t last_ts = 0; + for (const auto& b : src) { + if (b.timestamp < t.bar.timestamp || b.timestamp >= t.bar.timestamp + span) continue; + if (o != o) o = b.open; + hi = std::max(hi, b.high); + lo = std::min(lo, b.low); + c = b.close; + v += b.volume; + last_ts = b.timestamp; + } + // The input bar the bucket rides on: the one whose period holds its last contributing bar. + const int64_t carrier = last_ts - (last_ts - acid_day_ms(0)) % (15 * ACID_MINUTE_MS); + const bool ok = same(t.bar.open, o) && same(t.bar.high, hi) && same(t.bar.low, lo) + && same(t.bar.close, c) && same(t.bar.volume, v) + && t.completion == NativeCompletionKind::Confirmed && t.delivered_at == carrier + && t.iv.open_ms == t.bar.timestamp && t.iv.next_period_open_ms == t.bar.timestamp + span; + if (!ok) ++bad; + } + const int bars = run.opt.bars; + const int hours = (bars / ACID_SLOTS) * 6 + (bars % ACID_SLOTS) / 4; + const int halves = (bars / ACID_SLOTS) * 12 + (bars % ACID_SLOTS) / 2; + C(id, nm, n0 == hours, fmt("\"60\" over the input: %d buckets (hand %d)", n0, hours)); + C(id, nm, n1 == halves, fmt("\"30\" from the \"5\" auxiliary feed: %d buckets (hand %d)", n1, halves)); + C(id, nm, bad == 0, fmt("every bucket is the hand aggregate, its interval and carrier the hand grid (%d bad)", bad)); + C(id, nm, pulls == static_cast(h.tfs.size()), "native_series_bar answers the delivered bucket inside the callback"); + } + // ---------------- F10 session-day first/last flags + { + const char* id = "F10"; + const char* nm = "session-day-flags"; + int bad = 0; + for (std::size_t i = 0; i < h.bars.size(); ++i) { + const auto& c = h.bars[i].ctx; + const int gi = static_cast(i); + const bool last_of_run = gi == run.opt.bars - 1; + const bool first_of_day = acid_slot_of(gi) == 0; + const bool last_of_day = acid_slot_of(gi) == ACID_SLOTS - 1; + // A batch's final bar closes its day (complete input); a stream reads + // the calendar there (market_driver.hpp:143-147). + const bool closes = last_of_day || (last_of_run && !run.opt.stream); + const bool ok = c.in_session && c.opens_session_day == first_of_day && c.closes_session_day == closes + && c.closes_session_day_open_ended == last_of_day + && h.bars[i].sd == ACID_DAY0_ORDINAL + acid_day_of(gi); + if (!ok) ++bad; + } + C(id, nm, bad == 0, fmt("opens on 09:00, closes on 14:45 (and a batch's final bar), day ordinals (%d bad)", bad)); + } + // ---------------- F11 partial close across lots + exact-sum close + { + const char* id = "F11"; + const char* nm = "fifo-partial-across-lots-and-exact-sum"; + const auto* d4 = lots_at(h, "D4FILL"); + C(id, nm, d4 && d4->size() == 4, "four Thursday lots"); + const FillRec* p1 = fill_of(h, h.h_p1); + C(id, nm, p1 && same(p1->ev.closed_units, 1.5) && p1->ev.closed_trade_count == 2, "P1 closes 1.5 in two rows"); + const double r1 = 1.5 - 1.1; // 0.3999999999999999 + const double keep7 = 0.7 - r1; // 0.30000000000000004 + if (p1 && p1->ev.closed_trade_count == 2) { + const Trade& a = h.get_report_trade(static_cast(p1->ev.first_trade_index)); + const Trade& b = h.get_report_trade(static_cast(p1->ev.first_trade_index + 1)); + C(id, nm, same(a.qty, 1.1) && same(b.qty, r1), fmt("P1 rows %s + %s", D(a.qty).c_str(), D(b.qty).c_str())); + } + const auto* after1 = lots_at(h, "P1FILL"); + C(id, nm, after1 && after1->size() == 3 && same((*after1)[0].signed_units, keep7), + fmt("E7 keeps %s", after1 && !after1->empty() ? D((*after1)[0].signed_units).c_str() : "-")); + const double U = keep7 + 0.2; // fl(l0 + l1) == 0.5 + C(id, nm, same(h.p2_units, U) && same(U, 0.5), fmt("P2 = fl(l0 + l1) = %s", D(h.p2_units).c_str())); + C(id, nm, (U - keep7) < 0.2, "non-vacuous: fl(U - l0) < l1, the case that used to leave a dust lot (K-ULP3)"); + const FillRec* p2 = fill_of(h, h.h_p2); + C(id, nm, p2 && same(p2->ev.closed_units, U) && p2->ev.closed_trade_count == 2, "P2 closes U in two rows"); + if (p2 && p2->ev.closed_trade_count == 2) { + const Trade& a = h.get_report_trade(static_cast(p2->ev.first_trade_index)); + const Trade& b = h.get_report_trade(static_cast(p2->ev.first_trade_index + 1)); + C(id, nm, same(a.qty, keep7) && same(b.qty, 0.2), "P2's rows are the two whole lots"); + } + const auto* after2 = lots_at(h, "P2FILL"); + C(id, nm, after2 && after2->size() == 1 && h.h_d4[3] && (*after2)[0].entry_incarnation == h.h_d4[3]->incarnation + && same((*after2)[0].signed_units, 0.1), + "EXACT SUM: only E9 (0.1) is left, no dust lot"); + } + // ---------------- F12 typed refusal path + { + const char* id = "F12"; + const char* nm = "typed-refusal-path"; + const std::map want = { + {"R01.transact_zero", "Rejected/InvalidQuantity"}, + {"R02.off_grid", "Rejected/OffGrid"}, + {"R03.bad_basis", "Rejected/InvalidQuantityBasis"}, + {"R04.placement", "Rejected/PlacementAdmission"}, + {"R05.declare_outside_begin", fmt("Failed/%d/%d", static_cast(NativeRunSpecError::WrongPhase), + static_cast(NativeRunSpecField::None))}, + {"R06.fx_curve_while_running", fmt("Failed/%d/0", static_cast(NativeFxCurveError::WrongPhase))}, + {"R07.written_anchor", "Rejected/InvalidTrigger"}, + {"R08.sized_child", "Rejected/InvalidOwner"}, + {"R09.replace_terminal", "NotWorking"}, + {"R10.cancel_cancelled_leg", "NotWorking"}, + }; + int seen = 0; + for (const auto& rf : h.refusals) { + const auto it = want.find(rf.id); + C(id, nm, it != want.end() && it->second == rf.word && rf.unchanged, + fmt("%s: %s (want %s), unchanged %d", rf.id.c_str(), rf.word.c_str(), + it != want.end() ? it->second.c_str() : "?", rf.unchanged ? 1 : 0)); + ++seen; + } + C(id, nm, seen == static_cast(want.size()), fmt("%d refusals issued", seen)); + C(id, nm, run.bad_curve.status == NativeSetupStatus::Failed + && run.bad_curve.validation.error == NativeFxCurveError::NotStrictlyIncreasing + && run.bad_curve.validation.index == 1 && run.good_curve.status == NativeSetupStatus::Applied, + "a curve refused while Ready: NotStrictlyIncreasing at 1; the good curve then applies"); + for (const auto& [aid, a] : run.appends) { + const bool ok = a.status == NativeSetupStatus::Failed + && ((aid == "after_batch" || aid == "after_end") ? a.error == NativeAuxiliaryAppendError::NotRealtime + : a.error == NativeAuxiliaryAppendError::UnorderedBars); + C(id, nm, ok, fmt("append %s: %s/%zu", aid.c_str(), append_word(a.error), a.index)); + } + } + // ---------------- F13 magnifier counters (the magnified run; zero elsewhere) + { + const char* id = "F13"; + const char* nm = "magnifier-counters-bare-host"; + if (run.opt.magnify) { + const int bars = run.opt.bars; + C(id, nm, rep.bar_magnifier_enabled == 1, "the report says magnified"); + C(id, nm, rep.magnifier_sub_bars_total == 3 * bars, fmt("sub-bars %lld == 3 x %d", (ll)rep.magnifier_sub_bars_total, bars)); + C(id, nm, rep.magnifier_sample_ticks_total == 4 * 3 * bars, + fmt("sample ticks %lld == 4 x sub-bars", (ll)rep.magnifier_sample_ticks_total)); + C(id, nm, h.sub_bars == 3 * bars, fmt("on_native_sub_bar %d == sub-bars", h.sub_bars)); + const auto& s = h.bars.empty() ? NativeDriverStatistics{} : h.bars.back().ctx.driver_statistics; + C(id, nm, s.intrabar_path_enabled && s.sub_bars_per_script_bar == 3 && s.samples_per_sub_bar == 4, + "driver statistics: 3 sub-bars x 4 samples per script bar"); + C(id, nm, run.state.kind == NativeLifecycleKind::Completed, "the magnified run completes"); + } else { + C(id, nm, rep.bar_magnifier_enabled == 0 && rep.magnifier_sub_bars_total == 0 + && rep.magnifier_sample_ticks_total == 0 && h.sub_bars == 0, + "an unmagnified run reports no magnifier"); + } + } + // ---------------- F14 KernelRecorded report + per-bar broker-state hashes + { + const char* id = "F14"; + const char* nm = "kernel-recorded-report-per-bar-hashes"; + const ll n = run.opt.bars; + C(id, nm, rep.broker_state_hash_len == n && rep.equity_curve_len == n && rep.script_bars_processed == n, + fmt("len identity %lld == %lld == %lld == %lld", (ll)rep.broker_state_hash_len, (ll)rep.equity_curve_len, + (ll)rep.script_bars_processed, n)); + C(id, nm, rep.broker_state_hash_len > 0 && rep.broker_state_hash[rep.broker_state_hash_len - 1] == run.scalar, + "the last row == broker_state_hash() (pineforge.h:447-452)"); + C(id, nm, run.state.kind == NativeLifecycleKind::Completed && run.drive_ok, "the run completes"); + if (rep.equity_curve_len == n) { + const auto& last = rep.equity_curve[n - 1]; + C(id, nm, near_rel(last.equity, ACID_CAPITAL + h.net() + last.open_profit, 1e-12), + "equity point = capital + net profit + open profit (pineforge.h:435-437)"); + } + if (!run.opt.magnify && n == ACID_BARS) { + const int last = rep.trades_len - 1; + const bool ok = last >= 0 && rep.trades[last].open_at_end == 1 && h.closed_trade_close_cause(last) == 6 + && same(rep.trades[last].exit_price, nearest_tick(g_b15[G_LAST].close)) + && same(rep.trades[last].qty, 0.1); + C(id, nm, ok, "the still-open E9 is the range-end row: the last close on the nearest tick, no slippage, cause 6"); + } + } +} + +// F03: keep_binding against the plain twin, within one driving mode. +void check_keep(const Run& keep, const Run& plain) { + const char* id = "F03"; + const char* nm = "reissued-exit-keep-binding"; + const std::string m = keep.opt.stream ? "stream" : "batch"; + auto C = [&](bool ok, const std::string& what) { fcheck(id, nm, ok, m + ": " + what); }; + auto replaced = [](const Run& r) { + int n = 0; + for (const auto& x : r.host->x_replaces) n += x.status == no::ReplaceStatus::Replaced; + return n; + }; + C(replaced(keep) == 8 && replaced(plain) == 8, "eight re-issues, all Replaced, in both"); + // The X chain: every handle it ever had. + auto chain = [](const Run& r) { + std::set s; + for (const auto* a : of(r)) + if (a->request().label == "X") s.insert(a->handle().incarnation); + for (const auto* rp : of(r)) + if (rp->successor_request().label == "X") s.insert(rp->successor().incarnation); + return s; + }; + auto bound = [&](const Run& r) { + const auto c = chain(r); + int n = 0; + for (const auto* b : of(r)) n += c.count(b->definition->handle.incarnation) ? 1 : 0; + return n; + }; + const int kb = bound(keep), pb = bound(plain); + C(pb == 9 && kb == 1, fmt("CloseBoundEvents on the X chain: plain %d (one per successor), keep %d", pb, kb)); + int kept = 0; + for (const auto* rp : of(keep)) kept += rp->successor_definition->kept_binding ? 1 : 0; + int plain_kept = 0; + for (const auto* rp : of(plain)) plain_kept += rp->successor_definition->kept_binding ? 1 : 0; + C(kept == 8 && plain_kept == 0, fmt("successors carrying kept_binding: keep %d, plain %d", kept, plain_kept)); + // Every other event, ordinal for ordinal (native-engine.md:931-935: "taking + // the timeline ordinal the event would have taken"). + auto others = [](const Run& r) { + std::vector> v; + for (const auto& e : r.events) { + const auto pe = project(e); + if (pe.kind == PF_NATIVE_EVENT_CLOSE_BOUND) continue; + v.emplace_back(pe.kind, pe.ordinal); + } + return v; + }; + const auto ko = others(keep), po = others(plain); + std::size_t drop = 0; + { // plain's extra CloseBound events are the only difference + std::size_t i = 0, j = 0; + while (i < ko.size() && j < po.size()) { + if (ko[i] == po[j]) { ++i; ++j; } else { ++drop; ++i; } + } + drop += (ko.size() - i) + (po.size() - j); + } + C(drop == 0 && ko.size() == po.size(), fmt("all %zu non-CloseBound events equal, kind and ordinal", ko.size())); + bool fills_equal = keep.host->fills.size() == plain.host->fills.size(); + for (std::size_t i = 0; fills_equal && i < keep.host->fills.size(); ++i) { + const auto& a = keep.host->fills[i].ev; + const auto& b = plain.host->fills[i].ev; + fills_equal = a.ordinal == b.ordinal && same(a.resolved_price, b.resolved_price) + && same(a.closed_units, b.closed_units) && same(a.opened_units, b.opened_units); + } + C(fills_equal, "the option changes the events, never what matches: every fill equal"); + const FillRec* xf = nullptr; + for (const auto& f : keep.host->fills) if (f.label == "X") xf = &f; + C(xf && xf->gi == G_XFILL && same(xf->ev.resolved_price, floor_tick(ACID_X_LEVEL0 + 8 * ACID_TICK) - ACID_TICK) + && same(xf->ev.closed_units, ACID_E2_UNITS), + "X (re-issued to 203.00) flattens E2 on bar 15 at 203.00 - 1 tick"); + int first_diff = -1, equal_rows = 0; + const ll n = std::min(keep.report.broker_state_hash_len, plain.report.broker_state_hash_len); + for (ll i = 0; i < n; ++i) { + if (keep.report.broker_state_hash[i] == plain.report.broker_state_hash[i]) ++equal_rows; + else if (first_diff < 0) first_diff = static_cast(i); + } + xfield("C-SURFACE-2:replace-options", m + ".keep_binding", + fmt("replaced:%d;closebound_plain:%d;closebound_keep:%d;kept:%d;events_equal:%zu;" + "hash_rows_equal:%d/%lld;first_diff_row:%d;keep_final_hash:%llu", + replaced(keep), pb, kb, kept, ko.size(), equal_rows, n, first_diff, (ull)keep.scalar)); +} + +// ============================================================ driving mode +void driving_section(const Run& b, const Run& s, const Run& cut) { + auto D1 = [&](const char* cls, const char* what, bool ok, const std::string& detail, const char* cite) { + out(fmt("DRIVING %s %s %s: %s [%s]", ok ? "PASS" : "FAIL", cls, what, detail.c_str(), cite)); + fcheck("DM", "driving-mode-accounting", ok, fmt("%s %s: %s", cls, what, detail.c_str())); + }; + const AcidHost& hb = *b.host; + const AcidHost& hs = *s.host; + // MUST_EQUAL: the outcome is the batch<->stream oracle. + auto same_trade = [](const TradeC& x, const TradeC& y) { + return x.entry_time == y.entry_time && x.exit_time == y.exit_time && same(x.entry_price, y.entry_price) + && same(x.exit_price, y.exit_price) && same(x.pnl, y.pnl) && same(x.pnl_pct, y.pnl_pct) + && x.is_long == y.is_long && same(x.max_runup, y.max_runup) && same(x.max_drawdown, y.max_drawdown) + && same(x.qty, y.qty) && same(x.commission, y.commission) && x.entry_bar_index == y.entry_bar_index + && x.exit_bar_index == y.exit_bar_index && x.open_at_end == y.open_at_end; + }; + bool trades = b.report.trades_len == s.report.trades_len; + for (int i = 0; trades && i < b.report.trades_len; ++i) trades = same_trade(b.report.trades[i], s.report.trades[i]); + D1("MUST_EQUAL", "closed+range-end rows", trades, fmt("%d rows, byte-identical", b.report.trades_len), + "pineforge.h:1010-1011; native-engine.md:2039-2045"); + bool fills = hb.fills.size() == hs.fills.size(); + for (std::size_t i = 0; fills && i < hb.fills.size(); ++i) { + const auto& x = hb.fills[i].ev; + const auto& y = hs.fills[i].ev; + fills = x.ordinal == y.ordinal && same(x.resolved_price, y.resolved_price) && same(x.closed_units, y.closed_units) + && same(x.opened_units, y.opened_units) && same(x.current_ticket, y.current_ticket) + && x.cursor.point.effective_time_ms == y.cursor.point.effective_time_ms; + } + D1("MUST_EQUAL", "fills", fills, fmt("%zu fills: ordinal, price, units, ticket, time", hb.fills.size()), + "pineforge.h:1010-1011 (the trades, the position and the equity)"); + bool curve = b.report.equity_curve_len == s.report.equity_curve_len; + for (ll i = 0; curve && i < b.report.equity_curve_len; ++i) + curve = same(b.report.equity_curve[i].equity, s.report.equity_curve[i].equity) + && b.report.equity_curve[i].time_ms == s.report.equity_curve[i].time_ms; + D1("MUST_EQUAL", "equity curve", curve, fmt("%lld points", (ll)b.report.equity_curve_len), "pineforge.h:1010-1011"); + bool tfs = hb.tfs.size() == hs.tfs.size(); + for (std::size_t i = 0; tfs && i < hb.tfs.size(); ++i) { + const auto& x = hb.tfs[i]; + const auto& y = hs.tfs[i]; + tfs = x.sub == y.sub && std::memcmp(&x.bar, &y.bar, sizeof(Bar)) == 0 && x.delivered_at == y.delivered_at + && x.completion == y.completion && x.gi == y.gi && x.iv.open_ms == y.iv.open_ms + && x.iv.next_period_open_ms == y.iv.next_period_open_ms; + } + D1("MUST_EQUAL", "series deliveries (the aux feed appended live)", tfs, + fmt("%zu buckets, bucket for bucket and delivery point for delivery point", hb.tfs.size()), + "native-engine.md:2665-2668"); + bool checks = hb.checks.size() == hs.checks.size(); + for (std::size_t i = 0; checks && i < hb.checks.size(); ++i) { + const auto& x = hb.checks[i].p; + const auto& y = hs.checks[i].p; + checks = x.kind == y.kind && same(x.mark, y.mark) && x.cursor.point.ordinal == y.cursor.point.ordinal + && same(x.position.signed_units, y.position.signed_units); + } + D1("MUST_EQUAL", "margin check points incl. FxRoll", checks, fmt("%zu points", hb.checks.size()), + "native-engine.md:3445-3453"); + int half_equal = 0; + for (std::size_t i = 0; i < std::min(hb.bars.size(), hs.bars.size()); ++i) + half_equal += hb.bars[i].broker_half == hs.bars[i].broker_half ? 1 : 0; + D1("MUST_EQUAL", "broker half at every bar (C++ only: protected fold)", half_equal == static_cast(hb.bars.size()), + fmt("%d/%zu bars", half_equal, hb.bars.size()), "native-engine.md:2034-2037; engine.hpp:2117-2119"); + // LENGTH: the one identity of the array that holds across drivings. + const bool len = b.report.broker_state_hash_len == b.report.script_bars_processed + && s.report.broker_state_hash_len == s.report.script_bars_processed + && b.report.equity_curve_len == b.report.broker_state_hash_len + && s.report.equity_curve_len == s.report.broker_state_hash_len; + D1("LENGTH", "hash_len == curve_len == script_bars", len, + fmt("batch %lld/%lld/%lld stream %lld/%lld/%lld", (ll)b.report.broker_state_hash_len, (ll)b.report.equity_curve_len, + (ll)b.report.script_bars_processed, (ll)s.report.broker_state_hash_len, (ll)s.report.equity_curve_len, + (ll)s.report.script_bars_processed), + "native-engine.md:2006-2010; pineforge.h:447-448, :1009-1010"); + // KEYED: the rows fold the continuation, whose driving mode is part of it. + int equal_rows = 0, first_equal = -1; + const ll n = std::min(b.report.broker_state_hash_len, s.report.broker_state_hash_len); + for (ll i = 0; i < n; ++i) + if (b.report.broker_state_hash[i] == s.report.broker_state_hash[i]) { + ++equal_rows; + if (first_equal < 0) first_equal = static_cast(i); + } + D1("KEYED", "per-bar broker_state_hash rows", equal_rows == 0, + fmt("%d/%lld rows equal; the rows differ from index 0", equal_rows, n), + "native-engine.md:2028-2032; pineforge.h:1003-1009; native_host.hpp:37-40"); + D1("KEYED", "final broker_state_hash / continuation hash", b.scalar != s.scalar && b.continuation != s.continuation, + fmt("batch %llu/%llu stream %llu/%llu", (ull)b.scalar, (ull)b.continuation, (ull)s.scalar, (ull)s.continuation), + "native-engine.md:2022-2032; native_c_api.h:3035-3041"); + // KEYED presentation: the final bar's session close. + const auto& fb = hb.bars.back().ctx; + const auto& fs = hs.bars.back().ctx; + D1("KEYED", "closes_session_day on the final (mid-session) bar", + fb.closes_session_day && !fs.closes_session_day && !fb.closes_session_day_open_ended + && fs.closes_session_day == fb.closes_session_day_open_ended, + fmt("batch %d (complete input) stream %d (reads on); batch open_ended %d == stream", fb.closes_session_day ? 1 : 0, + fs.closes_session_day ? 1 : 0, fb.closes_session_day_open_ended ? 1 : 0), + "market_driver.hpp:143-155; native_c_api.h:1428-1431"); + int sess_equal = 0; + for (std::size_t i = 0; i + 1 < std::min(hb.bars.size(), hs.bars.size()); ++i) { + const auto& x = hb.bars[i].ctx; + const auto& y = hs.bars[i].ctx; + sess_equal += (x.in_session == y.in_session && x.opens_session_day == y.opens_session_day + && x.closes_session_day == y.closes_session_day) ? 1 : 0; + } + D1("MUST_EQUAL", "session-day facts on every other bar", sess_equal == static_cast(hb.bars.size()) - 1, + fmt("%d/%zu", sess_equal, hb.bars.size() - 1), "market_driver.hpp:131-147"); + // WITHIN one driving: the last row is the scalar, and a shorter batch is a prefix. + D1("WITHIN", "last row == broker_state_hash()", + b.report.broker_state_hash[b.report.broker_state_hash_len - 1] == b.scalar + && s.report.broker_state_hash[s.report.broker_state_hash_len - 1] == s.scalar, + "batch and stream", "pineforge.h:447-452"); + bool prefix = cut.report.broker_state_hash_len == cut.opt.bars; + for (ll i = 0; prefix && i < cut.report.broker_state_hash_len; ++i) + prefix = cut.report.broker_state_hash[i] == b.report.broker_state_hash[i]; + D1("WITHIN", "prefix closure", prefix, + fmt("a batch cut after bar %d records the full batch's first %d rows", cut.opt.bars - 1, cut.opt.bars), + "native-engine.md:2024-2027; pineforge.h:1004-1008"); + // INFO: equal by measurement here, not promised by a doc line. + uint64_t eb = ACID_FNV_OFFSET, es = ACID_FNV_OFFSET; + for (const auto& e : b.events) { const auto pe = project(e); eb = acid_event_fold(eb, &pe); } + for (const auto& e : s.events) { const auto pe = project(e); es = acid_event_fold(es, &pe); } + out(fmt("DRIVING INFO events %s: batch %zu digest %llu, stream %zu digest %llu (not a documented invariant)", + eb == es ? "equal" : "differ", b.events.size(), (ull)eb, s.events.size(), (ull)es)); + out(fmt("DRIVING INFO phase at the end: batch %d stream %d; completion batch %d stream %d", + static_cast(b.state.phase), static_cast(s.state.phase), static_cast(b.state.completion), + static_cast(s.state.completion))); +} + +// ============================================================ findings +// Facts this witness measured that no requested feature asserts: printed, not +// checked, so the row stays a coverage witness and the finding goes to a lane. +void findings(const Run& plain, const Run& mag) { + int bar_open = 0, after = 0, other = 0; + for (const auto& c : mag.host->checks) { + if (c.p.kind == NativeMarginCheckKind::BarOpen) ++bar_open; + else if (c.p.kind == NativeMarginCheckKind::AfterApplied) ++after; + else ++other; + } + const auto* pc = plain.host->calls.empty() ? nullptr : &plain.host->calls.front(); + const auto* mc = mag.host->calls.empty() ? nullptr : &mag.host->calls.front(); + out(fmt("FINDING intrabar-margin: lower_tf run offers %d BarOpen + %d AfterApplied (+%d other) margin points for %d " + "script bars / %lld samples -- no per-sample point; the spike that crosses L inside bar %d is liquidated " + "at bar %d, price %s, in %zu slices (unmagnified: bar %d, price %s, %zu slice) " + "[docs claim per-sample re-evaluation: native-engine.md:1261-1262, native_run_spec.hpp:389-391, " + "native_execution_consumer.cpp:3076-3077]", + bar_open, after, other, static_cast(mag.host->bars.size()), + (ll)mag.report.magnifier_sample_ticks_total, G_SPIKE, + mc ? mc->cursor.point.interval_index : -1, mc ? D(mc->mark).c_str() : "-", mag.host->calls.size(), + pc ? pc->cursor.point.interval_index : -1, pc ? D(pc->mark).c_str() : "-", plain.host->calls.size())); + const auto& e2p = plain.host->fills.size() > 2 ? plain.host->fills[2] : FillRec{}; + const auto& e2m = mag.host->fills.size() > 2 ? mag.host->fills[2] : FillRec{}; + out(fmt("FINDING intrabar-birth: E2 (born in the take-profit's recalculation) fills on bar %d at %s unmagnified, " + "on bar %d at %s under lower_tf (the next sub-bar open is the next discrete matching point: " + "native-engine.md:1647-1649)", + e2p.gi, D(e2p.ev.resolved_price).c_str(), e2m.gi, D(e2m.ev.resolved_price).c_str())); +} + +// ============================================================ typed configuration +void typed_configuration() { + // A quantizing grid with no ladder: GridRequiresPriceTick at PriceGrid + // (native-engine.md:339-341). C++ fails the host (native_host.hpp:1156-1161). + AcidHost bad(Opts{}); + NativeRunSpec spec = make_spec(Opts{}); + spec.price_tick = 0.0; + const auto r = bad.configure_native(spec); + field("cfg.refusal", fmt("status:%d;err:%d;field:%d", static_cast(r.status), + static_cast(r.validation.error), static_cast(r.validation.field))); + const auto st = bad.native_state(); + out(fmt("CPPONLY cfg.cpp_host_after_refusal=kind:%d;failure:%d (the C ext path keeps its handle Unconfigured)", + static_cast(st.kind), static_cast(st.failure.code))); + fcheck("F12", "typed-refusal-path", + r.status == NativeSetupStatus::Failed && r.validation.error == NativeRunSpecError::GridRequiresPriceTick + && r.validation.field == NativeRunSpecField::PriceGrid, + "configure refusal is typed: GridRequiresPriceTick at PriceGrid"); +} + +#ifdef ACID_WITH_C +void sink(void* user, const char* l) { + auto* v = static_cast*>(user); + v->emplace_back(l); +} +#endif + +} // namespace + +int main() { + build_tape(); + out("# acid composite, C++ half: kernel-only host, source-free"); + typed_configuration(); + Opts ob; ob.tag = "batch"; + Opts os; os.tag = "stream"; os.stream = true; + Opts okb; okb.tag = "keep.batch"; okb.keep = true; + Opts oks; oks.tag = "keep.stream"; oks.keep = true; oks.stream = true; + Opts om; om.tag = "mag"; om.magnify = true; + Opts oc; oc.tag = "cut"; oc.bars = G_SPIKE + 1; + const auto batch = execute(ob); + const auto stream = execute(os); + const auto keep_b = execute(okb); + const auto keep_s = execute(oks); + const auto mag = execute(om); + const auto cut = execute(oc); + emit_run(*batch); + emit_run(*stream); + emit_run(*mag); + check_run_features(*batch); + check_run_features(*stream); + check_run_features(*mag); + check_keep(*keep_b, *batch); + check_keep(*keep_s, *stream); + driving_section(*batch, *stream, *cut); + findings(*batch, *mag); + +#ifdef ACID_WITH_C + { + std::vector c_lines; + const int c_failures = acid_c_main(&sink, &c_lines); + std::set cpp(g_fields.begin(), g_fields.end()), c; + std::set excluded; + for (const auto& l : c_lines) { + if (l.rfind("FIELD ", 0) == 0) c.insert(l); + if (l.rfind("EXCLUDED ", 0) == 0) excluded.insert(l.substr(9, l.find(' ', 9) - 9)); + if (l.rfind("PASS ", 0) == 0 || l.rfind("FAIL ", 0) == 0 || l.rfind("CONLY ", 0) == 0) out("C: " + l); + } + std::size_t only_cpp = 0, only_c = 0; + for (const auto& l : cpp) if (!c.count(l)) { if (only_cpp++ < 20) out("CMP only-C++ " + l); } + for (const auto& l : c) if (!cpp.count(l)) { if (only_c++ < 20) out("CMP only-C " + l); } + const bool equal = only_cpp == 0 && only_c == 0; + out(fmt("CMP C vs C++ FIELD sets: C++ %zu, C %zu, only-C++ %zu, only-C %zu -> %s", cpp.size(), c.size(), + only_cpp, only_c, equal ? "IDENTICAL" : "DIFFER")); + fcheck("CX", "c-port-shared-fields-identical", equal, "C and C++ FIELD sets"); + const char* families[] = {"C-SURFACE-2:closed-trade-entry-comment", "C-SURFACE-2:applied-origin-label", + "C-SURFACE-2:replace-options", "C-SURFACE-2:inspect_current_execution", + "COVERAGE:closes_session_day_open_ended", + "PROTECTED:broker_state_hash_from_execution_hash", "NOC:quote_origin_ordinal", + "NOC:driver_statistics"}; + for (const char* f : families) + fcheck("CX", "c-port-shared-fields-identical", excluded.count(f) == 1, + std::string("the C half names the excluded family ") + f); + fcheck("CX", "c-port-shared-fields-identical", c_failures == 0, fmt("C-side checks: %d failures", c_failures)); + } +#endif + // F01..F14 in the requested order, then the driving-mode accounting and the C comparison. + std::stable_sort(g_features.begin(), g_features.end(), [](const Feature& a, const Feature& b) { + const bool fa = a.id[0] == 'F', fb = b.id[0] == 'F'; + if (fa != fb) return fa; + return fa ? a.id < b.id : a.id > b.id; + }); + int failed_features = 0; + for (const auto& f : g_features) { + out(fmt("%s %s %s (%d checks)", f.failed ? "FAIL" : "PASS", f.id.c_str(), f.name.c_str(), f.total)); + for (const auto& w : f.failures) out(" - " + w); + failed_features += f.failed ? 1 : 0; + } + int checks = 0, fails = 0; + for (const auto& f : g_features) { checks += f.total; fails += f.failed; } + out(fmt("acid composite: %d features, %d failed; %d checks, %d failures; %zu FIELD lines", static_cast(g_features.size()), + failed_features, checks, fails, g_fields.size())); + return failed_features == 0 ? 0 : 1; +} diff --git a/tests/test_native_acid_composite_c.c b/tests/test_native_acid_composite_c.c new file mode 100644 index 00000000..52387a6f --- /dev/null +++ b/tests/test_native_acid_composite_c.c @@ -0,0 +1,1270 @@ +/* + * test_native_acid_composite_c.c -- the G1 acid composite, C half (lane H-MEASURE, item + * A4-ACID-COVERAGE; extends the fourth audit's codex C port). + * + * The same strategy as test_native_acid_composite.cpp, written against + * only: the same run specification (typed configure, + * strategy_configure_native_ext_result_v1), the same FX curve (typed, + * strategy_configure_native_fx_curve_ext_v1), the same commands at the same + * bars, the bracket legs spelled by hand (the toolkit is C++-only, + * native-engine.md:1052-1060), and the re-issued exit X spelled as a plain + * replace (C has no ReplaceOptions word: C-SURFACE-2, ruled to 1.1.0). It + * prints one `FIELD key=value` line per shared ledger fact -- the set the C++ + * half prints for the same runs -- plus one `EXCLUDED ` line + * per C++-only family, and `PASS|FAIL C ...` for the checks only C can make + * (the typed configuration path on one handle, the interval query's phase). + * + * C11, no C++ construct: it includes only pineforge.h and native_c_api.h + * (through native_acid_composite.h). Standalone it prints to stdout; compiled with + * -DACID_C_NO_MAIN it is the C translation unit of the linked twin, whose C++ + * half compares the two FIELD sets in process. + * + * (a CTest row: built with test_native_acid_composite.cpp, see tests/CMakeLists.txt) + */ +#include "native_acid_composite.h" + +#include +#include + +/* ── Output ─────────────────────────────────────────────────────────── */ +static acid_sink_fn g_sink; +static void* g_user; +static int g_fail; + +static void line(const char* fmt, ...) { + char buf[4096]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof buf, fmt, ap); + va_end(ap); + g_sink(g_user, buf); +} +static void cfield(const char* key, const char* fmt, ...) { + char value[3584]; + va_list ap; + va_start(ap, fmt); + vsnprintf(value, sizeof value, fmt, ap); + va_end(ap); + line("FIELD %s=%s", key, value); +} +static void ccheck(const char* id, const char* name, int ok, const char* what) { + line("%s %s %s: %s", ok ? "PASS" : "FAIL", id, name, what); + if (!ok) ++g_fail; +} +#define D(buf, v) acid_d(buf, sizeof buf, (v)) + +/* ── Tape ───────────────────────────────────────────────────────────── */ +static pf_bar_t g_b15[ACID_BARS]; +static pf_bar_t g_b5[ACID_FEED]; + +static double ceil_tick(double p) { return ceil(p / ACID_TICK) * ACID_TICK; } +static double buy_fill(double open) { return ceil_tick(open) + ACID_TICK; } + +/* ── Records ────────────────────────────────────────────────────────── */ +enum { MAX_FILLS = 32, MAX_TFS = 128, MAX_RECALC = 32, MAX_CHECKS = 256, MAX_REQS = 64, + MAX_CALLS = 8, MAX_LOT_SNAPS = 16, MAX_LOTS = 8, MAX_WSNAPS = 8, MAX_WROWS = 8, + MAX_TAGGED = 16, MAX_REFUSALS = 16, MAX_ANCHORS = 8, MAX_TRAILS = 8, MAX_APPENDS = 4, + MAX_XREP = 16 }; + +typedef struct { pf_native_open_lot_v1 lot; char label[32]; char comment[48]; } lot_rec; +typedef struct { char tag[16]; int n; lot_rec lots[MAX_LOTS]; } lots_snap; +typedef struct { pf_native_working_v1 w; char label[32]; } working_row; +typedef struct { char tag[16]; int n; working_row rows[MAX_WROWS]; } working_snap; +typedef struct { int gi; pf_native_applied_v1 a; pf_native_decision_v1 at; } fill_rec; +typedef struct { uint32_t sub; pf_bar_t bar; uint32_t comp; int64_t delivered_at; + pf_native_timeframe_interval_v1 iv; int iv_rc; int pulled; int gi; } tf_rec; +typedef struct { uint64_t cord, cinc; int gi; int has_partial; pf_bar_t partial; + pf_native_decision_v1 at; pf_native_risk_state_v1 risk; } recalc_rec; +typedef struct { char tag[16]; double v; } tagged_double; +typedef struct { char tag[16]; pf_native_risk_state_v1 r; } tagged_risk; +typedef struct { char id[40]; char word[48]; int same; } refusal_rec; +typedef struct { int gi; int rc; pf_native_trail_state_v1 s; } trail_rec; +typedef struct { char id[24]; uint32_t error; int32_t index; } append_rec; +typedef struct { double units, avg, eq; uint64_t lots; int working; } snap_t; + +typedef struct host_state { + /* options */ + const char* tag; + int stream, magnify, bars; + /* the handle */ + pf_strategy_t h; + /* scenario */ + int nbars; + uint64_t e1, tp, sl, e2, x, e3, t3, e4a, e4b, e5, xf, p1, p2, d4[4]; + int tp_accepted, sl_accepted, x_done; + int inputs, sub_bars, begins; + int interval_outside_rc; /* the interval query outside on_timeframe_bar */ + int interval_inside_bad; /* inside it: how many answered anything but OK */ + /* records */ + pf_native_decision_v1 bar_at[ACID_BARS]; + pf_native_risk_state_v1 bar_risk[ACID_BARS]; + fill_rec fills[MAX_FILLS]; int nfills; + tf_rec tfs[MAX_TFS]; int ntfs; + recalc_rec recalcs[MAX_RECALC]; int nrecalcs; + pf_native_margin_view_v1 checks[MAX_CHECKS]; int nchecks; + pf_native_margin_view_v1 reqs[MAX_REQS]; int nreqs; + pf_native_margin_view_v1 callviews[MAX_CALLS]; int ncallviews; + pf_native_margin_call_v1 calls[MAX_CALLS]; int ncalls; + lots_snap lots_before, lots_after; int lots_before_taken; + lots_snap lot_snaps[MAX_LOT_SNAPS]; int nlot_snaps; + working_snap wsnaps[MAX_WSNAPS]; int nwsnaps; + tagged_double liq[MAX_TAGGED]; int nliq; + tagged_risk risks[MAX_TAGGED]; int nrisks; + tagged_double sized[4]; int nsized; + refusal_rec refusals[MAX_REFUSALS]; int nrefusals; + pf_native_anchored_level_view_v1 anchors[MAX_ANCHORS]; int nanchors; + trail_rec trails[MAX_TRAILS]; int ntrails; + append_rec appends[MAX_APPENDS]; int nappends; + char x_replaces[MAX_XREP][24]; int nxrep; +} host_state; + +/* ── Small helpers ──────────────────────────────────────────────────── */ +static pf_native_request_v1 req0(uint32_t intent, double value, const char* label, const char* comment) { + pf_native_request_v1 r; + memset(&r, 0, sizeof r); + r.struct_size = (uint32_t)sizeof r; + r.version = PF_NATIVE_API_VERSION; + r.intent = intent; + r.intent_value = value; + r.label = label; + r.comment = comment; + return r; +} +static const char* reject_word(uint32_t r) { + switch (r) { + case PF_NATIVE_REJECT_INVALID_QUANTITY: return "InvalidQuantity"; + case PF_NATIVE_REJECT_OFF_GRID: return "OffGrid"; + case PF_NATIVE_REJECT_INVALID_TRIGGER: return "InvalidTrigger"; + case PF_NATIVE_REJECT_INVALID_CAPACITY: return "InvalidCapacity"; + case PF_NATIVE_REJECT_INVALID_OWNER: return "InvalidOwner"; + case PF_NATIVE_REJECT_INVALID_QUANTITY_BASIS: return "InvalidQuantityBasis"; + case PF_NATIVE_REJECT_INVALID_GROUP: return "InvalidGroup"; + case PF_NATIVE_REJECT_PLACEMENT_ADMISSION: return "PlacementAdmission"; + default: return "?"; + } +} +static const char* status_word(int rc) { + switch (rc) { + case PF_NATIVE_OK: return "Replaced"; + case PF_NATIVE_E_REJECTED: return "ReplaceRejected"; + case PF_NATIVE_E_NOT_WORKING: return "NotWorking"; + case PF_NATIVE_E_INVALID_TARGET: return "InvalidHandle"; + default: return "?"; + } +} +static const char* append_word(uint32_t e) { + switch (e) { + case PF_NATIVE_APPEND_ERROR_NONE: return "None"; + case PF_NATIVE_APPEND_ERROR_HOST_FAILED: return "HostFailed"; + case PF_NATIVE_APPEND_ERROR_REENTRANT: return "Reentrant"; + case PF_NATIVE_APPEND_ERROR_NOT_REALTIME: return "NotRealtime"; + case PF_NATIVE_APPEND_ERROR_NO_AUXILIARY_FEED: return "NoAuxiliaryFeed"; + case PF_NATIVE_APPEND_ERROR_INVALID_BAR_ARRAY: return "InvalidBarArray"; + case PF_NATIVE_APPEND_ERROR_INVALID_BAR: return "InvalidBar"; + case PF_NATIVE_APPEND_ERROR_UNORDERED_BARS: return "UnorderedBars"; + case PF_NATIVE_APPEND_ERROR_INPUT_PERIOD_ALREADY_ACCEPTED: return "InputPeriodAlreadyAccepted"; + case PF_NATIVE_APPEND_ERROR_ALLOCATION_FAILURE: return "AllocationFailure"; + default: return "?"; + } +} +/* Submit and answer the word the C++ half prints for the same SubmitResult. */ +static uint64_t submit_w(host_state* st, const pf_native_request_v1* r, char* word, size_t n) { + uint64_t inc = 0; + uint32_t reject = 0; + const int rc = strategy_native_submit_v1(st->h, r, &inc, &reject); + if (word) { + if (rc == PF_NATIVE_OK) snprintf(word, n, "Accepted"); + else if (rc == PF_NATIVE_E_REJECTED) snprintf(word, n, "Rejected/%s", reject_word(reject)); + else snprintf(word, n, "C-error/%d", rc); + } + return rc == PF_NATIVE_OK ? inc : 0; +} +static uint64_t submit(host_state* st, const pf_native_request_v1* r) { return submit_w(st, r, NULL, 0); } + +static snap_t snap(host_state* st, double mark) { + snap_t s; + memset(&s, 0, sizeof s); + strategy_native_position_v1(st->h, &s.units, &s.avg, &s.lots); + strategy_native_marked_equity_v1(st->h, mark, &s.eq); + s.working = strategy_native_working_len_v1(st->h); + return s; +} +static int same_snap(snap_t a, snap_t b) { + return memcmp(&a.units, &b.units, sizeof(double)) == 0 && memcmp(&a.avg, &b.avg, sizeof(double)) == 0 + && memcmp(&a.eq, &b.eq, sizeof(double)) == 0 && a.lots == b.lots && a.working == b.working; +} +static void refusal(host_state* st, const char* id, const char* word, snap_t before, double mark) { + refusal_rec* r; + if (st->nrefusals >= MAX_REFUSALS) return; + r = &st->refusals[st->nrefusals++]; + snprintf(r->id, sizeof r->id, "%s", id); + snprintf(r->word, sizeof r->word, "%s", word); + r->same = same_snap(snap(st, mark), before); +} +static void take_lots(host_state* st, lots_snap* out, const char* tag, double mark) { + int i, n = strategy_native_open_lot_count_v1(st->h, mark); + snprintf(out->tag, sizeof out->tag, "%s", tag); + out->n = 0; + for (i = 0; i < n && i < MAX_LOTS; ++i) { + lot_rec* l = &out->lots[out->n]; + memset(&l->lot, 0, sizeof l->lot); + l->lot.struct_size = (uint32_t)sizeof l->lot; + l->lot.version = PF_NATIVE_API_VERSION; + if (strategy_native_open_lot_get_v1(st->h, i, &l->lot) != PF_NATIVE_OK) continue; + snprintf(l->label, sizeof l->label, "%s", l->lot.entry_label ? l->lot.entry_label : ""); + snprintf(l->comment, sizeof l->comment, "%s", l->lot.entry_comment ? l->lot.entry_comment : ""); + ++out->n; + } +} +static void snap_lots(host_state* st, const char* tag, double mark) { + if (st->nlot_snaps < MAX_LOT_SNAPS) take_lots(st, &st->lot_snaps[st->nlot_snaps++], tag, mark); +} +static void snap_working(host_state* st, const char* tag) { + working_snap* w; + int i, n; + if (st->nwsnaps >= MAX_WSNAPS) return; + w = &st->wsnaps[st->nwsnaps++]; + snprintf(w->tag, sizeof w->tag, "%s", tag); + w->n = 0; + n = strategy_native_working_len_v1(st->h); + for (i = 0; i < n && i < MAX_WROWS; ++i) { + working_row* row = &w->rows[w->n]; + memset(&row->w, 0, sizeof row->w); + row->w.struct_size = (uint32_t)sizeof row->w; + row->w.version = PF_NATIVE_API_VERSION; + if (strategy_native_working_get_v1(st->h, i, &row->w) != PF_NATIVE_OK) continue; + snprintf(row->label, sizeof row->label, "%s", row->w.label ? row->w.label : ""); + ++w->n; + } +} +static void snap_liq(host_state* st, const char* tag) { + tagged_double* t; + if (st->nliq >= MAX_TAGGED) return; + t = &st->liq[st->nliq++]; + snprintf(t->tag, sizeof t->tag, "%s", tag); + t->v = NAN; + strategy_native_liquidation_price_v1(st->h, &t->v); /* NaN when absent */ +} +static pf_native_risk_state_v1 risk_now(host_state* st) { + pf_native_risk_state_v1 r; + memset(&r, 0, sizeof r); + r.struct_size = (uint32_t)sizeof r; + r.version = PF_NATIVE_API_VERSION; + strategy_native_risk_state_v1(st->h, &r); + return r; +} +static void snap_risk(host_state* st, const char* tag) { + tagged_risk* t; + if (st->nrisks >= MAX_TAGGED) return; + t = &st->risks[st->nrisks++]; + snprintf(t->tag, sizeof t->tag, "%s", tag); + t->r = risk_now(st); +} + +/* ── The scenario: Monday 09:00 ─────────────────────────────────────── */ +static pf_native_request_v1 sized_req(double cash, uint32_t time, int reserve, const char* label, + const char* comment) { + pf_native_request_v1 r = req0(PF_NATIVE_INTENT_SIZED, cash, label, comment); + r.side = PF_NATIVE_SIDE_LONG; + r.size_basis = PF_NATIVE_SIZE_BASIS_CASH; + r.size_time = time; + r.grid_policy = PF_NATIVE_GRID_SNAP; + r.size_price = PF_NATIVE_SIZE_PRICE_RESOLVED; + r.reserve_percent_fee = (uint32_t)reserve; + return r; +} +static pf_native_request_v1 leg_req(uint64_t* parent, uint32_t trigger, double offset, uint32_t rounding, + const char* label) { + pf_native_request_v1 r = req0(PF_NATIVE_INTENT_REDUCE, 0.0, label, "bracket"); + r.reduce_size = PF_NATIVE_REDUCE_OWNER_OPENED; + r.trigger = trigger; + r.p1 = 0.0; + r.anchor = PF_NATIVE_ANCHOR_FROM_OWNER_FILL; + r.anchor_offset = offset; + r.anchor_rounding = rounding; + r.owner = PF_NATIVE_OWNER_WAIT_FOR_APPLIED; + r.owner_n = 1; + r.owner_incarnations = parent; + return r; +} + +static void monday_open(host_state* st, const pf_bar_t* bar) { + const double mark = bar->close; + snap_t s0 = snap(st, mark), s1; + char word[64]; + pf_native_request_v1 r; + { + r = req0(PF_NATIVE_INTENT_TRANSACT, 0.0, "R01", "zero"); + submit_w(st, &r, word, sizeof word); + refusal(st, "R01.transact_zero", word, s0, mark); + r = req0(PF_NATIVE_INTENT_TRANSACT, 0.005, "R02", "off grid"); + submit_w(st, &r, word, sizeof word); + refusal(st, "R02.off_grid", word, s0, mark); + r = sized_req(-5.0, PF_NATIVE_SIZE_AT_MATCH, 0, "R03", "negative cash"); + submit_w(st, &r, word, sizeof word); + refusal(st, "R03.bad_basis", word, s0, mark); + r = sized_req(5000000.0, PF_NATIVE_SIZE_AT_ACCEPTANCE, 0, "R04", "too big"); + submit_w(st, &r, word, sizeof word); + refusal(st, "R04.placement", word, s0, mark); + } + { + uint32_t err = 999, field = 999; + const int rc = strategy_native_declare_subscriptions_ext_v1(st->h, NULL, 0, NULL, &err, &field); + snprintf(word, sizeof word, "%s/%u/%u", rc == PF_NATIVE_OK ? "Applied" : "Failed", err, field); + refusal(st, "R05.declare_outside_begin", word, s0, mark); + } + { + int64_t t = bar->timestamp + 1; + double rate = 2.0; + pf_native_fx_curve_v1 c; + pf_native_fx_curve_error_t err = PF_NATIVE_FX_CURVE_ERROR_NONE; + uint64_t idx = 0; + int rc; + memset(&c, 0, sizeof c); + c.struct_size = (uint32_t)sizeof c; + c.n = 1; + c.effective_from_ms = &t; + c.account_per_quote = &rate; + rc = strategy_configure_native_fx_curve_ext_v1(st->h, &c, &err, &idx); + snprintf(word, sizeof word, "%s/%d/%llu", rc == 0 ? "Applied" : "Failed", (int)err, + (unsigned long long)idx); + refusal(st, "R06.fx_curve_while_running", word, s0, mark); + } + { + /* F01: the kernel's sizing as a pure query, at the candidate's price. */ + const double px = buy_fill(g_b15[G_E1FILL].open); + double u = NAN; + r = sized_req(ACID_E1_CASH, PF_NATIVE_SIZE_AT_MATCH, 1, "E1", "sized entry"); + strategy_native_sized_units_v1(st->h, &r, px, 0.0, ACID_ACCOUNT_FX, &u); + snprintf(st->sized[0].tag, sizeof st->sized[0].tag, "E1.reserve"); + st->sized[0].v = u; + r.reserve_percent_fee = 0; + u = NAN; + strategy_native_sized_units_v1(st->h, &r, px, 0.0, ACID_ACCOUNT_FX, &u); + snprintf(st->sized[1].tag, sizeof st->sized[1].tag, "E1.no_reserve"); + st->sized[1].v = u; + st->nsized = 2; + } + r = sized_req(ACID_E1_CASH, PF_NATIVE_SIZE_AT_MATCH, 1, "E1", "sized entry"); + st->e1 = submit(st, &r); + if (!st->e1) return; + /* The bracket the toolkit builds (native_toolkit.hpp:138-183), by hand: + * WAIT_FOR_APPLIED children of E1 in one OCA group (the parent's + * incarnation), one cohort per leg, Directional rounding, pending. */ + r = leg_req(&st->e1, PF_NATIVE_TRIGGER_LIMIT, ACID_TP_OFFSET, PF_NATIVE_ANCHOR_ROUNDING_DIRECTIONAL, "TP"); + r.visibility = PF_NATIVE_ARM_VISIBILITY_PENDING_UNTIL_ARMED; + r.group_kind = PF_NATIVE_GROUP_MEMBER; + r.group_id = st->e1; + r.group_cohort = 1; + r.group_effect = PF_NATIVE_GROUP_CANCEL; + st->tp = submit(st, &r); + st->tp_accepted = st->tp != 0; + r = leg_req(&st->e1, PF_NATIVE_TRIGGER_STOP, ACID_SL_OFFSET, PF_NATIVE_ANCHOR_ROUNDING_DIRECTIONAL, "SL"); + r.visibility = PF_NATIVE_ARM_VISIBILITY_PENDING_UNTIL_ARMED; + r.group_kind = PF_NATIVE_GROUP_MEMBER; + r.group_id = st->e1; + r.group_cohort = 2; + r.group_effect = PF_NATIVE_GROUP_CANCEL; + st->sl = submit(st, &r); + st->sl_accepted = st->sl != 0; + snap_working(st, "E1"); + s1 = snap(st, mark); + r = leg_req(&st->e1, PF_NATIVE_TRIGGER_LIMIT, ACID_TP_OFFSET, PF_NATIVE_ANCHOR_ROUNDING_RAW, "R07"); + r.p1 = 203.0; /* a written anchored level */ + submit_w(st, &r, word, sizeof word); + refusal(st, "R07.written_anchor", word, s1, mark); + r = sized_req(1000.0, PF_NATIVE_SIZE_AT_MATCH, 0, "R08", "sized child"); + r.owner = PF_NATIVE_OWNER_WAIT_FOR_APPLIED; + r.owner_n = 1; + r.owner_incarnations = &st->e1; + submit_w(st, &r, word, sizeof word); + refusal(st, "R08.sized_child", word, s1, mark); +} + +static void reissue_x(host_state* st, int gi) { + const double level = ACID_X_LEVEL0 + ACID_TICK * (double)(gi - G_E2FILL); + pf_native_request_v1 r; + uint64_t succ = 0; + uint32_t reject = 0; + int rc; + if (!st->x || st->x_done) return; + r = req0(PF_NATIVE_INTENT_FLATTEN, 0.0, "X", "protect"); + r.trigger = PF_NATIVE_TRIGGER_STOP; + r.p1 = level; + /* C has no ReplaceOptions (native_host.hpp:1206): a plain replace. */ + rc = strategy_native_replace_ext_v1(st->h, st->x, &r, &succ, &reject); + if (st->nxrep < MAX_XREP) snprintf(st->x_replaces[st->nxrep++], 24, "%s", status_word(rc)); + if (rc == PF_NATIVE_OK) st->x = succ; +} + +static void on_bar_close(host_state* st, const pf_bar_t* bar, const pf_native_decision_v1* at) { + const int gi = st->nbars++; + const double mark = bar->close; + pf_native_request_v1 r; + int k; + if (gi < ACID_BARS) { + st->bar_at[gi] = *at; + st->bar_risk[gi] = risk_now(st); + } + { /* The interval query answers only inside on_timeframe_bar. */ + pf_native_timeframe_interval_v1 iv; + memset(&iv, 0, sizeof iv); + iv.struct_size = (uint32_t)sizeof iv; + iv.version = PF_NATIVE_API_VERSION; + if (gi == G_E1) st->interval_outside_rc = strategy_native_timeframe_bar_interval_v1(st->h, &iv); + } + if (gi == G_E1) monday_open(st, bar); + if (gi == G_E1FILL) { + snap_lots(st, "E1FILL", mark); + snap_working(st, "E1FILL"); + } + if (gi == G_E2FILL) { + snap_t s0 = snap(st, mark); + if (st->e1) { + uint64_t succ = 0; + uint32_t reject = 0; + r = req0(PF_NATIVE_INTENT_TRANSACT, 1.0, "R09", ""); + refusal(st, "R09.replace_terminal", + status_word(strategy_native_replace_ext_v1(st->h, st->e1, &r, &succ, &reject)), s0, mark); + } + if (st->sl) refusal(st, "R10.cancel_cancelled_leg", status_word(strategy_native_cancel_v1(st->h, st->sl)), s0, mark); + snap_lots(st, "E2FILL", mark); + r = req0(PF_NATIVE_INTENT_FLATTEN, 0.0, "X", "protect"); + r.trigger = PF_NATIVE_TRIGGER_STOP; + r.p1 = ACID_X_LEVEL0; + st->x = submit(st, &r); + } + if (gi >= G_X_FIRST && gi <= G_X_LAST) reissue_x(st, gi); + if (gi == G_XFILL) snap_lots(st, "XFILL", mark); + if (gi == G_E3) { + r = req0(PF_NATIVE_INTENT_TRANSACT, ACID_E3_UNITS, "E3", "trail entry"); + st->e3 = submit(st, &r); + if (st->e3) { + r = req0(PF_NATIVE_INTENT_REDUCE, 0.0, "T3", "trail"); + r.reduce_size = PF_NATIVE_REDUCE_OWNER_OPENED; + r.trigger = PF_NATIVE_TRIGGER_TRAIL; + r.p1 = ACID_T3_TICKS; + r.trail_offset_in_ticks = 1; + r.owner = PF_NATIVE_OWNER_WAIT_FOR_APPLIED; + r.owner_n = 1; + r.owner_incarnations = &st->e3; + st->t3 = submit(st, &r); + } + snap_working(st, "E3"); + } + if (gi == G_E3FILL) snap_lots(st, "E3FILL", mark); + if (st->t3 && gi >= G_E3FILL && gi <= G_T3X && st->ntrails < MAX_TRAILS) { + trail_rec* t = &st->trails[st->ntrails++]; + memset(&t->s, 0, sizeof t->s); + t->s.struct_size = (uint32_t)sizeof t->s; + t->s.version = PF_NATIVE_API_VERSION; + t->gi = gi; + t->rc = strategy_native_trail_state_v1(st->h, st->t3, &t->s); + } + if (gi == G_E4A) { + r = req0(PF_NATIVE_INTENT_TRANSACT, ACID_E4A_UNITS, "E4a", "short"); + st->e4a = submit(st, &r); + } + if (gi == G_E4B) { + r = req0(PF_NATIVE_INTENT_TRANSACT, ACID_E4B_UNITS, "E4b", "short"); + st->e4b = submit(st, &r); + } + if (gi == G_E4BFILL) { + snap_lots(st, "E4BFILL", mark); + snap_liq(st, "E4BFILL"); + } + if (gi == G_PRESTEP) snap_liq(st, "PRESTEP"); + if (gi == G_STEP) snap_liq(st, "STEP"); + if (gi == G_PRESPIKE) snap_liq(st, "PRESPIKE"); + if (gi == G_SPIKE) { + snap_liq(st, "SPIKE"); + snap_lots(st, "SPIKE", mark); + snap_risk(st, "SPIKE"); + r = req0(PF_NATIVE_INTENT_TRANSACT, ACID_E5_UNITS, "E5", "add while blocked"); + st->e5 = submit(st, &r); + } + if (gi == G_E5REJ) { + snap_risk(st, "E5REJ"); + r = req0(PF_NATIVE_INTENT_FLATTEN, 0.0, "XF", "flatten while blocked"); + st->xf = submit(st, &r); + } + if (gi == G_XFFILL) { + snap_liq(st, "XFFILL"); + snap_lots(st, "XFFILL", mark); + } + if (gi == G_D4) { + static const char* labels[4] = {"E6", "E7", "E8", "E9"}; + snap_risk(st, "D4"); + for (k = 0; k < 4; ++k) { + r = req0(PF_NATIVE_INTENT_TRANSACT, acid_d4_units[k], labels[k], "thursday"); + st->d4[k] = submit(st, &r); + } + snap_working(st, "D4"); + } + if (gi == G_D4FILL) snap_lots(st, "D4FILL", mark); + if (gi == G_P1) { + r = req0(PF_NATIVE_INTENT_REDUCE, ACID_P1_UNITS, "P1", "fifo partial"); + r.reduce_size = PF_NATIVE_REDUCE_EXPLICIT_UNITS; + st->p1 = submit(st, &r); + } + if (gi == G_P1FILL) snap_lots(st, "P1FILL", mark); + if (gi == G_P2) { + lots_snap book; + take_lots(st, &book, "book", mark); + if (book.n >= 2) { + /* the binary64 sum of the first two lots */ + const double units = book.lots[0].lot.signed_units + book.lots[1].lot.signed_units; + r = req0(PF_NATIVE_INTENT_REDUCE, units, "P2", "exact sum"); + r.reduce_size = PF_NATIVE_REDUCE_EXPLICIT_UNITS; + st->p2 = submit(st, &r); + } + } + if (gi == G_P2FILL) snap_lots(st, "P2FILL", mark); + if (gi == st->bars - 1) { + snap_lots(st, "LAST", mark); + snap_risk(st, "LAST"); + } +} + +/* ── Callbacks ──────────────────────────────────────────────────────── */ +static int cb_run_begin(void* user) { ++((host_state*)user)->begins; return 0; } +static int cb_input(void* user, const pf_bar_t* bar, int32_t i, int32_t c) { + (void)bar; (void)i; (void)c; + ++((host_state*)user)->inputs; + return 0; +} +static int cb_sub_bar(void* user, const pf_bar_t* sub, const pf_native_decision_v1* at) { + (void)sub; (void)at; + ++((host_state*)user)->sub_bars; + return 0; +} +static int cb_bar(void* user, const pf_bar_t* bar, const pf_native_decision_v1* at) { + on_bar_close((host_state*)user, bar, at); + return 0; +} +static int cb_recalculate(void* user, const pf_bar_t* bar, const pf_native_decision_v1* at, uint32_t reason, + const pf_native_applied_v1* cause) { + host_state* st = (host_state*)user; + recalc_rec* rc; + if (reason == PF_NATIVE_CALC_BAR_CLOSE) { + on_bar_close(st, bar, at); + return 0; + } + if (reason != PF_NATIVE_CALC_ORDER_FILL || st->nrecalcs >= MAX_RECALC) return 0; + rc = &st->recalcs[st->nrecalcs++]; + memset(rc, 0, sizeof *rc); + rc->gi = st->nbars; + if (cause) { + rc->cord = cause->ordinal; + rc->cinc = cause->incarnation; + } + rc->has_partial = strategy_native_partial_bar_v1(st->h, &rc->partial) == PF_NATIVE_OK; + rc->at = *at; + rc->risk = risk_now(st); + /* F08: a request born in the take-profit's fill recalculation. */ + if (cause && st->tp && cause->incarnation == st->tp && !st->e2) { + pf_native_request_v1 r = req0(PF_NATIVE_INTENT_TRANSACT, ACID_E2_UNITS, "E2", "re-entry on the fill"); + st->e2 = submit(st, &r); + } + return 0; +} +static int cb_applied(void* user, const pf_native_applied_v1* a, const pf_native_decision_v1* at) { + host_state* st = (host_state*)user; + if (st->nfills < MAX_FILLS) { + fill_rec* f = &st->fills[st->nfills++]; + f->gi = st->nbars; + f->a = *a; + f->at = *at; + } + if (st->x && a->incarnation == st->x) st->x_done = 1; + return 0; +} +static int cb_timeframe_bar(void* user, const pf_bar_t* bar, uint32_t sub, uint32_t completion, + int64_t delivered_at) { + host_state* st = (host_state*)user; + tf_rec* t; + pf_bar_t pulled; + if (st->ntfs >= MAX_TFS) return 0; + t = &st->tfs[st->ntfs++]; + memset(t, 0, sizeof *t); + t->sub = sub; + t->bar = *bar; + t->comp = completion; + t->delivered_at = delivered_at; + t->gi = st->nbars; + t->iv.struct_size = (uint32_t)sizeof t->iv; + t->iv.version = PF_NATIVE_API_VERSION; + t->iv_rc = strategy_native_timeframe_bar_interval_v1(st->h, &t->iv); + if (t->iv_rc != PF_NATIVE_OK) ++st->interval_inside_bad; + memset(&pulled, 0, sizeof pulled); + t->pulled = strategy_native_series_bar_v1(st->h, sub, &pulled) == PF_NATIVE_OK + && memcmp(&pulled, bar, sizeof pulled) == 0; + return 0; +} +static int cb_margin_call(void* user, const pf_native_event_v1* e) { + host_state* st = (host_state*)user; + if (st->ncalls < MAX_CALLS) { + pf_native_margin_call_v1* c = &st->calls[st->ncalls]; + memset(c, 0, sizeof *c); + c->struct_size = (uint32_t)sizeof *c; + c->version = PF_NATIVE_API_VERSION; + if (strategy_native_margin_call_v1(st->h, e->ordinal, c) == PF_NATIVE_OK) { + ++st->ncalls; + take_lots(st, &st->lots_after, "CALLAFTER", c->mark); + } + } + return 0; +} +static int cb_margin_check(void* user, const pf_native_margin_view_v1* v, int32_t* allowed) { + host_state* st = (host_state*)user; + (void)allowed; + if (st->nchecks < MAX_CHECKS) st->checks[st->nchecks++] = *v; + return PF_NATIVE_ANSWER_DEFAULT; +} +static int cb_margin_requirement(void* user, const pf_native_margin_view_v1* v, pf_native_margin_decision_v1* out) { + host_state* st = (host_state*)user; + (void)out; + if (st->nreqs < MAX_REQS) st->reqs[st->nreqs++] = *v; + return PF_NATIVE_ANSWER_DEFAULT; +} +static int cb_margin_call_units(void* user, const pf_native_margin_view_v1* v, double* units) { + host_state* st = (host_state*)user; + (void)units; + if (st->ncallviews < MAX_CALLS) st->callviews[st->ncallviews++] = *v; + if (!st->lots_before_taken) { + take_lots(st, &st->lots_before, "CALLBEFORE", v->mark); + st->lots_before_taken = 1; + } + return PF_NATIVE_ANSWER_DEFAULT; +} +static int cb_anchored_level(void* user, const pf_native_anchored_level_view_v1* v, double* level) { + host_state* st = (host_state*)user; + (void)level; + if (st->nanchors < MAX_ANCHORS) st->anchors[st->nanchors++] = *v; + return PF_NATIVE_ANSWER_DEFAULT; +} + +/* ── The run specification, spelled in C ────────────────────────────── */ +static pf_native_run_spec_v1 base_spec(double tick) { + pf_native_run_spec_v1 s; + memset(&s, 0, sizeof s); + s.struct_size = (uint32_t)sizeof s; + s.session_key = "acid-composite"; + s.run_number = 1; + s.input_tf = "15"; + s.script_tf = "15"; + s.ticker = "ACIDX"; + s.tickerid = "ACID:ACIDX"; + s.type = "cfd"; + s.currency = "EUR"; + s.basecurrency = ""; + s.description = "acid composite"; + s.volumetype = ""; + s.timezone = "UTC"; + s.session = "0900-1500:23456"; + s.chart_timezone = ""; + s.initial_capital = ACID_CAPITAL; + s.point_value = ACID_POINT_VALUE; + s.account_fx = ACID_ACCOUNT_FX; + s.price_tick = tick; + s.slippage_ticks = ACID_SLIPPAGE; + s.fee_kind = PF_NATIVE_FEE_PERCENT; + s.fee_value = ACID_FEE_PCT; + s.optional_mask = PF_NATIVE_SPEC_OPTIONAL_QUANTITY_GRID; + s.quantity_grid = ACID_QTY_GRID; + s.close_execution = PF_NATIVE_CLOSE_EXECUTION_NEXT_ELIGIBLE_POINT; + s.allowed_open_directions = PF_NATIVE_OPEN_DIRECTIONS_BOTH; + return s; +} +static pf_native_subscription_v1 g_subs[2]; +static const uint32_t g_sources[2] = {PF_NATIVE_SERIES_SOURCE_INPUT, PF_NATIVE_SERIES_SOURCE_AUXILIARY_FEED}; +static pf_native_run_spec_ext_v1 ext_spec(const host_state* st) { + pf_native_run_spec_ext_v1 e; + memset(&e, 0, sizeof e); + memset(g_subs, 0, sizeof g_subs); + g_subs[0].struct_size = (uint32_t)sizeof g_subs[0]; + g_subs[0].tf = "60"; + g_subs[1].struct_size = (uint32_t)sizeof g_subs[1]; + g_subs[1].tf = "30"; + e.struct_size = (uint32_t)sizeof e; + e.version = PF_NATIVE_API_VERSION; + e.present_mask = PF_NATIVE_SPEC_EXT_REPORT | PF_NATIVE_SPEC_EXT_PRICE_GRID | PF_NATIVE_SPEC_EXT_CALCULATION + | PF_NATIVE_SPEC_EXT_MARGIN | PF_NATIVE_SPEC_EXT_SUBSCRIPTIONS | PF_NATIVE_SPEC_EXT_RISK + | PF_NATIVE_SPEC_EXT_AUXILIARY_FEED | PF_NATIVE_SPEC_EXT_EVENT_RETENTION; + e.report_policy = PF_NATIVE_REPORT_KERNEL_RECORDED; + e.report_open_position_at_end = 1; + e.price_grid = PF_NATIVE_PRICE_GRID_QUANTIZE_FILLS_AND_TRIGGERS; + e.grid_rounding = PF_NATIVE_GRID_ROUNDING_DIRECTIONAL; + e.calculation = PF_NATIVE_CALC_TRIGGER_BAR_CLOSE_AND_FILLS; + e.max_recalculations_per_point = ACID_MAX_RECALCS; + e.open_bar_view = PF_NATIVE_OPEN_BAR_VIEW_COMPLETE; + e.margin_sizing = PF_NATIVE_LIQUIDATION_SIZING_RESTORE_MINIMUM; + e.margin_check = PF_NATIVE_LIQUIDATION_PATH_ADVERSE_EXTREME; + e.margin_initial_long = ACID_INITIAL_LONG; + e.margin_initial_short = 0.0; /* the maintenance-only side */ + e.margin_has_maintenance_short = 1; + e.margin_maintenance_short = ACID_MAINT_SHORT; + e.margin_shortfall_multiple = 1.0; + e.margin_equity_basis = PF_NATIVE_MARGIN_EQUITY_MARKED; + e.margin_level_base = PF_NATIVE_MARGIN_LEVEL_MARKED_EQUITY; + e.margin_liquidation_label = ACID_LIQ_LABEL; + e.margin_liquidation_comment = ACID_LIQ_COMMENT; + e.subscriptions = g_subs; + e.subscriptions_n = 2; + e.subscription_sources = g_sources; + e.risk_has_max_intraday_loss = 1; + e.risk_max_intraday_loss = ACID_RISK_LOSS; + e.risk_day_basis = PF_NATIVE_RISK_DAY_SESSION; + e.risk_action = PF_NATIVE_RISK_BLOCK_OPENINGS; + e.auxiliary_tf = "5"; + e.auxiliary_bars = g_b5; + e.auxiliary_n = st->stream ? 3 * ACID_WARMUP : ACID_FEED; + e.event_retention = PF_NATIVE_EVENT_RETENTION_FULL; + if (st->magnify) { + e.present_mask |= PF_NATIVE_SPEC_EXT_INTRABAR; + e.intrabar_kind = PF_NATIVE_INTRABAR_LOWER_TF; + e.intrabar_samples = 4; + e.intrabar_distribution = PF_MAGNIFIER_ENDPOINTS; + e.intrabar_volume_weighted = 0; + e.intrabar_volume_weighted_min_samples = 2; + e.intrabar_volume_weighted_max_samples = 64; + e.intrabar_sample_eligibility = PF_NATIVE_SAMPLE_CONTINUOUS_SEGMENTS; + e.intrabar_n = ACID_FEED; + e.intrabar_tf = "5"; + e.intrabar_bars = g_b5; + } + return e; +} + +/* ── Emission (the C++ half's formats, character for character) ─────── */ +static void risk_str(char* out, size_t n, const pf_native_risk_state_v1* r) { + char a[64], b[64]; + snprintf(out, n, "%u/%u/%u/%u/%lld/%llu/%u/%s/%s", r->blocked, r->has_reason, r->has_reason ? r->reason : 0u, + r->has_day, (long long)r->day_ordinal, (unsigned long long)r->fills_today, r->consecutive_loss_days, + D(a, r->peak_equity), D(b, r->day_open_equity)); +} +static void cursor_str(char* out, size_t n, uint64_t ord, int64_t eff, double t, int32_t ii, uint8_t prov, + uint8_t ph) { + char a[64]; + snprintf(out, n, "%llu/%lld/%s/%d/%d/%d", (unsigned long long)ord, (long long)eff, D(a, t), ii, prov, ph); +} +static void emit_lots(const char* p, const char* tag, const lots_snap* s) { + char key[96]; + int i; + snprintf(key, sizeof key, "%s.lots.%s", p, tag); + cfield(key, "n:%d", s->n); + for (i = 0; i < s->n; ++i) { + const pf_native_open_lot_v1* l = &s->lots[i].lot; + char a[64], b[64], c[64], d[64], e[64], f[64], g[64]; + snprintf(key, sizeof key, "%s.lots.%s.%02d", p, tag, i); + cfield(key, "ord:%llu;inc:%llu;cyc:%lld;side:%u;eb:%d;et:%lld;ep:%s;u:%s;com:%s;mark:%s;upnl:%s;fav:%s;adv:%s;" + "lab:%s;cmt:%s", + (unsigned long long)l->ordinal, (unsigned long long)l->entry_incarnation, (long long)l->cycle, l->side, + l->entry_bar_index, (long long)l->entry_time_ms, D(a, l->entry_price), D(b, l->signed_units), + D(c, l->entry_commission), D(d, l->mark), D(e, l->unrealized_pnl), D(f, l->favorable_excursion), + D(g, l->adverse_excursion), s->lots[i].label, s->lots[i].comment); + } +} + +typedef struct run_out { + pf_report_t rep; + int rep_ok; + int drive_ok; + int setup_rc; + uint32_t setup_err, setup_field; + int bad_rc, good_rc; + pf_native_fx_curve_error_t bad_err, good_err; + uint64_t bad_idx, good_idx; + uint64_t hash, cont, shash; + pf_native_state_v1 state; + uint64_t recalc_driven, recalc_skipped; +} run_out; + +static void emit_run(host_state* st, run_out* ro) { + const char* p = st->tag; + const pf_report_t* r = &ro->rep; + char key[128], a[64], b[64], c[64], d[64], e[64], f[64], g[64], h[64], i2[64], j[64], rs[512], cs[256]; + int i; + snprintf(key, sizeof key, "%s.setup", p); + cfield(key, "status:%d;err:%u;field:%u", ro->setup_rc == PF_NATIVE_OK ? 0 : 1, ro->setup_err, ro->setup_field); + snprintf(key, sizeof key, "%s.fx_curve", p); + cfield(key, "bad:%d/%d/%llu;good:%d/%d/%llu", ro->bad_rc == 0 ? 0 : 1, (int)ro->bad_err, + (unsigned long long)ro->bad_idx, ro->good_rc == 0 ? 0 : 1, (int)ro->good_err, + (unsigned long long)ro->good_idx); + snprintf(key, sizeof key, "%s.report", p); + cfield(key, "trades:%d;trades_len:%d;net:%s;inputs:%lld;scripts:%lld;mag_sub:%lld;mag_ticks:%lld;itf:%d;stf:%d;" + "ratio:%d;agg:%d;mag:%d;eq_len:%lld;hash_len:%lld", + r->total_trades, r->trades_len, D(a, r->net_profit), (long long)r->input_bars_processed, + (long long)r->script_bars_processed, (long long)r->magnifier_sub_bars_total, + (long long)r->magnifier_sample_ticks_total, r->input_tf_seconds, r->script_tf_seconds, r->script_tf_ratio, + r->needs_aggregation, r->bar_magnifier_enabled, (long long)r->equity_curve_len, + (long long)r->broker_state_hash_len); + for (i = 0; i < r->trades_len; ++i) { + const pf_trade_t* t = &r->trades[i]; + const char* eid = strategy_closed_trade_entry_id(st->h, i); + const char* xid = strategy_closed_trade_exit_id(st->h, i); + const char* xc = strategy_closed_trade_exit_comment(st->h, i); + snprintf(key, sizeof key, "%s.trade.%02d", p, i); + cfield(key, "et:%lld;xt:%lld;ep:%s;xp:%s;pnl:%s;pct:%s;long:%d;ru:%s;dd:%s;qty:%s;com:%s;eb:%d;xb:%d;end:%d;" + "eid:%s;xid:%s;xc:%s;cause:%d;inc:%llu", + (long long)t->entry_time, (long long)t->exit_time, D(a, t->entry_price), D(b, t->exit_price), + D(c, t->pnl), D(d, t->pnl_pct), t->is_long, D(e, t->max_runup), D(f, t->max_drawdown), D(g, t->qty), + D(h, t->commission), t->entry_bar_index, t->exit_bar_index, t->open_at_end, eid ? eid : "", + xid ? xid : "", xc ? xc : "", strategy_closed_trade_close_cause(st->h, i), + (unsigned long long)strategy_closed_trade_entry_incarnation(st->h, i)); + } + for (i = 0; i < st->nbars && i < ACID_BARS; ++i) { + const pf_native_decision_v1* at = &st->bar_at[i]; + const uint64_t row = i < r->broker_state_hash_len ? r->broker_state_hash[i] : 0; + const double eq = i < r->equity_curve_len ? r->equity_curve[i].equity : NAN; + const double op = i < r->equity_curve_len ? r->equity_curve[i].open_profit : NAN; + risk_str(rs, sizeof rs, &st->bar_risk[i]); + snprintf(key, sizeof key, "%s.bar.%02d", p, i); + cfield(key, "ord:%llu;ii:%d;eff:%lld;sbo:%lld;floor:%lld;px:%s;qk:%d;prov:%d;ph:%d;comp:%d;sub:%d/%d/%d;" + "sess:%d%d%d;iv:%lld/%lld/%lld/%lld/%lld;sd:%lld/%lld;row:%llu;eq:%s;op:%s;risk:%s", + (unsigned long long)at->ordinal, at->interval_index, (long long)at->effective_time_ms, + (long long)at->script_bar_open_ms, (long long)at->decision_floor_ms, D(a, at->price), at->quote_kind, + at->provenance, at->path_phase, at->completion, at->sub_index, at->sub_count, at->is_terminal_sub_bar, + at->in_session, at->opens_session_day, at->closes_session_day, + (long long)at->script_interval_open_ms, (long long)at->script_interval_eligible_open_ms, + (long long)at->script_interval_last_traded_close_ms, (long long)at->script_interval_next_period_open_ms, + (long long)at->script_interval_next_input_open_ms, + at->has_session_day ? (long long)at->session_day_ordinal : -1LL, + at->has_next_input_session_day ? (long long)at->next_input_session_day_ordinal : -1LL, + (unsigned long long)row, D(b, eq), D(c, op), rs); + } + for (i = 0; i < st->nfills; ++i) { + const pf_native_applied_v1* x = &st->fills[i].a; + const pf_native_decision_v1* at = &st->fills[i].at; + snprintf(key, sizeof key, "%s.fill.%02d", p, i); + cfield(key, "gi:%d;ord:%llu;inc:%llu;lot:%llu;raw:%s;res:%s;tkt:%s;cu:%s;ou:%s;fw:%s;cb:%lld;ca:%lld;term:%d;" + "tr:%d;at:%llu/%d/%lld/%s/%d", + st->fills[i].gi, (unsigned long long)x->ordinal, (unsigned long long)x->incarnation, + (unsigned long long)x->opened_lot_incarnation, D(a, x->raw_price), D(b, x->resolved_price), + D(c, x->ticket), D(d, x->closed_units), D(e, x->opened_units), D(f, x->filled_working), + (long long)x->cycle_before, (long long)x->cycle_after, x->terminal, + x->has_terminal_reason ? (int)x->terminal_reason : -1, (unsigned long long)at->ordinal, + at->interval_index, (long long)at->effective_time_ms, D(g, at->price), at->quote_kind); + } + for (i = 0; i < st->ntfs; ++i) { + const tf_rec* t = &st->tfs[i]; + snprintf(key, sizeof key, "%s.tf.%02d", p, i); + cfield(key, "sub:%u;gi:%d;t:%lld;o:%s;h:%s;l:%s;c:%s;v:%s;comp:%u;at:%lld;iv:%lld/%lld/%lld/%lld/%lld;pull:%d", + t->sub, t->gi, (long long)t->bar.timestamp, D(a, t->bar.open), D(b, t->bar.high), D(c, t->bar.low), + D(d, t->bar.close), D(e, t->bar.volume), t->comp, (long long)t->delivered_at, + (long long)t->iv.open_ms, (long long)t->iv.eligible_open_ms, (long long)t->iv.last_traded_close_ms, + (long long)t->iv.next_period_open_ms, (long long)t->iv.next_input_open_ms, t->pulled); + } + for (i = 0; i < st->nrecalcs; ++i) { + const recalc_rec* x = &st->recalcs[i]; + const double nan = NAN; + risk_str(rs, sizeof rs, &x->risk); + snprintf(key, sizeof key, "%s.recalc.%02d", p, i); + cfield(key, "cause:%llu/%llu;gi:%d;part:%d/%s/%s/%s/%s/%s;px:%s;qk:%d;ord:%llu;risk:%s", + (unsigned long long)x->cord, (unsigned long long)x->cinc, x->gi, x->has_partial, + D(a, x->has_partial ? x->partial.open : nan), D(b, x->has_partial ? x->partial.high : nan), + D(c, x->has_partial ? x->partial.low : nan), D(d, x->has_partial ? x->partial.close : nan), + D(e, x->has_partial ? x->partial.volume : nan), D(f, x->at.price), x->at.quote_kind, + (unsigned long long)x->at.ordinal, rs); + } + snprintf(key, sizeof key, "%s.recalc.totals", p); + cfield(key, "driven:%llu;skipped:%llu", (unsigned long long)ro->recalc_driven, + (unsigned long long)ro->recalc_skipped); + { + int kinds[4] = {0, 0, 0, 0}, fx_n = 0, rq_n = 0; + uint64_t cd = ACID_FNV_OFFSET, rd = ACID_FNV_OFFSET; + for (i = 0; i < st->nchecks; ++i) { + const pf_native_margin_view_v1* q = &st->checks[i]; + ++kinds[q->kind & 3u]; + cd = acid_fnv_u64(cd, q->kind); + cd = acid_fnv_d(cd, q->signed_units); + cd = acid_fnv_d(cd, q->average_price); + cd = acid_fnv_u64(cd, q->lot_count); + cd = acid_fnv_d(cd, q->mark); + cd = acid_fnv_u64(cd, q->liquidation_resting ? 1u : 0u); + cd = acid_fnv_u64(cd, q->cursor_ordinal); + cd = acid_fnv_u64(cd, (uint64_t)q->cursor_effective_time_ms); + cd = acid_fnv_d(cd, q->cursor_t); + if (q->kind == PF_NATIVE_MARGIN_CHECK_FX_ROLL) { + cursor_str(cs, sizeof cs, q->cursor_ordinal, q->cursor_effective_time_ms, q->cursor_t, + q->cursor_interval_index, q->cursor_provenance, q->cursor_path_phase); + snprintf(key, sizeof key, "%s.margin.fxroll.%02d", p, fx_n++); + cfield(key, "units:%s;avg:%s;lots:%llu;mark:%s;rest:%u;cur:%s", D(a, q->signed_units), + D(b, q->average_price), (unsigned long long)q->lot_count, D(c, q->mark), + q->liquidation_resting, cs); + } + } + snprintf(key, sizeof key, "%s.margin.checks", p); + cfield(key, "n:%d;bar_open:%d;after_applied:%d;calc:%d;fx_roll:%d;digest:%llu", st->nchecks, kinds[0], + kinds[1], kinds[2], kinds[3], (unsigned long long)cd); + for (i = 0; i < st->nreqs; ++i) { + const pf_native_margin_view_v1* v = &st->reqs[i]; + rd = acid_fnv_u64(rd, v->kind); + rd = acid_fnv_d(rd, v->signed_units); + rd = acid_fnv_d(rd, v->mark); + rd = acid_fnv_d(rd, v->equity); + rd = acid_fnv_d(rd, v->required); + rd = acid_fnv_u64(rd, v->cursor_ordinal); + if (v->kind == PF_NATIVE_MARGIN_CHECK_FX_ROLL || v->required > v->equity) { + cursor_str(cs, sizeof cs, v->cursor_ordinal, v->cursor_effective_time_ms, v->cursor_t, + v->cursor_interval_index, v->cursor_provenance, v->cursor_path_phase); + snprintf(key, sizeof key, "%s.margin.req.%02d", p, rq_n++); + cfield(key, "kind:%u;units:%s;mark:%s;eq:%s;req:%s;cur:%s", v->kind, D(a, v->signed_units), + D(b, v->mark), D(c, v->equity), D(d, v->required), cs); + } + } + snprintf(key, sizeof key, "%s.margin.reqs", p); + cfield(key, "n:%d;digest:%llu", st->nreqs, (unsigned long long)rd); + } + for (i = 0; i < st->ncallviews; ++i) { + const pf_native_margin_view_v1* v = &st->callviews[i]; + cursor_str(cs, sizeof cs, v->cursor_ordinal, v->cursor_effective_time_ms, v->cursor_t, + v->cursor_interval_index, v->cursor_provenance, v->cursor_path_phase); + snprintf(key, sizeof key, "%s.margin.callview.%02d", p, i); + cfield(key, "units:%s;avg:%s;lots:%llu;mark:%s;eq:%s;req:%s;cur:%s", D(a, v->signed_units), + D(b, v->average_price), (unsigned long long)v->lot_count, D(c, v->mark), D(d, v->equity), + D(e, v->required), cs); + } + for (i = 0; i < st->ncalls; ++i) { + const pf_native_margin_call_v1* x = &st->calls[i]; + cursor_str(cs, sizeof cs, x->cursor_ordinal, x->cursor_effective_time_ms, x->cursor_t, + x->cursor_interval_index, x->cursor_provenance, x->cursor_path_phase); + snprintf(key, sizeof key, "%s.margin.call.%02d", p, i); + cfield(key, "ord:%llu;inc:%llu;app:%llu;side:%u;mark:%s;eq:%s;req:%s;liq:%s;u:%s;pb:%s;pa:%s;cur:%s", + (unsigned long long)x->ordinal, (unsigned long long)x->incarnation, + (unsigned long long)x->applied_ordinal, x->side, D(a, x->mark), D(b, x->equity), D(c, x->required), + D(d, x->liquidation_price), D(e, x->units), D(f, x->position_before), D(g, x->position_after), cs); + } + if (st->ncalls > 0) { + emit_lots(p, "CALLBEFORE", &st->lots_before); + emit_lots(p, "CALLAFTER", &st->lots_after); + } + for (i = 0; i < st->nliq; ++i) { + snprintf(key, sizeof key, "%s.liq.%s", p, st->liq[i].tag); + cfield(key, "%s", D(a, st->liq[i].v)); + } + for (i = 0; i < st->nlot_snaps; ++i) emit_lots(p, st->lot_snaps[i].tag, &st->lot_snaps[i]); + for (i = 0; i < st->nwsnaps; ++i) { + const working_snap* w = &st->wsnaps[i]; + int k; + snprintf(key, sizeof key, "%s.working.%s", p, w->tag); + cfield(key, "n:%d", w->n); + for (k = 0; k < w->n; ++k) { + const pf_native_working_v1* x = &w->rows[k].w; + const int from_owner = x->anchor == PF_NATIVE_ANCHOR_FROM_OWNER_FILL; + snprintf(key, sizeof key, "%s.working.%s.%02d", p, w->tag, k); + cfield(key, "inc:%llu;intent:%u/%s;trig:%u;p1:%s;p2:%s;rem:%u/%s;ts:%u;org:%u;owner:%u;anc:%d/%s;vis:%u;" + "grp:%u;lab:%s", + (unsigned long long)x->incarnation, x->intent, D(a, x->intent_value), x->trigger, D(b, x->p1), + D(c, x->p2), x->remaining_kind, + D(d, x->remaining_kind == PF_NATIVE_REMAINING_UNITS ? x->remaining_units : 0.0), x->trigger_state, + x->origin, x->owner, from_owner, D(e, from_owner ? x->anchor_offset : 0.0), + x->owner == PF_NATIVE_OWNER_WAIT_FOR_APPLIED ? x->visibility : 0u, x->group_kind, + w->rows[k].label); + } + } + for (i = 0; i < st->ntrails; ++i) { + const trail_rec* t = &st->trails[i]; + snprintf(key, sizeof key, "%s.trail.%02d", p, t->gi); + if (t->rc == PF_NATIVE_OK) { + cfield(key, "act:%u;best:%s;lvl:%s;aord:%llu", t->s.activated, D(a, t->s.best_price), + D(b, t->s.current_level), (unsigned long long)t->s.activation_ordinal); + } else { + cfield(key, "absent"); + } + } + for (i = 0; i < st->nanchors; ++i) { + const pf_native_anchored_level_view_v1* v = &st->anchors[i]; + cursor_str(cs, sizeof cs, v->owner_cursor_ordinal, v->owner_cursor_effective_time_ms, v->owner_cursor_t, + v->owner_cursor_interval_index, v->owner_cursor_provenance, v->owner_cursor_path_phase); + snprintf(key, sizeof key, "%s.anchor.%02d", p, i); + cfield(key, "owner:%llu;app:%llu;lot:%llu;fill:%s;leg:%llu;side:%u;trig:%u;off:%s;tick:%s;lvl:%s;cur:%s", + (unsigned long long)v->owner, (unsigned long long)v->owner_applied_ordinal, + (unsigned long long)v->owner_lot_incarnation, D(a, v->owner_fill_price), (unsigned long long)v->leg, + v->leg_side, v->trigger, D(b, v->offset), D(c, v->price_tick), D(d, v->kernel_level), cs); + } + for (i = 0; i < st->nrisks; ++i) { + risk_str(rs, sizeof rs, &st->risks[i].r); + snprintf(key, sizeof key, "%s.risk.%s", p, st->risks[i].tag); + cfield(key, "%s", rs); + } + for (i = 0; i < st->nrefusals; ++i) { + snprintf(key, sizeof key, "%s.refusal.%s", p, st->refusals[i].id); + cfield(key, "%s;same:%d", st->refusals[i].word, st->refusals[i].same); + } + for (i = 0; i < st->nsized; ++i) { + snprintf(key, sizeof key, "%s.sized.%s", p, st->sized[i].tag); + cfield(key, "%s", D(a, st->sized[i].v)); + } + for (i = 0; i < st->nappends; ++i) { + snprintf(key, sizeof key, "%s.append.%s", p, st->appends[i].id); + cfield(key, "%s/%d", append_word(st->appends[i].error), st->appends[i].index); + } + snprintf(key, sizeof key, "%s.bracket", p); + cfield(key, "tp:%d;sl:%d;trail:0;all:%d", st->tp_accepted ? 2 : 3, st->sl_accepted ? 2 : 3, + st->tp_accepted && st->sl_accepted); + { + char xs[512] = ""; + size_t used = 0; + for (i = 0; i < st->nxrep; ++i) + used += (size_t)snprintf(xs + used, sizeof xs - used, "%s%s", i ? "," : "", st->x_replaces[i]); + snprintf(key, sizeof key, "%s.x_replaces", p); + cfield(key, "%s", st->nxrep ? xs : "none"); + } + { /* Every event, read back page by page (native_c_api.h:2796-2810). */ + static pf_native_event_v1 page[256]; + int counts[32]; + uint64_t after = 0, digest = ACID_FNV_OFFSET; + long total = 0; + int n, k; + memset(counts, 0, sizeof counts); + for (;;) { + for (k = 0; k < 256; ++k) { + memset(&page[k], 0, sizeof page[k]); + page[k].struct_size = (uint32_t)sizeof page[k]; + page[k].version = PF_NATIVE_API_VERSION; + } + n = strategy_native_events_v1(st->h, after, page, 256); + if (n <= 0) break; + for (k = 0; k < n; ++k) { + if (page[k].kind < 32) ++counts[page[k].kind]; + digest = acid_event_fold(digest, &page[k]); + } + total += n; + after = page[n - 1].ordinal; + } + snprintf(key, sizeof key, "%s.events.n", p); + cfield(key, "%ld", total); + for (k = 0; k < 32; ++k) { + if (!counts[k]) continue; + snprintf(key, sizeof key, "%s.events.kind.%02d", p, k); + cfield(key, "%d", counts[k]); + } + snprintf(key, sizeof key, "%s.events.digest", p); + cfield(key, "%llu", (unsigned long long)digest); + } + snprintf(key, sizeof key, "%s.counts", p); + cfield(key, "inputs:%d;subbars:%d;begins:%d", st->inputs, st->sub_bars, st->begins); + snprintf(key, sizeof key, "%s.final", p); + cfield(key, "hash:%llu;cont:%llu;shash:%llu;life:%u;phase:%u;comp:%u;fail:%u/%u;floor:%lld;hw:%llu;drive:%d", + (unsigned long long)ro->hash, (unsigned long long)ro->cont, (unsigned long long)ro->shash, + ro->state.lifecycle, ro->state.phase, ro->state.completion, ro->state.failure_code, + ro->state.failure_operation, (long long)ro->state.decision_floor_ms, + (unsigned long long)ro->state.consumed_high_water, ro->drive_ok); + (void)i2; (void)j; +} + +/* ── One run through the C API ──────────────────────────────────────── */ +static host_state* run_c(const char* tag, int stream, int magnify, int typed_config_checks) { + host_state* st = (host_state*)calloc(1, sizeof(host_state)); + run_out ro; + pf_native_callbacks_v1 cb; + pf_native_run_spec_v1 base = base_spec(ACID_TICK); + pf_native_run_spec_ext_v1 ext; + int i; + if (!st) return NULL; + memset(&ro, 0, sizeof ro); + st->tag = tag; + st->stream = stream; + st->magnify = magnify; + st->bars = ACID_BARS; + memset(&cb, 0, sizeof cb); + cb.struct_size = (uint32_t)sizeof cb; + cb.version = PF_NATIVE_API_VERSION; + cb.user = st; + cb.on_run_begin = cb_run_begin; + cb.on_input = cb_input; + cb.on_bar = cb_bar; + cb.on_applied = cb_applied; + cb.on_timeframe_bar = cb_timeframe_bar; + cb.on_margin_call = cb_margin_call; + cb.on_recalculate = cb_recalculate; + cb.on_sub_bar = cb_sub_bar; + cb.on_margin_requirement = cb_margin_requirement; + cb.on_margin_check = cb_margin_check; + cb.on_margin_call_units = cb_margin_call_units; + cb.on_anchored_level = cb_anchored_level; + st->h = strategy_native_host_create_v1(&cb); + if (!st->h) { + line("FAIL C0 c-host: strategy_native_host_create_v1 refused the table"); + ++g_fail; + return st; + } + ext = ext_spec(st); + if (typed_config_checks) { + /* C1: the typed configuration path on the handle the run then uses. A + * grid with no ladder is refused by name and the handle stays + * Unconfigured and usable (native_c_api.h:3062-3066, :3088-3099). */ + pf_native_run_spec_v1 bad = base_spec(0.0); + uint32_t err = 999, field = 999; + pf_native_state_v1 s; + const int rc = strategy_configure_native_ext_result_v1(st->h, &bad, &ext, &err, &field); + memset(&s, 0, sizeof s); + s.struct_size = (uint32_t)sizeof s; + s.version = PF_NATIVE_API_VERSION; + strategy_native_state_v1(st->h, &s); + cfield("cfg.refusal", "status:%d;err:%u;field:%u", rc == PF_NATIVE_OK ? 0 : 1, err, field); + ccheck("C1", "c-typed-configuration", rc == PF_NATIVE_E_ARGUMENT && err == PF_NATIVE_SPEC_ERROR_GRID_REQUIRES_PRICE_TICK + && field == PF_NATIVE_SPEC_FIELD_PRICE_GRID && s.lifecycle == PF_NATIVE_LIFECYCLE_UNCONFIGURED, + "a refused spec answers E_ARGUMENT, GRID_REQUIRES_PRICE_TICK at PRICE_GRID, handle still UNCONFIGURED"); + } + ro.setup_err = 999; + ro.setup_field = 999; + ro.setup_rc = strategy_configure_native_ext_result_v1(st->h, &base, &ext, &ro.setup_err, &ro.setup_field); + if (typed_config_checks) { + uint32_t err = 999, field = 999; + pf_native_state_v1 s; + const int again = strategy_configure_native_ext_result_v1(st->h, &base, &ext, &err, &field); + memset(&s, 0, sizeof s); + s.struct_size = (uint32_t)sizeof s; + s.version = PF_NATIVE_API_VERSION; + strategy_native_state_v1(st->h, &s); + ccheck("C1", "c-typed-configuration", ro.setup_rc == PF_NATIVE_OK && ro.setup_err == 0 && ro.setup_field == 0, + "the same handle then configures: OK with NONE/NONE written"); + ccheck("C1", "c-typed-configuration", again == PF_NATIVE_E_STATE && err == PF_NATIVE_SPEC_ERROR_WRONG_PHASE + && field == PF_NATIVE_SPEC_FIELD_NONE && s.lifecycle == PF_NATIVE_LIFECYCLE_READY, + "configuring a Ready handle answers E_STATE, WRONG_PHASE/NONE, and leaves it Ready"); + } + if (ro.setup_rc != PF_NATIVE_OK) { + line("FAIL C0 c-host: configure refused err %u field %u: %s", ro.setup_err, ro.setup_field, + strategy_get_last_error(st->h)); + ++g_fail; + return st; + } + { /* The typed FX curve: refused while Ready (unordered), then applied. */ + int64_t t_bad[2] = {ACID_FX_T1, ACID_FX_T0}, t_good[2] = {ACID_FX_T0, ACID_FX_T1}; + double rates_bad[2] = {ACID_FX_R1, ACID_FX_R0}, rates[2] = {ACID_FX_R0, ACID_FX_R1}; + pf_native_fx_curve_v1 c; + memset(&c, 0, sizeof c); + c.struct_size = (uint32_t)sizeof c; + c.n = 2; + c.effective_from_ms = t_bad; + c.account_per_quote = rates_bad; + ro.bad_rc = strategy_configure_native_fx_curve_ext_v1(st->h, &c, &ro.bad_err, &ro.bad_idx); + c.effective_from_ms = t_good; + c.account_per_quote = rates; + ro.good_rc = strategy_configure_native_fx_curve_ext_v1(st->h, &c, &ro.good_err, &ro.good_idx); + if (typed_config_checks) { + ccheck("C1", "c-typed-configuration", ro.bad_rc != 0 && ro.bad_err == PF_NATIVE_FX_CURVE_ERROR_NOT_STRICTLY_INCREASING + && ro.bad_idx == 1 && ro.good_rc == 0, + "the typed FX curve: NOT_STRICTLY_INCREASING at 1, then the good curve applies"); + } + } + strategy_set_broker_state_hash_recording(st->h, 1); + memset(&ro.rep, 0, sizeof ro.rep); + if (!stream) { + uint32_t aerr = 999; + int32_t aidx = -1; + ro.drive_ok = strategy_native_run_v1(st->h, g_b15, ACID_BARS, &ro.rep) == PF_NATIVE_OK; + ro.rep_ok = 1; + strategy_native_append_auxiliary_bars_ext_v1(st->h, &g_b5[0], 1, &aerr, &aidx); + snprintf(st->appends[st->nappends].id, 24, "after_batch"); + st->appends[st->nappends].error = aerr; + st->appends[st->nappends++].index = aidx; + } else { + uint32_t aerr; + int32_t aidx; + ro.drive_ok = strategy_stream_begin(st->h, g_b15, ACID_WARMUP, "15", "15") == 0; + for (i = ACID_WARMUP; i < ACID_BARS && ro.drive_ok; ++i) { + if (i == ACID_WARMUP + 2) { + pf_bar_t pair[2]; + pf_bar_t stale = g_b5[3 * (i - 1)]; + pair[0] = g_b5[3 * i + 1]; + pair[1] = g_b5[3 * i]; + aerr = 999; aidx = -1; + strategy_native_append_auxiliary_bars_ext_v1(st->h, pair, 2, &aerr, &aidx); + snprintf(st->appends[st->nappends].id, 24, "unordered_pair"); + st->appends[st->nappends].error = aerr; + st->appends[st->nappends++].index = aidx; + aerr = 999; aidx = -1; + strategy_native_append_auxiliary_bars_ext_v1(st->h, &stale, 1, &aerr, &aidx); + snprintf(st->appends[st->nappends].id, 24, "stale_bar"); + st->appends[st->nappends].error = aerr; + st->appends[st->nappends++].index = aidx; + } + aerr = 999; aidx = -1; + if (strategy_native_append_auxiliary_bars_ext_v1(st->h, &g_b5[3 * i], 3, &aerr, &aidx) != PF_NATIVE_OK) { + line("C append refused at %d: %s", i, append_word(aerr)); + ro.drive_ok = 0; + } + ro.drive_ok = ro.drive_ok && strategy_stream_push_bar(st->h, &g_b15[i]) == 0; + } + ro.drive_ok = ro.drive_ok && strategy_stream_end(st->h, 0) == 0; + aerr = 999; aidx = -1; + strategy_native_append_auxiliary_bars_ext_v1(st->h, &g_b5[0], 1, &aerr, &aidx); + snprintf(st->appends[st->nappends].id, 24, "after_end"); + st->appends[st->nappends].error = aerr; + st->appends[st->nappends++].index = aidx; + ro.rep_ok = strategy_stream_fill_report(st->h, &ro.rep) == 0; + ro.shash = strategy_stream_state_hash(st->h); + } + ro.hash = strategy_broker_state_hash(st->h); + strategy_native_continuation_hash_v1(st->h, &ro.cont); + memset(&ro.state, 0, sizeof ro.state); + ro.state.struct_size = (uint32_t)sizeof ro.state; + ro.state.version = PF_NATIVE_API_VERSION; + strategy_native_state_v1(st->h, &ro.state); + strategy_native_recalculations_v1(st->h, &ro.recalc_driven, &ro.recalc_skipped); + emit_run(st, &ro); + if (typed_config_checks) { + ccheck("C1", "c-typed-configuration", ro.drive_ok && ro.state.lifecycle == PF_NATIVE_LIFECYCLE_COMPLETED, + "the run on the handle that was first refused completes"); + } + ccheck("C2", "c-interval-query-phase", + st->interval_outside_rc == PF_NATIVE_E_STATE && st->interval_inside_bad == 0 && st->ntfs > 0, + "strategy_native_timeframe_bar_interval_v1: OK inside every on_timeframe_bar, E_STATE outside"); + if (ro.rep_ok) strategy_native_report_free_v1(&ro.rep); + return st; +} + +int acid_c_main(acid_sink_fn sink, void* user) { + host_state* runs[3]; + int i; + g_sink = sink; + g_user = user; + g_fail = 0; + acid_build_tape(g_b15, g_b5); + line("# acid composite, C half: native_c_api.h only"); + runs[0] = run_c("batch", 0, 0, 1); + runs[1] = run_c("stream", 1, 0, 0); + runs[2] = run_c("mag", 0, 1, 0); + /* What the C++ half prints and C cannot: named, with the doc line (read on + * tree 6df7850a). The first four are C-SURFACE-2, ruled to 1.1.0; two of + * them have no exclusion sentence on the tree yet. */ + line("EXCLUDED C-SURFACE-2:closed-trade-entry-comment the report-row accessors are entry_id / exit_id / " + "exit_comment / close_cause (pineforge.h:1171, :1184, :1186, :1253) and pf_trade_t has no comment " + "field (pineforge.h:174-211); no sentence on the tree names the entry comment's absence; ruled to 1.1.0"); + line("EXCLUDED C-SURFACE-2:applied-origin-label pf_native_applied_v1 carries neither the request's origin " + "nor its label (native_c_api.h:1512-1531); no sentence on the tree names the absence; ruled to 1.1.0"); + line("EXCLUDED C-SURFACE-2:replace-options keep_handle / keep_binding: \"neither has a C spelling\" " + "(native-engine.md:936; native_host.hpp:1206 \"No C spelling: the C replace takes no options\"); the C " + "twin re-issues X with a plain replace; ruled to 1.1.0"); + line("EXCLUDED C-SURFACE-2:inspect_current_execution [--] in the COVERAGE block (native_c_api.h:101-104; " + "native-engine.md:3929-3932; native_host.hpp:1070-1071); ruled to 1.1.0"); + line("EXCLUDED COVERAGE:closes_session_day_open_ended no byte in pf_native_decision_v1, the third of the " + "COVERAGE block's recorded asymmetries (native_c_api.h:195-203, :1428-1431)"); + line("EXCLUDED PROTECTED:broker_state_hash_from_execution_hash a protected BacktestEngine member " + "(engine.hpp:413), not a @host-seam: no C spelling by construction"); + line("EXCLUDED NOC:quote_origin_ordinal NativeCurrentPointView::quote_origin_ordinal (native_host.hpp:650) " + "has no field in pf_native_decision_v1 (native_c_api.h:1440-1486) and is not among the recorded " + "asymmetries (native_c_api.h:169-203)"); + line("EXCLUDED NOC:driver_statistics NativeDecisionContext::driver_statistics (market_driver.hpp:166) has " + "no field in pf_native_decision_v1 and is not among the recorded asymmetries; the report's magnifier " + "counters (pf_report_t) are the shared totals"); + for (i = 0; i < 3; ++i) { + if (runs[i]) { + if (runs[i]->h) strategy_native_host_free(runs[i]->h); + free(runs[i]); + } + } + line("acid composite (C): %d C-side failures", g_fail); + return g_fail; +} + +#ifndef ACID_C_NO_MAIN +static void print_line(void* user, const char* l) { + (void)user; + printf("%s\n", l); +} +int main(void) { return acid_c_main(print_line, NULL) == 0 ? 0 : 1; } +#endif From 6c5f7467a7f1b4bfd50044dfd56e118ff7db5489 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:25:12 +0800 Subject: [PATCH 06/83] H-MEASURE: H12 mutation battery; the PR parity subset covers what the sweep catches AUDIT3-opus2 H12 (carried by AUDIT4 X14): measure the 30-probe pull-request subset against mutations. 44 single-line mutations (22 kernel: matching, fees, sizing, path order, sessions, timeframe aggregation; 10 adapter; 12 ta) plus 17 designed afterwards as a holdout, each built on spark and run over all 312 corpus probes, the unmutated tree 312/312 moved=0 before, between and after: the full sweep caught 39, the 30-probe subset 19. Escaped: ten ta.* arithmetic mutations, the AUTO path-order tie, an HTF aggregation low, strategy.risk.max_position_size's equality gate and the stop-limit entry (the last two with one corpus witness each, outside the 40-item scan the subset was built from). - scripts/corpus_parity_subset.txt: 24 probes appended (the original 30 untouched): the greedy cover of the escapes, one witness per ta class a generated strategy constructs, and the two holdout witnesses. The 54 catch all 39; subset-only reruns of the escaped mutations detect them. Its header no longer claims every one-witness mechanism by construction. - scripts/corpus_parity_mutation_battery.tsv: the 39 caught mutations with the probes that caught them. - scripts/test_corpus_parity_subset_cover.py, a new ci_preflight stage: fails when the subset stops witnessing a recorded mutation (it reports 20 escapes against the original 30). - docs/ci.md records the battery and the full-sweep timing it measured (19-24 s ten-wide, 113 s serial, after a 99 s build); the "30 probes" counts in docs/ci.md, CONTRIBUTING.md, README.md, check_corpus_parity.sh and the two workflows' comments now read 54, the historical timings are labelled as the first 30's. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 9c0579d2b332eaebe579a9b5baa7aa8dcedb8be6) INT26 pick note: the two workflow comments this commit re-counts were rewritten on main (57c89ff5, the advisory PR subset and the maintainers' parity status). The merged comments are main's text with this commit's count: corpus-parity.yml's "PR EARLY SIGNAL" and "advisory PR subset" sentences name the 54 probes; ci.yml's rewritten comment names no count. No other hunk differs from 9c0579d2 (hunkcheck). --- .github/workflows/corpus-parity.yml | 8 +-- CONTRIBUTING.md | 4 +- README.md | 2 +- docs/ci.md | 35 ++++++++++--- scripts/check_corpus_parity.sh | 11 +++-- scripts/ci_preflight.py | 4 ++ scripts/corpus_parity_mutation_battery.tsv | 45 +++++++++++++++++ scripts/corpus_parity_subset.txt | 46 +++++++++++++++-- scripts/test_corpus_parity_subset_cover.py | 57 ++++++++++++++++++++++ 9 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 scripts/corpus_parity_mutation_battery.tsv create mode 100644 scripts/test_corpus_parity_subset_cover.py diff --git a/.github/workflows/corpus-parity.yml b/.github/workflows/corpus-parity.yml index 9ef63432..c1b7691f 100644 --- a/.github/workflows/corpus-parity.yml +++ b/.github/workflows/corpus-parity.yml @@ -24,10 +24,10 @@ name: Corpus parity # the parity tooling itself. # # PR EARLY SIGNAL. The `corpus-parity-subset` job below -# re-runs the 30 probes scripts/corpus_parity_subset.txt names -- in parallel, +# re-runs the 54 probes scripts/corpus_parity_subset.txt names -- in parallel, # they write into disjoint directories -- and judges them against the same # pinned sha256 rows the nightly uses. Measured on the same laptop at JOBS=8, -# under sibling load: derive 2 s, build the runtime + the 30 strategy .so 68 s +# under sibling load, when the list held its first 30 probes: derive 2 s, build the runtime + the 30 strategy .so 68 s # from clean, run 45 s wall for 80 s of probe CPU, judge <1 s; 94 s end to end # over an up-to-date build directory, under 3 min cold. # .github/workflows/ci.yml calls it as a reusable workflow and includes it in @@ -156,7 +156,7 @@ jobs: path: corpus-parity-diagnostics/ if-no-files-found: warn - # The advisory PR subset: the 30 probes scripts/corpus_parity_subset.txt + # The advisory PR subset: the 54 probes scripts/corpus_parity_subset.txt # names, judged against the same pinned sha256 rows. Reached through CI. corpus-parity-subset: if: inputs.mode == 'subset' @@ -218,7 +218,7 @@ jobs: # A restored cache is not evidence. Every LFS pointer the corpus commits # under data/ records its content's sha256; check the bytes on disk # against it, so a corrupt or stale cache is "the feed is wrong" here and - # not "30 probes drifted" three steps later. + # not "54 probes drifted" three steps later. - name: Verify the chart feed against its Git-LFS pointers shell: bash run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 194ba34d..0c12ff36 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -162,7 +162,7 @@ The validation corpus is a **byte oracle**, not a smoke test. ```bash ./scripts/check_corpus_parity.sh # the full 312-probe sweep, ~32 min -./scripts/check_corpus_parity.sh --subset # the 30 probes a pull request waits for +./scripts/check_corpus_parity.sh --subset # the 54 probes a pull request waits for ``` It checks the corpus out at the gitlink this repository records, refuses to @@ -186,7 +186,7 @@ python3 scripts/corpus_trades_identity.py --update ``` The subset runs as a required check on a pull request; the full sweep runs -nightly and on demand. The details, including which 30 probes and why, are in +nightly and on demand. The details, including which 54 probes and why, are in `docs/ci.md`. ### "Expectation corrected" diff --git a/README.md b/README.md index 78843df1..b42edcf1 100644 --- a/README.md +++ b/README.md @@ -390,7 +390,7 @@ The gates a pull request passes, one line each: | Gate | Command | What it refuses | |---|---|---| -| TradingView parity | `./scripts/check_corpus_parity.sh --subset` | A trade that moved: 30 probes re-run and hashed against `scripts/corpus_parity_baseline.txt`. The full 312-probe sweep (`--subset` dropped) runs nightly. | +| TradingView parity | `./scripts/check_corpus_parity.sh --subset` | A trade that moved: 54 probes re-run and hashed against `scripts/corpus_parity_baseline.txt`. The full 312-probe sweep (`--subset` dropped) runs nightly. | | CTest row floors | `ci_verify.py release` / `kernel` | A test row that vanished: each profile counts the rows that actually ran against a floor. | | Kernel residuals | `scripts/check_kernel_residuals.py` | A TradingView-shaped name reaching the kernel archive or its installed headers without an ADR 0001 row. | | Feature rulings | `scripts/check_native_feature_rulings.py` | A `NativeRunSpec` field the adapter does not declare and the ADR does not rule. | diff --git a/docs/ci.md b/docs/ci.md index c0495e46..89cf62b8 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -346,7 +346,7 @@ population using the same byte oracle: ./scripts/check_corpus_parity.sh --subset ``` -builds and re-runs only the 30 probes +builds and re-runs only the 54 probes [`scripts/corpus_parity_subset.txt`](../scripts/corpus_parity_subset.txt) names — **in parallel**, which the full sweep's serial loop is not, because the probes write into disjoint directories and share only the read-only feeds — and @@ -356,8 +356,8 @@ judges them against the same pinned sha256 rows of never re-record the oracle it is measured by, and a row naming a probe the corpus does not commit is exit 2, never a pass. -The 30 were chosen mechanically, and the reason each one is in the list is on -its line. Every strategy.pine was scanned for the 40 engine mechanisms the +The first 30 were chosen mechanically, and the reason each one is in the list +is on its line. Every strategy.pine was scanned for the 40 engine mechanisms the corpus exercises at all — order kinds, exit shapes, OCA, pyramiding, the three commission kinds, the three sizing bases, slippage, margin, the three calculation-timing switches, `request.security` and `_lower_tf`, the magnifier, @@ -366,13 +366,32 @@ ta/math/array/matrix/map/UDT/drawing surfaces — a greedy cover took the fewest probes witnessing all 40, then one probe per corpus family of three or more members the cover had missed, then the keepers: the corpus's only anomaly-tier verdict, its two largest trade surfaces, and the mechanisms the campaign -measured as divergence-prone. Every mechanism with exactly one witness in the -corpus is therefore in the subset by construction — `strategy.cancel_all`, -`calc_on_order_fills=true`, `commission.cash_per_order`, a non-zero -`commission_value`, and `map.*`. +measured as divergence-prone. Every mechanism of that 40-item scan with +exactly one witness in the corpus is therefore in the subset by construction — +`strategy.cancel_all`, `calc_on_order_fills=true`, `commission.cash_per_order`, +a non-zero `commission_value`, and `map.*`. + +**Measured against mutations (R5 lane H-MEASURE, AUDIT3 H12).** A battery of +44 single-line mutations — kernel matching, fees, sizing, path order, sessions, +timeframe aggregation, adapter policy and the `ta.*` library — each built and +run over all 312 probes, then 17 more designed afterwards as a holdout: the full +sweep caught 39, and the first 30 rows caught 19 of them. What escaped was +mostly `ta.*` arithmetic (`wma`, `cci`, `mfi`, the crossover tie, `supertrend`, +`stdev`, `atr`, `dmi`, `linreg`, `roc`), plus the AUTO path-order tie, an +HTF aggregation low, `strategy.risk.max_position_size`'s equality gate and the +stop-limit entry — the last two mechanisms outside the 40-item scan with one +witness each. 24 probes were added: the greedy cover of those escapes, then one +witness per `ta` class a generated strategy constructs; the 54 catch all 39. +[`scripts/corpus_parity_mutation_battery.tsv`](../scripts/corpus_parity_mutation_battery.tsv) +records every caught mutation with the probes that caught it, and +`scripts/test_corpus_parity_subset_cover.py`, a `ci_preflight` stage, fails +when the subset stops witnessing one. The same run measured the full sweep +parallelised ten-wide on an idle 20-core host at 19–24 s (113 s serial on one +core) after a 99 s build: the serial loop of `scripts/run_corpus.sh` and the +hosted runner, not the probes, are what keep the whole population nightly. Measured on the same 16-core laptop at `JOBS=8`, corpus 442d497, under sibling -load: derive 2 s (a no-op when the feeds are fresh), build the runtime and the +load, when the list held its first 30 probes: derive 2 s (a no-op when the feeds are fresh), build the runtime and the 30 strategy `.so` 68 s from clean, run **45 s wall** for 80 s of probe CPU, judge <1 s — **94 s** end to end over an up-to-date build directory, against 1792 s for the run phase alone in full mode. diff --git a/scripts/check_corpus_parity.sh b/scripts/check_corpus_parity.sh index e0199bcf..94d20f8f 100755 --- a/scripts/check_corpus_parity.sh +++ b/scripts/check_corpus_parity.sh @@ -23,18 +23,19 @@ # # --subset — THE HALF A PULL REQUEST CAN WAIT FOR. The whole sweep is ~32 min, # so it cannot hold a merge (docs/ci.md). With --subset this builds and re-runs -# only the 30 probes scripts/corpus_parity_subset.txt names, in parallel, and +# only the 54 probes scripts/corpus_parity_subset.txt names, in parallel, and # judges them against the SAME pinned sha256 rows. Measured on a 16-core laptop -# at JOBS=8, corpus 442d497, under sibling load: derive 2 s (a no-op when the -# feeds are fresh), build the runtime + the 30 strategy .so 68 s from clean, +# at JOBS=8, corpus 442d497, under sibling load, when the list held its first 30 +# probes: derive 2 s (a no-op when the feeds are fresh), build the runtime + +# the 30 strategy .so 68 s from clean, # run 45 s wall for 80 s of probe CPU, judge <1 s — 94 s end to end over an # up-to-date build directory, against 1792 s for the run phase alone in full # mode. That fits the ~25 min a required check is budgeted for, which is why # .github/workflows/ci.yml can make it a dependency of the required `build` # context. # -# What --subset does NOT prove: the other 282 probes, and the tier headline -# (scripts/verify_corpus.py grades the whole population, so 30 runs cannot +# What --subset does NOT prove: the other 258 probes, and the tier headline +# (scripts/verify_corpus.py grades the whole population, so a subset run cannot # print its line). Both stay with the nightly full sweep. The subset is a # blocking floor, not a replacement. # diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index e948da8a..ebdef01e 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -196,6 +196,10 @@ def check_commands(source: Path) -> list[tuple]: [sys.executable, str(source / 'scripts/test_check_kernel_residuals.py')]), ('corpus-parity-identity-tests', [sys.executable, str(source / 'scripts/test_corpus_trades_identity.py')]), + # R5 lane H-MEASURE (AUDIT3 H12): the pull-request subset witnesses + # every mutation of the recorded battery the full sweep caught. + ('corpus-parity-subset-cover-tests', + [sys.executable, str(source / 'scripts/test_corpus_parity_subset_cover.py')]), ('design-inventory-tests', [sys.executable, str(source / 'scripts/test_check_design_inventory.py')]), ('design-inventory', diff --git a/scripts/corpus_parity_mutation_battery.tsv b/scripts/corpus_parity_mutation_battery.tsv new file mode 100644 index 00000000..81c2dd02 --- /dev/null +++ b/scripts/corpus_parity_mutation_battery.tsv @@ -0,0 +1,45 @@ +# The H12 mutation battery (lane H-MEASURE, 2026-09-26, tree r5/hmeasure 6df7850a, corpus eede4a2): +# every single-line mutation the FULL 312-probe sweep detected (27 of the 44 battery mutations, 12 of the +# 17 holdout ones), with the probes that detected it. The 22 neither sweep detected are not rows here. +# scripts/test_corpus_parity_subset_cover.py requires the PR subset to name at least one of them. +# Re-derive with the battery harness when the corpus pin or the engine moves a row. +id site mutation detected_by +K01 src/native_matching.hpp:260 slippage: sell-side sign bracket-aurora-market-atr-01,calendar-waypoint-weekday-macd-01,math-forge-log-channel-01,order-crosscurrent-opposite-market-pair-01,syntax-caliper-scalar-switch-01 +K03 src/native_matching.hpp:183 trigger region: touch at the level no longer counts (<= -> <) bracket-exit-stop-limit-trail-same-bar-01,bracket-rivet-calc-on-fill-01,bracket-trail-points-with-offset-only-01,bracket-trailing-activation-offset-path-01,order-dual-stop-both-touch-priority-01,order-harbor-stop-limit-01,order-opposite-entry-close-same-pass-01,pyramid-flip-stop-pyramiding-2-01 +K04 src/native_matching.hpp:122 tick rounding: half-up grid print -> floor analyzer-anvil-percent-costs-01,analyzer-cashflow-cash-costs-01,analyzer-parity-percent-of-equity-sizing-01,array-atlas-momentum-rotation-01,bands-constellation-mean-reversion-01,bracket-aurora-market-atr-01,calendar-waypoint-weekday-macd-01,composite-keystone-udf-tuple-01,map-mosaic-regime-weight-01,math-forge-log-channel-01,matrix-prism-factor-vote-01,mtf-orbit-trend-01,na-ledger-history-anchor-01,order-crosscurrent-opposite-market-pair-01,order-horizon-next-bar-close-01,order-percent-equity-cash-commission-01,order-switchback-all-in-reversal-01,pyramid-cash-fractional-commission-01,session-compass-utc-market-window-01,syntax-caliper-scalar-switch-01,udt-sentinel-method-regime-01,volume-tide-vwap-mfi-01 +K05 src/native_execution_consumer.cpp:483 path order AUTO: open-equidistant tie goes high-first (< -> <=) order-dual-side-same-id-stop-no-cancel-01,order-same-id-stop-modification-01,order-same-id-stop-raise-only-01,order-stale-stop-after-close-no-cancel-01 +K06 src/engine_orders.cpp:90 fee: exit commission dropped from closed-trade PnL analyzer-anvil-percent-costs-01,analyzer-cashflow-cash-costs-01,array-atlas-momentum-rotation-01,array-spectrum-deque-01,bands-constellation-mean-reversion-01,barstate-lattice-confirmed-pivot-01,bracket-aurora-market-atr-01,bracket-compass-partial-ladder-01,bracket-revision-exit-replace-01,bracket-rivet-calc-on-fill-01,calendar-waypoint-weekday-macd-01,cap-gatekeeper-intraday-risk-01,composite-keystone-udf-tuple-01,control-heliograph-while-channel-01,enum-relay-regime-01,map-archive-presence-01,map-mosaic-regime-weight-01,math-forge-log-channel-01,math-kiln-power-efficiency-01,matrix-cadence-transpose-pairs-01,matrix-eigen-covariance-01,matrix-prism-factor-vote-01,mtf-cascade-tuple-01,mtf-orbit-trend-01,mtf-rangefinder-daily-reference-01,na-ledger-history-anchor-01,order-crosscurrent-opposite-market-pair-01,order-flashpoint-immediate-close-01,order-harbor-stop-limit-01,order-horizon-next-bar-close-01,order-keystone-limit-replace-01,order-percent-equity-cash-commission-01,order-tranche-partial-market-close-01,order-vector-stop-breakout-01,pyramid-cash-fractional-commission-01,pyramid-terrace-staged-entry-01,session-borough-new-york-01,session-compass-utc-market-window-01,stats-ledger-outcome-throttle-01,syntax-caliper-scalar-switch-01,syntax-glyph-string-regex-01,ta-aperture-cog-linreg-01,ta-conduit-obv-accdist-01,ta-gyroscope-cmo-median-01,ta-mantle-keltner-regime-01,ta-meridian-dmi-regime-01,ta-pendulum-sar-rotation-01,ta-plumbline-pvt-vwma-01,ta-quarry-alma-correlation-01,ta-sextant-cci-hma-01,ta-tally-volume-index-roc-01,ta-torque-tsi-signal-01,ta-zenith-rci-wpr-01,timeframe-dial-main-period-01,udt-breaker-method-while-01,udt-courier-return-method-01,udt-loopback-method-for-01,udt-sentinel-method-regime-01,udt-switchboard-method-arms-01,udt-tessellate-tuple-method-01,volume-tide-vwap-mfi-01 +K09 src/engine_execution.cpp:381 fee: fused one-lot settlement quotes the close fee on the opening qty analyzer-anvil-percent-costs-01,analyzer-cashflow-cash-costs-01,array-atlas-momentum-rotation-01,array-spectrum-deque-01,bands-constellation-mean-reversion-01,barstate-lattice-confirmed-pivot-01,bracket-aurora-market-atr-01,bracket-compass-partial-ladder-01,bracket-revision-exit-replace-01,bracket-rivet-calc-on-fill-01,calendar-waypoint-weekday-macd-01,cap-gatekeeper-intraday-risk-01,composite-keystone-udf-tuple-01,control-heliograph-while-channel-01,enum-relay-regime-01,map-archive-presence-01,map-mosaic-regime-weight-01,math-forge-log-channel-01,math-kiln-power-efficiency-01,matrix-cadence-transpose-pairs-01,matrix-eigen-covariance-01,matrix-prism-factor-vote-01,mtf-cascade-tuple-01,mtf-orbit-trend-01,mtf-rangefinder-daily-reference-01,na-ledger-history-anchor-01,order-crosscurrent-opposite-market-pair-01,order-flashpoint-immediate-close-01,order-harbor-stop-limit-01,order-horizon-next-bar-close-01,order-keystone-limit-replace-01,order-tranche-partial-market-close-01,order-vector-stop-breakout-01,pyramid-cash-fractional-commission-01,pyramid-terrace-staged-entry-01,session-borough-new-york-01,session-compass-utc-market-window-01,stats-ledger-outcome-throttle-01,syntax-caliper-scalar-switch-01,syntax-glyph-string-regex-01,ta-aperture-cog-linreg-01,ta-conduit-obv-accdist-01,ta-gyroscope-cmo-median-01,ta-mantle-keltner-regime-01,ta-meridian-dmi-regime-01,ta-pendulum-sar-rotation-01,ta-plumbline-pvt-vwma-01,ta-quarry-alma-correlation-01,ta-sextant-cci-hma-01,ta-tally-volume-index-roc-01,ta-torque-tsi-signal-01,ta-zenith-rci-wpr-01,timeframe-dial-main-period-01,udt-breaker-method-while-01,udt-courier-return-method-01,udt-loopback-method-for-01,udt-sentinel-method-regime-01,udt-switchboard-method-arms-01,udt-tessellate-tuple-method-01,volume-tide-vwap-mfi-01 +K10 src/engine_path_resolve.cpp:99 entry-bar excursion mask: high/low path positions swapped bracket-tp-sl-oca-reduce-isolate-01,composite-bracket-cap-range-pending-stop-01,order-cross-entry-cancel-same-pass-01,order-deferred-flip-guaranteed-gap-stops-01,order-deferred-flip-pooc-cross-bar-01,order-dual-four-bar-stop-no-close-01,order-dual-side-same-id-stop-no-cancel-01,order-dual-stop-both-touch-priority-01,order-dual-stop-cancel-rotation-01,order-dual-stop-far-only-01,order-dual-stop-near-only-01,order-dual-stop-open-high-first-path-01,order-dual-stop-open-low-first-path-01,order-dual-stop-source-order-long-first-01,order-dual-stop-source-order-short-first-01,order-flip-stop-no-paired-close-01,order-harbor-stop-limit-01,order-keystone-limit-replace-01,order-one-side-four-bar-far-opposite-01,order-opposite-entry-close-same-pass-01,order-range-expansion-pending-stop-01,order-same-id-stop-after-flat-01,order-same-id-stop-cross-before-modify-01,order-same-id-stop-minute-zero-01,order-same-id-stop-modification-01,order-same-id-stop-raise-only-01,order-same-id-stop-window-four-bars-01,order-stale-stop-after-close-no-cancel-01,order-stop-cancel-no-regime-close-01,order-stop-entry-cancel-opposite-01,order-stop-entry-reversal-grouping-01,order-stop-entry-touch-boundary-01,order-vector-stop-breakout-01,pyramid-deferred-flip-close-all-01,pyramid-flip-stop-pyramiding-2-01 +K13 src/native_execution_consumer.cpp:608 sizing: percent-fee reserve applied when NOT requested analyzer-anvil-percent-costs-01,array-atlas-momentum-rotation-01,bands-constellation-mean-reversion-01,bracket-aurora-market-atr-01,calendar-waypoint-weekday-macd-01,composite-keystone-udf-tuple-01,map-mosaic-regime-weight-01,math-forge-log-channel-01,matrix-prism-factor-vote-01,mtf-orbit-trend-01,na-ledger-history-anchor-01,order-crosscurrent-opposite-market-pair-01,order-harbor-stop-limit-01,order-horizon-next-bar-close-01,order-keystone-limit-replace-01,order-vector-stop-breakout-01,session-compass-utc-market-window-01,syntax-caliper-scalar-switch-01,udt-sentinel-method-regime-01,volume-tide-vwap-mfi-01 +K15 src/native_execution_consumer.cpp:7877 chart aggregation (1m->15m script bar): low = last sub-bar low ltf-bool-array-bull-majority-01,ltf-numeric-float-ratio15-01 +K17 src/timeframe.cpp:1083 timeframe feed_merge_into_current: low keeps the max mtf-cascade-tuple-01,mtf-rangefinder-daily-reference-01 +K18 src/session_time.cpp:709 session window: end minute inclusive (< -> <=) cap-gatekeeper-intraday-risk-01,composite-4emarsi-session-window-nbar-exit-01,composite-vcp-integration-01,composite-vcp-session-tz-newyork-01,na-deep-history-int-na-01,session-borough-new-york-01,session-compass-utc-market-window-01,session-ny-spring-forward-dst-01 +K19 src/session_time.cpp:339 calendar: dayofweek shifted by one day analyzer-parity-edge-margin-50-pct-01,analyzer-parity-percent-of-equity-sizing-01,analyzer-parity-small-equity-fraction-01,anomaly-equity-mirror-strategy-equity-01,calendar-waypoint-weekday-macd-01,order-deferred-flip-guaranteed-gap-stops-01,order-deferred-flip-pooc-cross-bar-01 +A01 src/source/pine_adapter.cpp:2625 default MARKET sizing price: slippage applied on the wrong side analyzer-anvil-percent-costs-01,analyzer-cashflow-cash-costs-01,array-atlas-momentum-rotation-01,bands-constellation-mean-reversion-01,bracket-aurora-market-atr-01,calendar-waypoint-weekday-macd-01,composite-keystone-udf-tuple-01,map-mosaic-regime-weight-01,math-forge-log-channel-01,matrix-prism-factor-vote-01,mtf-orbit-trend-01,na-ledger-history-anchor-01,order-crosscurrent-opposite-market-pair-01,order-horizon-next-bar-close-01,session-compass-utc-market-window-01,syntax-caliper-scalar-switch-01,udt-sentinel-method-regime-01,volume-tide-vwap-mfi-01 +A03 src/source/pine_adapter.cpp:6014 pyramiding cap off-by-one (>= -> >) composite-bracket-cap-range-pending-stop-01,order-dual-stop-cancel-rotation-01,order-range-expansion-pending-stop-01,order-stop-cancel-no-regime-close-01,order-stop-entry-cancel-opposite-01,pyramid-cash-fractional-commission-01,pyramid-deferred-flip-close-all-01,pyramid-terrace-staged-entry-01,ta-closedtrades-risk-introspection-01 +A04 src/source/pine_adapter.cpp:13019 strategy.risk.max_position_size: equality no longer blocks (>= -> >) cap-risk-gates-allow-max-intraday-01 +A05 src/source/pine_strategy_host.cpp:915 source excursion fold: high/low path positions swapped analyzer-anvil-percent-costs-01,analyzer-parity-stop-limit-timing-01,array-atlas-momentum-rotation-01,bands-constellation-mean-reversion-01,barstate-lattice-confirmed-pivot-01,bracket-atr-trail-series-int-points-01,bracket-aurora-market-atr-01,bracket-compass-partial-ladder-01,bracket-entry-exit-same-pass-attach-01,bracket-exit-stop-limit-trail-same-bar-01,bracket-exit-three-way-set-once-entry-01,bracket-exit-tp-sl-fixed-01,bracket-narrow-stop-limit-with-trail8-01,bracket-partial-exit-qty-percent-01,bracket-revision-exit-replace-01,bracket-rivet-calc-on-fill-01,bracket-same-id-exit-replace-01,bracket-tp-sl-oca-reduce-isolate-01,bracket-trail-points-no-offset-explicit-01,bracket-trail-points-with-offset-only-01,bracket-trailing-activation-offset-path-01,calendar-waypoint-weekday-macd-01,cap-gatekeeper-intraday-risk-01,composite-bracket-cap-range-pending-stop-01,composite-ies-equity-feedback-sizing-01,composite-keystone-udf-tuple-01,composite-scalping-integration-01,composite-scalping-tight-tp-sl-points-01,composite-trendmaster-integration-01,composite-trendmaster-pivot-anchored-bracket-01,control-heliograph-while-channel-01,enum-relay-regime-01,magnifier-tick-dist-endpoints-01,magnifier-tick-dist-endpoints-rsi-cross-08a,magnifier-tick-dist-volume-weighted-on-01,map-mosaic-regime-weight-01,math-forge-log-channel-01,matrix-prism-factor-vote-01,mtf-htf-confluence-manual-trail-01,mtf-htf-confluence-static-bracket-01,mtf-orbit-trend-01,na-ledger-history-anchor-01,oca-exit-bracket-internal-cancel-01,oca-multi-bracket-isolation-01,oca-raw-strategy-order-reduce-01,order-cross-entry-cancel-same-pass-01,order-crosscurrent-opposite-market-pair-01,order-dual-four-bar-stop-no-close-01,order-dual-side-same-id-stop-no-cancel-01,order-dual-stop-both-touch-priority-01,order-dual-stop-far-only-01,order-dual-stop-near-only-01,order-dual-stop-open-high-first-path-01,order-dual-stop-open-low-first-path-01,order-dual-stop-open-tie-01,order-dual-stop-source-order-long-first-01,order-dual-stop-source-order-short-first-01,order-flip-stop-no-paired-close-01,order-harbor-stop-limit-01,order-horizon-next-bar-close-01,order-keystone-limit-replace-01,order-opposite-entry-close-same-pass-01,order-range-expansion-pending-stop-01,order-same-id-stop-minute-zero-01,order-same-id-stop-modification-01,order-same-id-stop-raise-only-01,order-stale-stop-after-close-no-cancel-01,order-stop-cancel-no-regime-close-01,order-stop-entry-reversal-grouping-01,order-vector-stop-breakout-01,pyramid-deferred-flip-close-all-01,pyramid-flip-stop-pyramiding-2-01,pyramid-terrace-staged-entry-01,session-compass-utc-market-window-01,stats-eventrades-zero-pnl-count-01,syntax-caliper-scalar-switch-01,ta-meridian-dmi-regime-01,ta-pivot-atr-stop-target-01,udt-method-extra-primitive-args-01,udt-method-feeds-strategy-exit-prices-01,udt-method-tuple-return-destructure-01,udt-method-uses-math-funcs-01,udt-sentinel-method-regime-01,volume-tide-vwap-mfi-01 +T01 src/ta_moving_averages.cpp:381 ta.wma weights off by one (feeds ta.hma) ta-hma-55-close-cross-01,ta-hma-fast-slow-cross-01,ta-sextant-cci-hma-01 +T02 src/ta_oscillators.cpp:387 ta.cci constant 0.015 -> 0.0015 ta-cci-threshold-cross-01,ta-sextant-cci-hma-01 +T03 src/ta_oscillators.cpp:459 ta.mfi money flow drops the volume factor ta-mfi-14-bands-20-80-01,volume-tide-vwap-mfi-01 +T04 src/ta_oscillators.cpp:109 ta.crossover: equal previous pair no longer counts (<= -> <) matrix-prism-factor-vote-01,ta-cmo-9-zero-cross-01,ta-gyroscope-cmo-median-01,ta-momentum-roc-zero-cross-01,ta-stochastic-rsi-cross-01,ta-wpr-14-bands-01,volume-tide-vwap-mfi-01,vwap-bands-breakout-1sigma-01,vwap-bands-mean-reversion-2sigma-01 +T05 src/ta_volatility_trend.cpp:239 ta.supertrend lower band drops the factor ta-supertrend-adx-filter-01,ta-supertrend-direction-flip-01 +T06 src/ta_volatility_trend.cpp:133 ta.stdev (biased): variance over n-1 bands-constellation-mean-reversion-01,composite-ies-bb-kc-squeeze-release-01,composite-vcp-integration-01,composite-vcp-vol-zscore-anomaly-01,control-heliograph-while-channel-01,ta-bb-kc-squeeze-breakout-01,ta-bb-rsi-mean-reversion-01,ta-linreg-stdev-channel-revert-01,ta-rsi-bb-self-bands-01,ta-stdev-sma-expansion-break-01,udt-method-calls-sibling-cumulative-01,udt-method-windowed-method-chain-01 +T07 src/ta_extremes_volume.cpp:299 ta.pivothigh: equal left high now invalidates (> -> >=) analyzer-parity-choch-bos-isolator-01,composite-boscurv-integration-01,composite-boscurv-pivot-bos-trigger-01,composite-ies-pivot-liquidity-sweep-01,composite-liqsweep-integration-01,composite-liqsweep-pivot-hh-ll-01,composite-liqsweep-wait-one-continuation-01,composite-liqsweep-wick-pierce-close-back-01,composite-marketshift-integration-01,composite-marketshift-pivot-state-machine-01,composite-marketshift-state-edge-detector-01,composite-trendmaster-integration-01,composite-trendmaster-line-new-projection-01,composite-trendmaster-pivot-anchored-bracket-01,composite-vcp-integration-01,composite-vcp-pivot-strength-5-01,ta-pivot-array-unshift-pop-01,ta-pivot-atr-stop-target-01,ta-pivot-confirmed-break-01 +T08 src/ta_volatility_trend.cpp:80 ta.atr true range drops |low - prev close| ta-supertrend-adx-filter-01,ta-supertrend-direction-flip-01 +T09 src/ta_volatility_trend.cpp:357 ta.dmi +DM drops the up_move > 0 condition composite-ies-equity-feedback-sizing-01,ta-dmi-adx-di-cross-01,ta-meridian-dmi-regime-01,ta-supertrend-adx-filter-01 +T11 src/ta_misc.cpp:71 ta.linreg offset off by one composite-boscurv-integration-01,composite-boscurv-linreg-slope-channel-01,ta-aperture-cog-linreg-01,ta-bb-kc-squeeze-breakout-01,ta-linreg-stdev-channel-revert-01 +T12 src/ta_oscillators.cpp:289 ta.roc lookback off by one array-spectrum-deque-01,matrix-eigen-covariance-01,order-flashpoint-immediate-close-01,ta-momentum-roc-zero-cross-01,ta-tally-volume-index-roc-01 +H01 src/engine_orders.cpp:100 percent PnL basis: lot qty instead of the closed qty (partial closes) bracket-compass-partial-ladder-01,bracket-partial-exit-qty-percent-01,oca-multi-bracket-isolation-01,order-tranche-partial-market-close-01 +H03 src/timeframe.cpp:1082 timeframe feed_merge_into_current: high keeps the min analyzer-self-test-multi-mode-01,mtf-cascade-tuple-01,mtf-daily-prev-high-break-01,mtf-rangefinder-daily-reference-01,mtf-roll-state-60-240-d-minimal-01 +H04 src/native_execution_consumer.cpp:7876 chart aggregation (1m->15m script bar): high = last sub-bar high ltf-bool-array-bull-majority-01,ltf-numeric-float-ratio15-01 +H06 src/source/pine_adapter.cpp:2698 stop-limit entry: stop and limit swapped order-harbor-stop-limit-01 +H09 src/source/pine_strategy_host.cpp:916 source excursion fold: low path position swapped analyzer-anvil-percent-costs-01,analyzer-parity-stop-limit-timing-01,array-atlas-momentum-rotation-01,bands-constellation-mean-reversion-01,barstate-lattice-confirmed-pivot-01,bracket-atr-trail-series-int-points-01,bracket-aurora-market-atr-01,bracket-compass-partial-ladder-01,bracket-entry-exit-same-pass-attach-01,bracket-exit-stop-limit-trail-same-bar-01,bracket-exit-three-way-set-once-entry-01,bracket-exit-tp-sl-fixed-01,bracket-narrow-stop-limit-with-trail8-01,bracket-partial-exit-qty-percent-01,bracket-revision-exit-replace-01,bracket-rivet-calc-on-fill-01,bracket-same-id-exit-replace-01,bracket-tp-sl-oca-reduce-isolate-01,bracket-trail-points-no-offset-explicit-01,bracket-trail-points-with-offset-only-01,bracket-trailing-activation-offset-path-01,calendar-waypoint-weekday-macd-01,cap-gatekeeper-intraday-risk-01,composite-bracket-cap-range-pending-stop-01,composite-ies-equity-feedback-sizing-01,composite-keystone-udf-tuple-01,composite-scalping-integration-01,composite-scalping-tight-tp-sl-points-01,composite-trendmaster-integration-01,composite-trendmaster-pivot-anchored-bracket-01,control-heliograph-while-channel-01,enum-relay-regime-01,magnifier-tick-dist-endpoints-01,magnifier-tick-dist-endpoints-rsi-cross-08a,magnifier-tick-dist-volume-weighted-on-01,map-mosaic-regime-weight-01,math-forge-log-channel-01,matrix-prism-factor-vote-01,mtf-htf-confluence-manual-trail-01,mtf-htf-confluence-static-bracket-01,mtf-orbit-trend-01,na-ledger-history-anchor-01,oca-exit-bracket-internal-cancel-01,oca-multi-bracket-isolation-01,oca-raw-strategy-order-reduce-01,order-cross-entry-cancel-same-pass-01,order-crosscurrent-opposite-market-pair-01,order-dual-four-bar-stop-no-close-01,order-dual-side-same-id-stop-no-cancel-01,order-dual-stop-both-touch-priority-01,order-dual-stop-far-only-01,order-dual-stop-near-only-01,order-dual-stop-open-high-first-path-01,order-dual-stop-open-low-first-path-01,order-dual-stop-open-tie-01,order-dual-stop-source-order-long-first-01,order-dual-stop-source-order-short-first-01,order-flip-stop-no-paired-close-01,order-harbor-stop-limit-01,order-horizon-next-bar-close-01,order-keystone-limit-replace-01,order-opposite-entry-close-same-pass-01,order-range-expansion-pending-stop-01,order-same-id-stop-minute-zero-01,order-same-id-stop-modification-01,order-same-id-stop-raise-only-01,order-stale-stop-after-close-no-cancel-01,order-stop-cancel-no-regime-close-01,order-stop-entry-reversal-grouping-01,order-vector-stop-breakout-01,pyramid-deferred-flip-close-all-01,pyramid-flip-stop-pyramiding-2-01,pyramid-terrace-staged-entry-01,session-compass-utc-market-window-01,stats-eventrades-zero-pnl-count-01,syntax-caliper-scalar-switch-01,ta-meridian-dmi-regime-01,ta-pivot-atr-stop-target-01,udt-method-extra-primitive-args-01,udt-method-feeds-strategy-exit-prices-01,udt-method-tuple-return-destructure-01,udt-method-uses-math-funcs-01,udt-sentinel-method-regime-01,volume-tide-vwap-mfi-01 +H10 src/ta_oscillators.cpp:136 ta.crossunder: equal previous pair no longer counts (>= -> >) composite-kanuck-kama-state-recurrence-01,ta-cmo-9-zero-cross-01,ta-stochastic-rsi-cross-01,volume-tide-vwap-mfi-01,vwap-bands-breakout-1sigma-01,vwap-bands-mean-reversion-2sigma-01 +H11 src/ta_extremes_volume.cpp:341 ta.pivotlow: equal left low now invalidates (< -> <=) analyzer-parity-choch-bos-isolator-01,composite-boscurv-integration-01,composite-boscurv-pivot-bos-trigger-01,composite-ies-pivot-liquidity-sweep-01,composite-liqsweep-integration-01,composite-liqsweep-pivot-hh-ll-01,composite-liqsweep-wait-one-continuation-01,composite-liqsweep-wick-pierce-close-back-01,composite-marketshift-integration-01,composite-marketshift-pivot-state-machine-01,composite-marketshift-state-edge-detector-01,composite-trendmaster-integration-01,composite-trendmaster-line-new-projection-01,composite-trendmaster-pivot-anchored-bracket-01,composite-vcp-pivot-strength-5-01,ta-pivot-array-unshift-pop-01,ta-pivot-atr-stop-target-01,ta-pivot-confirmed-break-01 +H12 src/ta_oscillators.cpp:149 ta.stoch: highest over src instead of high ta-stoch-slow-k-d-cross-01 +H13 src/ta_extremes_volume.cpp:492 ta.obv: an unchanged close counts as up (> -> >=) ta-conduit-obv-accdist-01,ta-obv-ema-cross-01 +H14 src/ta_moving_averages.cpp:423 ta.vwma drops the volume weight in the numerator ta-plumbline-pvt-vwma-01,ta-vwma-vs-sma-divergence-01 +H15 src/ta_moving_averages.cpp:461 ta.alma sigma inverted recompute-alma-sar-corr-magnifier-01,ta-quarry-alma-correlation-01 +H16 src/ta_misc.cpp:173 ta.correlation denominator drops the y mean term matrix-eigen-covariance-01,ta-quarry-alma-correlation-01 diff --git a/scripts/corpus_parity_subset.txt b/scripts/corpus_parity_subset.txt index 346f67db..11d82738 100644 --- a/scripts/corpus_parity_subset.txt +++ b/scripts/corpus_parity_subset.txt @@ -22,9 +22,13 @@ # campaign measured as divergence-prone: recompute state, risk rules, calendar # and timeframe resolution, trade statistics). # -# Every mechanism that exactly ONE probe in the whole corpus witnesses is in -# here by construction: strategy.cancel_all, calc_on_order_fills=true, -# commission.cash_per_order, a non-zero commission_value, and map.*. +# Every mechanism of that 40-item scan that exactly ONE probe in the whole +# corpus witnesses is in here by construction: strategy.cancel_all, +# calc_on_order_fills=true, commission.cash_per_order, a non-zero +# commission_value, and map.*. Mechanisms outside the scan were not covered by +# construction: the H12 mutation battery (below) found strategy.risk. +# max_position_size's equality gate and the stop-limit entry, each with one +# witness in the corpus and none in the first 30 rows. # # The subset is NOT a second oracle and has no baseline of its own: it names # probes, and scripts/corpus_trades_identity.py --subset judges them against @@ -63,3 +67,39 @@ validation/stats-ledger-outcome-throttle-01 # trade statistics re validation/ta-pivot-point-levels-break-01 # the ta.* family's largest surface: pivot_point_levels array returns validation/timeframe-main-period-self-adaptive-01 # timeframe.main_period resolution driving the script validation/udt-regime-stack-stress-01 # enum, flat type UDTs and var per-field state under stress + +# H12 mutation battery top-up (lane H-MEASURE, 2026-09-26): the greedy cover of every +# mutation the full sweep caught and the 30 rows above missed (battery: 44 single-line +# mutations, 27 caught by the full sweep, 13 escaped the rows above; the caught ones and the +# probes that catch them are scripts/corpus_parity_mutation_battery.tsv, which +# scripts/test_corpus_parity_subset_cover.py holds this file to). +validation/ta-supertrend-adx-filter-01 # ta.supertrend / ta.atr true range / ta.dmi +DM (battery T05, T08, T09) +validation/ta-momentum-roc-zero-cross-01 # ta.crossover equal-pair tie, ta.roc lookback (T04, T12) +validation/ta-sextant-cci-hma-01 # ta.wma weights under ta.hma, ta.cci constant (T01, T02) +validation/ta-bb-kc-squeeze-breakout-01 # ta.stdev (biased) and ta.linreg offset (T06, T11) +validation/cap-risk-gates-allow-max-intraday-01 # strategy.risk.max_position_size equality gate (A04) +validation/order-dual-side-same-id-stop-no-cancel-01 # AUTO path order: open-equidistant tie (K05) +validation/volume-tide-vwap-mfi-01 # ta.mfi money flow (volume factor) (T03) +validation/mtf-cascade-tuple-01 # request.security HTF low aggregation (timeframe feed merge) (K17) + +# ta.* class witnesses: every ta:: class a corpus generated.cpp constructs is constructed by at +# least one row of this file (greedy, larger TV tape first). Class-level witness is necessary, +# not sufficient: the battery's crossover-tie escape (T04) had 18 crossover users above. +validation/ta-highestbars-lowestbars-breakout-01 # ta.highest / lowest / highestbars / lowestbars +validation/ta-conduit-obv-accdist-01 # ta.obv, ta.accdist +validation/ta-plumbline-pvt-vwma-01 # ta.pvt, ta.vwma +validation/ta-zenith-rci-wpr-01 # ta.rci, ta.wpr +validation/vwap-bands-breakout-1sigma-01 # ta.vwap 3-tuple bands, anchored VWAP bands +validation/composite-ies-adx-regime-classify-01 # ta.rma, ta.tr +validation/ta-gyroscope-cmo-median-01 # ta.cmo, ta.median +validation/ta-tally-volume-index-roc-01 # ta.nvi, ta.pvi +validation/ta-stoch-slow-k-d-cross-01 # ta.stoch +validation/ta-cog-10-signal-cross-01 # ta.cog +validation/ta-tsi-25-13-signal-cross-01 # ta.tsi +validation/ta-dual-ma-switch-dispatch-01 # ta.wma (direct) +validation/array-atlas-momentum-rotation-01 # ta.percentrank +validation/control-heliograph-while-channel-01 # ta.stdev (direct) + +# holdout witnesses (H12 holdout battery: the two full-sweep detections subset-plus22 still missed) +validation/order-harbor-stop-limit-01 # strategy.entry with BOTH limit and stop: the corpus's only stop-limit entry witness (holdout H06) +validation/ta-quarry-alma-correlation-01 # ta.correlation on its compute path (holdout H16) and ta.alma diff --git a/scripts/test_corpus_parity_subset_cover.py b/scripts/test_corpus_parity_subset_cover.py new file mode 100644 index 00000000..9f164e06 --- /dev/null +++ b/scripts/test_corpus_parity_subset_cover.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""The pull-request parity subset must witness every mutation the full sweep caught in the +recorded mutation battery (scripts/corpus_parity_mutation_battery.tsv, R5 lane H-MEASURE). + +AUDIT3-opus2 H12 asked for the subset to be measured against mutations: 44 single-line kernel, +adapter and ta mutations plus 17 designed afterwards as a holdout, each built and run over the +whole 312-probe corpus. The battery file records the 39 the full sweep caught, each with the +probes that caught it; the original 30-row subset caught 19 of them. A subset row removed, or a +battery row added that no subset probe detects, fails here, in the source-only ci_preflight +stage, before any corpus build. +""" +import os +import sys +import unittest +from pathlib import Path + +sys.dont_write_bytecode = True +ROOT = Path(os.environ.get('PF_ROOT', Path(__file__).resolve().parents[1])) +sys.path.insert(0, str(ROOT / 'scripts')) + +from corpus_trades_identity import BASELINE, parse_subset # noqa: E402 + +SUBSET = Path(os.environ.get('SUBSET_FILE', ROOT / 'scripts' / 'corpus_parity_subset.txt')) +BATTERY = Path(os.environ.get('BATTERY_FILE', + ROOT / 'scripts' / 'corpus_parity_mutation_battery.tsv')) + + +def battery_rows(): + rows = [] + for line in BATTERY.read_text().splitlines(): + if not line or line.startswith('#') or line.startswith('id\t'): + continue + mid, site, mutation, probes = line.split('\t') + rows.append((mid, site, mutation, set(probes.split(',')))) + return rows + + +class SubsetCoversTheBattery(unittest.TestCase): + def test_every_caught_mutation_has_a_subset_witness(self): + subset = {p.split('/')[-1] for p in parse_subset(SUBSET.read_text())} + escaped = [f'{mid} {site} ({mutation}): detected only by {", ".join(sorted(probes))}' + for mid, site, mutation, probes in battery_rows() if not probes & subset] + self.assertEqual(escaped, [], f'{len(escaped)} recorded mutations escape {SUBSET.name}:\n ' + + '\n '.join(escaped)) + + def test_battery_names_only_pinned_probes(self): + pinned = {line.split()[1].split('/')[1] for line in BASELINE.read_text().splitlines() + if line and not line.startswith('#')} + unknown = sorted({p for *_, probes in battery_rows() for p in probes} - pinned) + self.assertEqual(unknown, []) + + def test_the_battery_is_not_empty(self): + self.assertGreaterEqual(len(battery_rows()), 20) + + +if __name__ == '__main__': + unittest.main(verbosity=2) From 3262e771c637a39d3ad3fae296a3b07a5f32eb07 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:27:34 +0800 Subject: [PATCH 07/83] H-MEASURE: put N12's seeded corpus experiment on the tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUDIT4 X14 row "N12's corpus half" (DONE-CLAIMED-ONLY: design §3.6.1 cited an uncommitted, env-gated experiment). Ruled, with its pin and a committed reproduction: - It cannot be a CTest row. Pine's strategy.risk statements reach the adapter on script bar 0, after project() built the run spec and configure_native digested it; prepare_native_begin is final and a second configure_native while Ready is refused, so no in-process test can seed the kernel's NativeRunSpec::risk for a Pine run without a source change. The pins stay the five probes' baseline rows and test_adapter_risk_relower's SW check. - scripts/check_seeded_risk_experiment.sh applies tests/fixtures/seeded_risk/n12-experiment.patch (N12's own experiment, recovered from its session record and ported; inert without PF_N12_ROUTE) to a throwaway copy, builds the runtime and the five risk probes, checks the unseeded runs still hash to the parity baseline, runs them with the kernel's rule seeded in the adapter's place and compares expected.tsv. On this tree (spark, 29 s) it reproduces every count the design states: 1502/1502 and 1464/1464 identical, 3840 of 3916, 312 of 604, 2370 of 2386 (two extra rows) on the ten TradingView-recorded columns. Exit 1 when the measurement moves, 2 when the patch no longer applies. Docs: design §3.6.1's acceptance evidence and ADR-0001's risk row cite the script; one contributing-llm.md anchor into the design re-anchored. No src/ or include/ change. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit bf5a11fbb4248d3f3bfbf5fcce1c610705c12f41) INT26 pick note: the ADR conflicted on citation digits only (int16_digit_merge, digits carried from the pre-pick tree). hunkcheck: every hunk of bf5a11fb carried; the ADR differs from its diff in anchor digits only. --- docs/adr/0001-kernel-adapter-boundary.md | 2 +- docs/design/native-feature-parity.md | 11 +- docs/pages/contributing-llm.md | 2 +- scripts/check_seeded_risk_experiment.sh | 55 ++++++++ tests/fixtures/seeded_risk/README.md | 33 +++++ tests/fixtures/seeded_risk/expected.tsv | 6 + .../fixtures/seeded_risk/n12-experiment.patch | 118 ++++++++++++++++++ tests/fixtures/seeded_risk/n12_diff.py | 48 +++++++ 8 files changed, 272 insertions(+), 3 deletions(-) create mode 100755 scripts/check_seeded_risk_experiment.sh create mode 100644 tests/fixtures/seeded_risk/README.md create mode 100644 tests/fixtures/seeded_risk/expected.tsv create mode 100644 tests/fixtures/seeded_risk/n12-experiment.patch create mode 100644 tests/fixtures/seeded_risk/n12_diff.py diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 5b240eaa..17c94a5e 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -593,7 +593,7 @@ 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:342`), `source_level_on_price_grid` (`:328`), `nearest_tick` (`:266`) / `source_bar_fill_tick` (`:287`) / `directional_tick` (`:311`), 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`, `tests/test_native_price_grid.cpp` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13867`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14922`), `chart_day_key` (`pine_adapter.cpp:13764`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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). `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` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13867`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14922`), `chart_day_key` (`pine_adapter.cpp:13764`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13226-13227`): 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:2360-2362`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index b27c84e8..dc3396df 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -538,7 +538,16 @@ the statements: ticket → day timezone → path check. the adapter half reproduced bit for bit against the 683a82f harvest, the kernel half pinned; corpus byte-identity not applicable (no `src/` or `include/` change); the five corpus risk probes measured as above with the -kernel seeded in an uncommitted, env-gated experiment. +kernel seeded in an env-gated experiment. That experiment is on the tree since +R5 lane H-MEASURE: `scripts/check_seeded_risk_experiment.sh` applies +`tests/fixtures/seeded_risk/n12-experiment.patch` to a throwaway copy, checks +that the unseeded probes still hash to the parity baseline, runs them with the +kernel's rule seeded in the adapter's place, and reproduces every count above +on `6df7850a` (counted on the ten TradingView-recorded columns, oldest row +first; the engine's own incarnation column renumbers a few rows earlier). It +stays a script, not a CTest row: seeding the kernel's `NativeRunSpec::risk` for a +Pine run needs the source change the patch makes, and the in-process pins +remain the five probes' baseline rows and the `SW` check above. **The P6 decision: native-only.** Re-verified on main `b8e7976e`: the ordering stands (`project()` at pine_strategy_host.cpp:324, `configure_native` at :325, diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 645ea88f..b82cf224 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -224,7 +224,7 @@ decision, and removing one is a regression: | the kernel's `max_abs_units` and the adapter's `max_position_size` | the kernel caps the *resulting* book, TradingView gates the *live* book before the fill. | ADR 0001 ruling table, row `max_abs_units` | | the kernel's `max_open_lots` and Pine's `pyramiding` | the adapter counts *entries per cycle*, the kernel counts *physical lots*. Measured against TradingView the kernel's count is the closer one (14 of 15 tape scenarios against the adapter's 8, `tests/test_pyramiding_count_differential.cpp`): TradingView checks an entry once, at its first eligible point, against the trades then open. The adapter keeps its count pending a re-lowering ruling. | ADR 0001 ruling table, row `max_open_lots` | | the kernel's margin model and the adapter's money admission | the adapter answers TradingView's ten-significant-digit admission itself and declares a *maintenance-only* model, because a positive initial requirement would decline openings TradingView takes and admit adds it refuses (measured both ways, `tests/test_adapter_margin_schedule_differential.cpp`). | ADR 0001 ruling table, row `initial_margin_fraction` | -| `NativeRunSpec::report_open_position_at_end` and the adapter's range-end rows | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it: report *shape*, not a mark-to-market row. | ADR 0001 ruling table, row `report_open_position_at_end`; design `native-feature-parity.md:615` | +| `NativeRunSpec::report_open_position_at_end` and the adapter's range-end rows | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it: report *shape*, not a mark-to-market row. | ADR 0001 ruling table, row `report_open_position_at_end`; design `native-feature-parity.md:624` | | `subscriptions` in the spec and the adapter's begin-time declaration | the adapter declares the same kernel subscriptions through a hook instead of the field, so a plain `request.security` site really is a kernel subscription. | ADR 0001 ruling table, row `subscriptions` | | the kernel's auxiliary feed and the adapter's auxiliary drive | the adapter's chart slice leaves pre-range coverage inert where the kernel folds by time, and evaluates after the bar's matching pass where the kernel delivers before it. | ADR 0001 ruling table, row `auxiliary_feed` | | `FeedTolerant` / `LegacyTolerant`, `NativeFeedTolerance` / `NativeLegacyTolerance` | deprecated spellings kept as exact aliases so existing hosts and the adapter compile unchanged; same value, same hash. | native_run_spec.hpp:311 and native_run_spec.hpp:353 | diff --git a/scripts/check_seeded_risk_experiment.sh b/scripts/check_seeded_risk_experiment.sh new file mode 100755 index 00000000..09daae13 --- /dev/null +++ b/scripts/check_seeded_risk_experiment.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# The corpus half of lane N12's strategy.risk measurement (design §3.6.1), as a re-runnable check +# that changes nothing under src/ (R5 lane H-MEASURE). Pine's risk statements reach the adapter on +# script bar 0, after the run spec is digested, so no in-process test can seed the kernel's +# NativeRunSpec::risk for a Pine run; the env-gated patch that does is a FIXTURE +# (tests/fixtures/seeded_risk/n12-experiment.patch), applied only to a throwaway copy of the tree, +# which builds the runtime + the five strategy.risk corpus probes and runs each probe +# (1) with PF_N12_ROUTE unset: must hash to scripts/corpus_parity_baseline.txt (the patch is inert), +# (2) routed + seeded (the kernel's rule instead of the adapter's): the TradingView-recorded +# columns must move exactly as tests/fixtures/seeded_risk/expected.tsv says. +# Exit 0 = reproduced, 1 = the measurement moved (re-measure, then re-pin design §3.6.1), 2 = could +# not run (the fixture no longer applies to this tree: re-anchor it). +# ROOT the tree to copy (default: this checkout; its corpus/ must hold the LFS feed) +# FIX the fixture directory (default: tests/fixtures/seeded_risk) +# WORK a scratch directory (default: mktemp -d) JOBS build parallelism (default 8) +# RUN a command prefix for every build and run step (e.g. "nice -n 10") +set -uo pipefail +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +ROOT=${ROOT:-$HERE}; FIX=${FIX:-$HERE/tests/fixtures/seeded_risk} +WORK=${WORK:-$(mktemp -d)}; JOBS=${JOBS:-8}; RUN=${RUN:-} +T0=$(date +%s) +rm -rf "$WORK/src" "$WORK/build" "$WORK/out"; mkdir -p "$WORK/out/base" "$WORK/out/seeded" +cp -a "$ROOT" "$WORK/src" && rm -f "$WORK"/src/corpus/validation/*/strategy.so "$WORK"/src/corpus/validation/*/strategy.dylib +(cd "$WORK/src" && patch -p1 --dry-run --quiet < "$FIX/n12-experiment.patch" >/dev/null) \ + || { echo "seeded-risk: the fixture patch no longer applies to this tree (exit 2)"; exit 2; } +(cd "$WORK/src" && patch -p1 --quiet < "$FIX/n12-experiment.patch") +PROBES=$(grep -v '^#' "$FIX/expected.tsv" | cut -f1) +TARGETS=$(for p in $PROBES; do printf 'strategy_validation_%s ' "$(echo "$p" | tr '-' '_')"; done) +# shellcheck disable=SC2086 # $RUN and $TARGETS are word lists on purpose +$RUN cmake -S "$WORK/src" -B "$WORK/build" -G Ninja -DCMAKE_BUILD_TYPE=Release -DPINEFORGE_VERSION_SOURCE=FILE \ + -DPINEFORGE_BUILD_TESTS=OFF -DPINEFORGE_BUILD_CORPUS_STRATEGIES=ON -Wno-dev > "$WORK/configure.log" 2>&1 \ + && $RUN ninja -C "$WORK/build" -j"$JOBS" pineforge $TARGETS > "$WORK/build.log" 2>&1 \ + || { tail -20 "$WORK/build.log"; echo "seeded-risk: build failed (exit 2)"; exit 2; } +(cd "$WORK/src" && python3 scripts/derive_corpus_feeds.py > /dev/null) +status=0 +while IFS=$'\t' read -r p seed ndiff rb rs first when; do + [[ "$p" == \#* || -z "$p" ]] && continue + (cd "$WORK/src" && $RUN python3 scripts/run_strategy.py "corpus/validation/$p" --so-name strategy.so \ + -o "$WORK/out/base/$p.csv" > "$WORK/out/base/$p.log" 2>&1) || { echo "$p: inert run failed"; exit 2; } + (cd "$WORK/src" && PF_N12_ROUTE=drawdown,cap PF_N12_SEED="$seed" $RUN python3 scripts/run_strategy.py \ + "corpus/validation/$p" --so-name strategy.so -o "$WORK/out/seeded/$p.csv" > "$WORK/out/seeded/$p.log" 2>&1) \ + || { echo "$p: seeded run failed"; exit 2; } + pinned=$(awk -v f="validation/$p/engine_trades.csv" '$2==f {print $1}' "$WORK/src/scripts/corpus_parity_baseline.txt") + got=$(python3 -c 'import hashlib,sys; print(hashlib.sha256(open(sys.argv[1]).read().encode()).hexdigest())' "$WORK/out/base/$p.csv") + line=$(N12_WIDTH=10 python3 "$FIX/n12_diff.py" "$WORK/out/base" "$WORK/out/seeded" "$p" | head -1) + if [[ "$ndiff" == 0 ]]; then want="identical on all $rs rows"; else want="differ=$ndiff of $(( rb > rs ? rb : rs )) first_chronological_row=$first (Entry"; fi + verdict=ok + [[ "$got" == "$pinned" ]] || { verdict="INERT-RUN-MOVED"; status=1; } + [[ "$line" == *"$want"* ]] || { verdict="${verdict/ok/}SEEDED-MOVED"; status=1; } + [[ "$ndiff" == 0 || "$line" == *"$when"* ]] || { verdict="${verdict/ok/}SEEDED-MOVED"; status=1; } + printf '%-45s inert=%s seeded:%s [%s]\n' "$p" "$([[ "$got" == "$pinned" ]] && echo baseline || echo MOVED)" "${line#*columns:}" "$verdict" + grep -h 'N12:' "$WORK/out/seeded/$p.log" | sed 's/^/ /' | sort -u | head -2 +done < "$FIX/expected.tsv" +echo "seeded-risk: $( ((status==0)) && echo REPRODUCED || echo MOVED ) in $(( $(date +%s) - T0 ))s (work dir $WORK)" +exit $status diff --git a/tests/fixtures/seeded_risk/README.md b/tests/fixtures/seeded_risk/README.md new file mode 100644 index 00000000..54c3dae8 --- /dev/null +++ b/tests/fixtures/seeded_risk/README.md @@ -0,0 +1,33 @@ +# Seeded strategy.risk corpus experiment (lane N12's corpus half, R5 lane H-MEASURE) + +Lane N12 ruled every `strategy.risk.*` rule a retention of the Pine adapter +(design §3.6.1): Pine's risk statements run inside the per-bar body, so the +adapter first learns a limit on script bar 0, after `project()` has built the +run spec and `configure_native` has digested it, and `NativeRunSpec::risk` is a +begin-time declaration. The paired scenarios of +`tests/test_adapter_risk_relower.cpp` measure each rule against the kernel's; +the corpus half — the five corpus probes that declare a risk rule, run with the +KERNEL's rule seeded in the adapter's place — needed a source change no test +can make in process, and was an uncommitted experiment until this fixture. + +- `n12-experiment.patch` — lane N12's env-gated experiment, recovered from its + session record and ported to this tree: with `PF_N12_ROUTE` set, `project()` + declares `spec.risk` from `PF_N12_SEED` (`drawdown:[:pct]`, `cap:`), + the adapter's own drawdown latch and fill-cap budget step aside, and the + kernel's first block is logged. With `PF_N12_ROUTE` unset it is inert. +- `expected.tsv` — per probe: the seed, and how many rows of the ten + TradingView-recorded columns differ from the unseeded run (oldest row first), + the row counts, and the first differing row. The engine's own trailing + columns (entry incarnation, range-end) renumber earlier and are not counted. +- `n12_diff.py` — the row comparison `expected.tsv` states. + +`scripts/check_seeded_risk_experiment.sh` applies the patch to a throwaway copy +of the tree, builds the runtime and the five probes, checks that the unseeded +run still hashes to `scripts/corpus_parity_baseline.txt`, runs each probe +seeded and compares the counts. Exit 0 reproduced, 1 the measurement moved, 2 +the patch no longer applies. On this tree (spark, 29 s): the drawdown probe +and `cap-risk-gates-allow-max-intraday-01` identical (1502 / 1464 rows), +`cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from +2025-04-08, `cap-gatekeeper-intraday-risk-01` 312 of 604 from 2025-10-25, +`composite-bracket-cap-range-pending-stop-01` 2370 of 2386 from 2025-04-02 +with two extra rows — the counts design §3.6.1 states. diff --git a/tests/fixtures/seeded_risk/expected.tsv b/tests/fixtures/seeded_risk/expected.tsv new file mode 100644 index 00000000..da7fbddc --- /dev/null +++ b/tests/fixtures/seeded_risk/expected.tsv @@ -0,0 +1,6 @@ +# probe seed tv_columns_rows_differ rows_base rows_seeded first_chronological_row first_row_time +ta-closedtrades-risk-introspection-01 drawdown:20:pct 0 1502 1502 - - +cap-risk-gates-allow-max-intraday-01 cap:3 0 1464 1464 - - +cap-max-intraday-filled-orders-isolate-01 cap:5 3840 3916 3916 77 2025-04-08 02:00 +cap-gatekeeper-intraday-risk-01 cap:4 312 604 604 293 2025-10-25 01:15 +composite-bracket-cap-range-pending-stop-01 cap:5 2370 2384 2386 17 2025-04-02 02:45 diff --git a/tests/fixtures/seeded_risk/n12-experiment.patch b/tests/fixtures/seeded_risk/n12-experiment.patch new file mode 100644 index 00000000..30ebc9fd --- /dev/null +++ b/tests/fixtures/seeded_risk/n12-experiment.patch @@ -0,0 +1,118 @@ +--- a/src/source/pine_adapter.cpp ++++ b/src/source/pine_adapter.cpp +@@ -2317,6 +2317,52 @@ + // entry must not consume a generic physical-lot cap before it fills, so + // the projected native spec deliberately leaves max_open_lots unbounded. + spec.allowed_open_directions = directions_for(risk_.direction); ++ // N12 EXPERIMENT (a fixture, env-gated): route the TradingView drawdown ++ // limit and/or the fill cap through NativeRunSpec::risk so the corpus ++ // risk probes measure the kernel rule against the adapter's own. ++ if (const char* n12 = std::getenv("PF_N12_ROUTE"); n12 && *n12) { ++ const std::string routes = n12; ++ NativeRiskLimits limits; ++ bool any = false; ++ // Pine's strategy.risk.* statements arrive per bar, after this ++ // projection; seed the values from PF_N12_SEED=drawdown:[:pct],cap:. ++ double seed_dd = 0.0; bool seed_dd_pct = false; int seed_cap = 0; ++ if (const char* seed = std::getenv("PF_N12_SEED")) { ++ std::string sv = seed; std::size_t pos = 0; ++ while (pos <= sv.size()) { ++ std::size_t next = sv.find(',', pos); if (next == std::string::npos) next = sv.size(); ++ const std::string item = sv.substr(pos, next - pos); ++ if (item.rfind("drawdown:", 0) == 0) { ++ seed_dd = std::atof(item.c_str() + 9); ++ seed_dd_pct = item.find(":pct") != std::string::npos; ++ } else if (item.rfind("cap:", 0) == 0) { ++ seed_cap = std::atoi(item.c_str() + 4); ++ } ++ pos = next + 1; ++ } ++ } ++ if (routes.find("drawdown") != std::string::npos && seed_dd > 0.0) { ++ NativeLossLimit limit; ++ limit.value = seed_dd; ++ limit.percent = seed_dd_pct; ++ limits.max_drawdown = limit; ++ limits.action = NativeRiskAction::BlockOpenings; ++ any = true; ++ } ++ if (routes.find("cap") != std::string::npos && seed_cap > 0) { ++ limits.max_fills_per_day = static_cast(seed_cap); ++ limits.day_basis = NativeRiskDay::CalendarDayInTimezone; ++ limits.action = NativeRiskAction::FlattenAndBlock; ++ const_cast(this)->cap.detach(); ++ any = true; ++ } ++ if (any) { ++ spec.risk = limits; ++ std::fprintf(stderr, "N12: spec.risk declared drawdown=%d fills=%u\n", ++ limits.max_drawdown ? 1 : 0, ++ limits.max_fills_per_day ? *limits.max_fills_per_day : 0u); ++ } ++ } + // Pine's frozen default sizing admits against its signal-time tuple. The + // generic initial-margin gate only sees the later fill-time FX rate, so + // source admission is reproduced in validate_precommit instead: every +@@ -13670,7 +13716,11 @@ + } + } + if (risk_.halted) return; +- if (risk_.max_drawdown > 0.0 && std::isfinite(risk_.observed_peak_equity)) { ++ static const bool n12_route_drawdown = [] { ++ const char* n12 = std::getenv("PF_N12_ROUTE"); ++ return n12 && std::string(n12).find("drawdown") != std::string::npos; ++ }(); ++ if (!n12_route_drawdown && risk_.max_drawdown > 0.0 && std::isfinite(risk_.observed_peak_equity)) { + const double threshold = risk_.max_drawdown_percent + ? risk_.observed_peak_equity * risk_.max_drawdown / 100.0 + : risk_.max_drawdown; +@@ -16563,6 +16613,16 @@ + if (!skip_quiet(QuietHook::TerminalExplicitMarket, bound && explicit_entries < 2)) + apply_terminal_explicit_market_policy(context); + update_risk_state(bar.close); ++ if (std::getenv("PF_N12_ROUTE")) { ++ const auto rs = require_host().native_risk_state(); ++ static bool n12_reported = false; ++ if (rs.blocked && !n12_reported) { ++ n12_reported = true; ++ std::fprintf(stderr, "N12: first kernel block at script bar %lld day=%lld fills_today=%llu\n", ++ (long long)context.script_bar_open_ms, (long long)rs.day_ordinal, ++ (unsigned long long)rs.fills_today); ++ } ++ } + if (stream_mode_) return; + // The native callback frame remains current after the source script + // returns. Reproduce the legacy once-per-script-bar margin checkpoint at +--- a/include/pineforge/compat/pine/intraday_cap.hpp ++++ b/include/pineforge/compat/pine/intraday_cap.hpp +@@ -87,6 +87,7 @@ + : attachment_(attachment) {} + + void attach() { attachment_ = CapAttachment::LegacySource; } ++ void detach() { attachment_ = CapAttachment::None; } + + // The old generated assignment is an explicit request for Pine behavior, + // even from an opted-out native fixture. It never silently drops a rule. +--- a/src/source/pine_strategy_host.cpp ++++ b/src/source/pine_strategy_host.cpp +@@ -11,6 +11,7 @@ + + #include + #include ++#include + #include + #include + #include +@@ -977,6 +978,12 @@ + } + + void source::PineStrategyHost::set_pine_risk_max_intraday_filled_orders(int limit) { ++ // N12 EXPERIMENT (a fixture): under a routed cap the statement must not ++ // re-attach the adapter's own budget. ++ if (const char* n12 = std::getenv("PF_N12_ROUTE"); ++ n12 && std::string(n12).find("cap") != std::string::npos && std::getenv("PF_N12_SEED")) { ++ return; ++ } + adapter_.cap = limit; + } + diff --git a/tests/fixtures/seeded_risk/n12_diff.py b/tests/fixtures/seeded_risk/n12_diff.py new file mode 100644 index 00000000..c555ef76 --- /dev/null +++ b/tests/fixtures/seeded_risk/n12_diff.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""n12_diff.py BASE_DIR EXP_DIR probe... -- row-level diff of engine_trades.csv (sha256 as the +identity judge hashes, universal newlines) the way the design doc states it: data rows, rows that +differ position by position, first differing row (1-based data row, CSV line = row + 1) and its +Date/time cell, extra rows, plus N12's own metric `diff base exp | grep -c '^[<>]'`.""" +import csv, hashlib, io, subprocess, sys +from pathlib import Path +base, exp = Path(sys.argv[1]), Path(sys.argv[2]) +for p in sys.argv[3:]: + a, b = (base / f'{p}.csv').read_text(), (exp / f'{p}.csv').read_text() + ra, rb = list(csv.reader(io.StringIO(a)))[1:], list(csv.reader(io.StringIO(b)))[1:] + n = max(len(ra), len(rb)) + diff = [i for i in range(n) if i >= len(ra) or i >= len(rb) or ra[i] != rb[i]] + first = diff[0] if diff else None + when = '' + if first is not None: + row = rb[first] if first < len(rb) else ra[first] + when = f' date={row[2]}' if len(row) > 2 else '' + dl = subprocess.run(f"diff '{base}/{p}.csv' '{exp}/{p}.csv' | grep -c '^[<>]'", shell=True, + capture_output=True, text=True).stdout.strip() + same = hashlib.sha256(a.encode()).hexdigest() == hashlib.sha256(b.encode()).hexdigest() + # the file is newest-first; count chronologically (oldest data row = row 1) too + ca, cb = ra[::-1], rb[::-1] + cdiff = [i for i in range(n) if i >= len(ca) or i >= len(cb) or ca[i] != cb[i]] + # the TradingView-recorded columns only (the tape header's width); the engine-only trailing + # columns ('Engine entry incarnation', 'Engine range-end') renumber whenever the kernel + # consumes an incarnation differently, without any trade moving + width = int(__import__('os').environ.get('N12_WIDTH', '0')) + if width: + tdiff = [i for i in range(n) if i >= len(ca) or i >= len(cb) or ca[i][:width] != cb[i][:width]] + tfirst = tdiff[0] if tdiff else None + twhen = '' + if tfirst is not None: + row = ca[tfirst] if tfirst < len(ca) else cb[tfirst] + twhen = f' ({row[1]} {row[2]})' + print(f' [{p}] first {width} columns: ' + + (f'differ={len(tdiff)} of {n} first_chronological_row={tfirst + 1}{twhen}' if tdiff + else f'identical on all {n} rows')) + cfirst = cdiff[0] if cdiff else None + cwhen = '' + if cfirst is not None: + row = ca[cfirst] if cfirst < len(ca) else cb[cfirst] + cwhen = f' ({row[1]} {row[2]})' if len(row) > 2 else '' + print(f'{p}: base_rows={len(ra)} exp_rows={len(rb)} ' + + ('IDENTICAL' if same else + f'differ(file order)={len(diff)} of {n}; differ(chronological)={len(cdiff)} of {n} ' + f'first_chronological_row={cfirst + 1}{cwhen}') + + f' diff_lines={dl}') From f6a86083f94ed092869a70ceb4bd1b9ead305899 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:32:59 +0800 Subject: [PATCH 08/83] H-MEASURE: the ctest floors count the lane's rows -- release 672 -> 684, kernel 271 -> 272 Counted with ctest -N on this tree, configured as ci_verify's release and kernel profiles: release registers 684 rows (the twelve H-MEASURE rows on top of 672, no row skips), kernel 273 (test_native_acid_composite is the lane's one source-free row; the WebSocket row still skips on a system libcurl, so 272 run). Each row is named beside the floor it raises. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 2b494497c8707c3365218cac4ae8d9548dc3c67b) INT26 pick note: scripts/ci_verify.py's two floors conflicted with INT25's (wave G: KERNEL 277, RELEASE 678). The merged floors add this lane's rows to INT25's -- KERNEL 277 + 1 = 278, RELEASE 678 + 12 = 690 -- with both comments kept; INT26 item (iv) recounts every floor with ctest -N per profile on the integrated tree. The four floor citations in CONTRIBUTING.md and contributing-llm.md name the merged lines (ci_verify.py:247 and :371). --- CONTRIBUTING.md | 4 ++-- docs/pages/contributing-llm.md | 6 +++--- scripts/ci_verify.py | 25 +++++++++++++++++++++++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c12ff36..887115b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -149,8 +149,8 @@ What each gate refuses: ### The floors `ci_verify.py` counts the CTest rows that actually **ran** and fails below a -floor — `KERNEL_MIN_TESTS` ci_verify.py:242 and `RELEASE_MIN_TESTS` -ci_verify.py:350. +floor — `KERNEL_MIN_TESTS` ci_verify.py:247 and `RELEASE_MIN_TESTS` +ci_verify.py:371. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index b82cf224..d481e090 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -106,8 +106,8 @@ finding to report, not a step to take. coverage claim. 10. **A test row never silently disappears.** Each profile counts the rows that - *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:242 and - `RELEASE_MIN_TESTS` ci_verify.py:350. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:247 and + `RELEASE_MIN_TESTS` ci_verify.py:371. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -253,7 +253,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:242, `RELEASE_MIN_TESTS` ci_verify.py:350). It +(`KERNEL_MIN_TESTS` ci_verify.py:247, `RELEASE_MIN_TESTS` ci_verify.py:371). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 61adeaf7..6da9cd9c 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -239,7 +239,12 @@ # extracted from the page and run) # All six register in release too; RATIO-HARDEN, V19-FIX and K-IDX add no row. # 278 registered, 277 run: the WebSocket row still skips on a system libcurl. -KERNEL_MIN_TESTS = 277 +# 278 run = those 277 plus the one source-free row of R5 lane H-MEASURE +# (INT26; summed at the pick, recounted with ctest -N at INT26 (iv)): +# +1 H-MEASURE test_native_acid_composite (the G1 acid composite, C++ and C) +# Its other eleven rows reach the source layer (see RELEASE_MIN_TESTS). 279 +# registered, 278 run: the WebSocket row still skips on a system libcurl. +KERNEL_MIN_TESTS = 278 # Release-row floor, the same gate for the default profile. Before lane P7 # only the kernel profile had one, so a row that left release alone (a # source-bound TU dropped from TEST_SOURCES, a deleted twin or ABI row) left a @@ -347,7 +352,23 @@ # wave-G lane adds a source-bound row (the K-ULP4 and K-ULP5 C checks and # V19-FIX's scaling rows are rows inside existing TUs). No release row skips, # so 678 registered is 678 run. -RELEASE_MIN_TESTS = 678 +# 690 = those 678 plus the twelve rows of R5 lane H-MEASURE (AUDIT4 X14 and +# A4-ACID-COVERAGE; INT26, summed at the pick, recounted at INT26 (iv)): the +# one KERNEL_MIN_TESTS lists above, which registers here too, plus eleven +# source-bound ones the kernel profile does not build: +# +1 G2-09/-10/-12/-13, E20 f1 test_adapter_margin_schedule_differential +# +3 G2-15, G2-18, E5/E14 test_pyramiding_count_differential, +# test_zero_trail_sibling_stop, +# test_pending_entry_trail_tapes +# +4 G2-22, G2-21, G2-32, G2-36 test_e19_excursion_tape, +# test_short_seed_report_swap, +# test_pine_dust_sweep_paired, +# test_session_ismarket_tape +# +3 F1(e), F1 magnified, G2-23 test_aggregated_entry_bar_index_tape, +# test_magnified_aggregated_tape, +# test_adapter_security_route_conditions +# No release row skips, so 690 registered is 690 run. +RELEASE_MIN_TESTS = 690 # PR-only registration floors: the complete CTest populations of the three # excluded profiles at INT25, counted with ctest -N on the integrated tree -- # 653/653/662 at 91d65ad6 (INT24) plus wave G's six rows (C-SURFACE-1 +1, From c36d63d814d8bcc1aec6ab0d38892e4d51ef5293 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 02:58:00 +0800 Subject: [PATCH 09/83] Gates: the residual gate rules a text by its exact words and reads Pine's dotted namespaces (R5 lane H-DOCGATES, X12) AUDIT4-opus X12 (kres K4, K5, K11) found check_kernel_residuals blind in three ways. Its own probe battery, copied and re-run on this tree's macOS kernel archive, flagged 16 of 51 probes at 21bdc2e8; it flags every Pine probe now, on all four surfaces (strings, nm, header literals, kernel literals). - A ruled name never covers a text. The bare words `Pine` (the PineMap row) and `syminfo` passed "match Pine semantics exactly" and "syminfo.tickerid must be set", and any ruled name passed any text around it. A text is now ruled by its exact words; a name by its name. - The vocabulary reads Pine's other dotted namespaces (timeframe., input., str., math., matrix., map., line., xloc., syminfo., format., currency., dayofweek., ...; the namespace must open the run, so a compiler's l_switch.table. label stays silent), the camel-case Tv/TV spelling, bare islastbar/isfirstbar, heikinashi, renko, tradingday and the word Pine in a text. - A row whose second column says "no archive symbol" is held to it: PineMap or a matrix text in the archive or a kernel literal fails. - The key line counts the rulings the vocabulary reads (174 identifiers, 45 texts), not every first-column token (202/25 counted 27 + 22 mechanism-table names), and ci_verify.py floors both counts (ADR_RULED_IDENTIFIERS_MIN / ADR_RULED_TEXTS_MIN), passed as --min-ruled-identifiers / --min-ruled-texts. The 57 installed-header literals are ruled, not reworded (rewording a generated strategy's runtime diagnostic is the GAP-15 codegen decision): one new ADR row holds the 32 distinct dotted argument-check texts of generic_matrix.hpp, map.hpp and drawing.hpp; the PineMap row holds map.hpp's four static_assert texts in place of `Pine`; the deprecated-alias row holds its six alias texts. No source, header or object changes: scripts, tests and docs only. Mac kernel archive: 21bdc2e8's checker "146 archive hits ... against 202 ruled identifiers and 25 ruled texts: 0 findings"; this commit "146 archive hits ... 326 installed-header hits ... against 174 ruled identifiers and 45 ruled texts (floors 174/45): 0 findings ... OK". test_check_kernel_residuals: 39 -> 46 cases (ReachTests), all pass. A symbol a compiler split or cloned (GCC's `.cold` / `.part.N` / `.isra.N` / `.constprop.N`, clang's `.cold.N` / `.llvm.N`) is still one name: x86-64 GCC 13's archive prints `strategy_set_syminfo_type.cold` and six more ruled exports that way (first NTU run of this change: 7 findings, all of that shape). Doc deletions this commit names (rewritten, not dropped): - ADR residual intro, "a text by a phrase it contains" (lane F6's rule) -> "a text by its exact words"; - ADR PineMap row, "where the bare word `Pine` is read" (lane F6); - native-engine.md "What the kernel-only archive still names": the vocabulary list and "requires every match to be listed, by name" (gap lane P2's wording, pre-F6). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit d5b6583119d321e3c6d41a2dc4e1d2ccb5c86fe1) INT26 pick note: scripts/ci_verify.py conflicted where INT25 and H-MEASURE rewrote the floors this commit sits beside; its ADR ruled-count floors are inserted after the merged RELEASE_MIN_TESTS, every floor and comment of both kept. hunkcheck: every hunk of d5b65831 carried unchanged. --- docs/adr/0001-kernel-adapter-boundary.md | 35 ++- docs/pages/native-engine.md | 26 ++- scripts/check_kernel_residuals.py | 261 +++++++++++++++++++---- scripts/ci_verify.py | 15 +- scripts/test_check_kernel_residuals.py | 195 +++++++++++++++-- scripts/test_ci_verify.py | 5 + 6 files changed, 464 insertions(+), 73 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 17c94a5e..0aa48f9e 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -431,11 +431,30 @@ underscore spelling of a Pine member whose namespace word is generic `session.` / `barstate.` member or a `__name__` sentinel label, where `` is a Pine member and not a C/C++ file suffix (`ta.ema` is a call, `ta.hpp` is this project's header, which a sanitizer build writes into rodata as a real -literal). Every match must be listed exactly — an identifier by its name, a text -by a phrase it contains — and every listed match must still be in the archive, +literal). Since R5 lane H-DOCGATES (AUDIT4-opus X12) it also reads the camel-case +`Tv` / `TV` spelling (`TvRound`, `TVSession`), the bare session-bar flags +(`islastbar`, `isfirstbar`), the chart-type and trade-date built-ins (`heikinashi`, +`renko`, `tradingday`), the word `Pine` in a text, and Pine's other dotted namespaces +(`timeframe.`, `input.`, `str.`, `math.`, `matrix.`, `map.`, `array.`, `line.`, +`label.`, `box.`, `table.`, `xloc.`, `syminfo.`, `format.`, `currency.`, +`dayofweek.`, `color.`, `chart.`, `ticker.` and the rest of `PINE_NAMESPACES`), where +the namespace must open the dotted run (a compiler's `l_switch.table.` label +is not Pine's `table.`). Every match must be listed exactly — an identifier by its +name, a text by its exact words — and every listed match must still be in the archive, the installed headers or a kernel source literal, so a lane that adds a name -adds its row and a lane that removes one removes its row. A name that is not in these tables fails the -`kernel` profile. The audit's original probe is a subset of that vocabulary and +adds its row and a lane that removes one removes its row. A ruled name covers that +name alone (a symbol, a code identifier, a string that is only the name) and never a +text that contains it: until lane H-DOCGATES the bare words `Pine` and `syminfo`, +ruled as names, passed any text such as "match Pine semantics exactly" or +"syminfo.tickerid must be set". A row whose second column says "no archive symbol" +rules its names and texts for the installed headers alone, and the checker fails when +the archive or a kernel source literal carries one. A name that is not in these tables fails the +`kernel` profile. The key line counts only the rulings the vocabulary reads — the +mechanism tables below are parsed too, but their first columns (`priority`, +`keep_binding`, `NativeRunSpec::event_retention`, ...) match no pattern — and the +`kernel` profile floors those two counts (`ADR_RULED_IDENTIFIERS_MIN`, +`ADR_RULED_TEXTS_MIN` in `scripts/ci_verify.py`), so a ruling cannot leave with its +name unless the change also lowers the floor. The audit's original probe is a subset of that vocabulary and still reads exactly the seventeen `pine_*` names: ``` @@ -446,7 +465,8 @@ Every match is one of the rows below: the field names of the frozen pending-row POD and the three `last_error` texts, and, since lane F6's wider vocabulary and second surface, frozen C export names, codegen-ABI members of `BacktestEngine`, the kernel's own sentinel labels and the installed-header spellings of the -generated-code runtime. Each is listed here with its ruling. +generated-code runtime, and, since lane H-DOCGATES, that runtime's own texts, each +ruled by its words. Each is listed here with its ruling. | archive string or installed-header name | where it comes from | ruling | |---|---|---| @@ -464,9 +484,10 @@ generated-code runtime. Each is listed here with its ruling. | `strategy_position_size`, `strategy_position_avg_price` | frozen `PF_API` exports (`src/c_abi.cpp`, `include/pineforge/pineforge.h`), pinned by `scripts/check_c_abi_runtime.py`; read since lane F6, whose vocabulary names the underscore spelling of every Pine `strategy.*` member | **retained**: frozen C ABI export names. The value is the kernel's own -- signed position units and their volume-weighted entry price -- and the spelling is the Pine built-in a porting consumer looks for, the vocabulary row of Section B ("the C ABI's documented vocabulary"). Renaming either is a C ABI break. | | `set_syminfo_mintick`, `set_syminfo_pointvalue`, `set_syminfo_session`, `set_syminfo_string`, `set_syminfo_timezone`, `set_syminfo_type`, `get_syminfo_metadata`, `syminfo`, `syminfo_`, `syminfo_mintick_`, `syminfo_tz`, `strategy_set_syminfo_metadata`, `strategy_set_syminfo_mintick`, `strategy_set_syminfo_pointvalue`, `strategy_set_syminfo_session`, `strategy_set_syminfo_string`, `strategy_set_syminfo_timezone`, `strategy_set_syminfo_type` | `BacktestEngine`'s symbol-information ingress and members (`include/pineforge/engine.hpp`), the `syminfo` begin argument (`NativeBeginArgs`, `include/pineforge/native_host.hpp`; `include/pineforge/execution_consumer.hpp`), the `syminfo_tz` parameter of the calendar functions (`include/pineforge/session_time.hpp`) and the frozen C setters (`src/c_abi.cpp`, `include/pineforge/pineforge.h`); read since lane F6, whose vocabulary names `syminfo` | **retained**, the whole family on the terms of the `SymInfo` / `set_syminfo_metadata` row below: `syminfo` is the frozen ABI's word for instrument metadata, the kernel reads the values it carries (tick, point value, session, timezone, instrument type) and never a Pine name. The members are codegen ABI as well -- generated strategies read `syminfo_` (e.g. `syminfo_.timezone`), `syminfo_mintick_` and `get_syminfo_metadata` -- so a rename is a C ABI break and a codegen change. | | `barstate_islast_` | a protected `BacktestEngine` member (`include/pineforge/engine.hpp`); read since lane F6, whose vocabulary names `barstate` | **retained as codegen ABI**: the Pine language flag generated strategies read directly (`barstate.islast`; protected members of `BacktestEngine` are the codegen contract). Only the source host writes it (`source::PineStrategyHost`); no kernel translation unit reads or writes it. The three session flags this row also ruled when lane F6 wrote it -- `session_ismarket_`, `session_isfirstbar_`, `session_islastbar_` -- left the kernel class in R5 lane F5, inside the then-unshipped `engine_script_run_v18` layout (since advanced to `engine_script_run_v19`): they are `PineStrategyHost` members now, which generated code reaches unqualified as before, selected from the kernel's session-day facts (E26-7 below). | -| `sharpe_tv`, `sortino_tv`, `Coof`, `MagnifierCoof` | `pf_equity_stats_t` (`include/pineforge/pineforge.h`) and `exit_legs::Domain` (`include/pineforge/exit_leg_lifecycle.hpp`); read since lane F6, whose gate reads the installed headers | **deprecated aliases**, ruled in "Deprecated public spellings" below: value-identical historical spellings of `sharpe_monthly` / `sortino_monthly` / `FillRecalc` / `MagnifierFillRecalc`, compiler-deprecated since lane F6, removed at `PF_ABI_VERSION` 5 / `lifecycle_v2`. | +| `sharpe_tv`, `sortino_tv`, `Coof`, `MagnifierCoof`, `sharpe_tv is the historical spelling of sharpe_monthly;`, `sortino_tv is the historical spelling of sortino_monthly;`, `Coof is the historical spelling of FillRecalc; removed at lifecycle_v2`, `MagnifierCoof is the historical spelling of`, `the deprecated sharpe_tv spelling must name the sharpe_monthly storage`, `the deprecated sortino_tv spelling must name the sortino_monthly storage` | `pf_equity_stats_t` (`include/pineforge/pineforge.h`) and `exit_legs::Domain` (`include/pineforge/exit_leg_lifecycle.hpp`), their `PF_DEPRECATED` / `[[deprecated]]` texts, and the two `static_assert` texts of `src/c_abi.cpp` that pin the aliases to their storage; read since lane F6, whose gate reads the installed headers (each text is ruled by its exact words since R5 lane H-DOCGATES, where the alias names ruled them before) | **deprecated aliases**, ruled in "Deprecated public spellings" below: value-identical historical spellings of `sharpe_monthly` / `sortino_monthly` / `FillRecalc` / `MagnifierFillRecalc`, compiler-deprecated since lane F6, removed at `PF_ABI_VERSION` 5 / `lifecycle_v2`. | | `pine_time`, `pine_time_close`, `pine_time_tradingday`, `pine_hour`, `pine_minute`, `pine_second`, `pine_dayofmonth`, `pine_dayofweek`, `pine_month`, `pine_year`, `pine_weekofyear`, `pine_session_ismarket`, `pine_session_ispremarket`, `pine_session_ispostmarket`, `PF_PINE_TIME_HAS_SYMINFO_TZ`, `PF_PINE_TIME_HAS_SESSION_DAY`, `pine_str_format`, `pine_str_format_time`, `pine_str_match`, `pine_str_split`, `pine_str_tostring`, `pine_random`, `PineMatrix`, `PineGenericMatrix` | installed headers `include/pineforge/session_time.hpp`, `include/pineforge/str_utils.hpp`, `include/pineforge/math.hpp`, `include/pineforge/matrix.hpp`, `include/pineforge/generic_matrix.hpp`; inline forwards and aliases, so no archive symbol | **retained as deprecated spellings for generated code** (design §2.ii h, lanes L11 and N14): each is an exact inline forward to, or alias of, a neutral primary (`timeframe_time`, `local_hour`, `session_ismarket`, `str_format`, `deterministic_random`, `NumericMatrix`, `GenericMatrix`, ...), and the two macros are the feature tests generated code compiles against. The codegen transpiler emits them (`pine_hour` alone in 66 corpus strategies), so dropping one is a codegen change. | -| `PineMap`, `PineMapBare`, `PineMapKeyEqual`, `PineMapKeyHash`, `is_pine_map_key_v`, `is_pine_map_snapshot_value_v`, `Pine`, `pine_color`, `pine_drawing_error`, `pine_log_info`, `pine_log_warning`, `pine_log_error`, `pine_runtime_error` | installed headers `include/pineforge/map.hpp` (the type, its detail traits and its `static_assert` texts, where the bare word `Pine` is read), `include/pineforge/color.hpp`, `include/pineforge/drawing.hpp`, `include/pineforge/log.hpp`; header-only, outside the kernel's compile closure, so no archive symbol | **retained**: the header-only runtime of generated strategies, primary spellings with no neutral alias (the codegen transpiler emits `PineMap`, `pine_color`, `pine_log_*` and `pine_runtime_error`). No kernel translation unit includes these headers; the kernel profile installs them because its install rule ships all of `include/pineforge` but `source/` and `compat/`. A neutral spelling, or a move under `source/`, is a codegen change and an open decision (AUDIT3-opus, GAP-15). | +| `PineMap`, `PineMapBare`, `PineMapKeyEqual`, `PineMapKeyHash`, `is_pine_map_key_v`, `is_pine_map_snapshot_value_v`, `PineMap key must be a Pine fundamental or enum type`, `PineMap ordered storage must support no-throw swap`, `PineMap index must support no-throw swap`, `PineMap::Snapshot supports primitive Pine values only;`, `pine_color`, `pine_drawing_error`, `pine_log_info`, `pine_log_warning`, `pine_log_error`, `pine_runtime_error` | installed headers `include/pineforge/map.hpp` (the type, its detail traits and its four `static_assert` texts, each ruled by its exact words: until R5 lane H-DOCGATES a ruling of the bare word `Pine` covered them, and would have covered any new text naming the language), `include/pineforge/color.hpp`, `include/pineforge/drawing.hpp`, `include/pineforge/log.hpp`; header-only, outside the kernel's compile closure, so no archive symbol | **retained**: the header-only runtime of generated strategies, primary spellings with no neutral alias (the codegen transpiler emits `PineMap`, `pine_color`, `pine_log_*` and `pine_runtime_error`). No kernel translation unit includes these headers; the kernel profile installs them because its install rule ships all of `include/pineforge` but `source/` and `compat/`. A neutral spelling, or a move under `source/`, is a codegen change and an open decision (AUDIT3-opus, GAP-15). | +| `line.get_price requires xloc.bar_index`, `matrix.reshape: dimension overflow`, `matrix.elements_count: total exceeds int range`, `matrix.new: negative dimensions`, `matrix.new: no-init overload requires default-constructible T`, `matrix.get: row index out of range`, `matrix.get: column index out of range`, `matrix.set: row index out of range`, `matrix.set: column index out of range`, `matrix.row: row index out of range`, `matrix.col: column index out of range`, `matrix.row_ref: row index out of range`, `matrix.add_row: row index out of range`, `matrix.add_row: values size must equal columns()`, `matrix.add_col on empty matrix: use add_row first`, `matrix.add_col: column index out of range`, `matrix.add_col: values size must equal rows()`, `matrix.remove_row: row index out of range`, `matrix.remove_col: column index out of range`, `matrix.swap_rows: row index out of range`, `matrix.swap_columns: column index out of range`, `matrix.submatrix: row index out of range`, `matrix.submatrix: column index out of range`, `matrix.submatrix: from_row must be <= to_row`, `matrix.submatrix: from_col must be <= to_col`, `matrix.transpose: requires default-constructible element type`, `matrix.concat: row count mismatch`, `matrix.concat: column count mismatch`, `matrix.reshape: requires default-constructible element type`, `matrix.sort: requires int, bool, or std::string element type`, `matrix.sort: not supported on bool element type`, `map.size: result exceeds int range` | the argument-check texts of the installed generated-code runtime: `include/pineforge/generic_matrix.hpp` (`GenericMatrix` and `NumericMatrix`, the Pine `matrix.*` built-ins), `include/pineforge/map.hpp` (`PineMap::size`) and `include/pineforge/drawing.hpp` (the line-price lookup); header-only, outside the kernel's compile closure, so no archive symbol; read since R5 lane H-DOCGATES, whose vocabulary reads Pine's dotted namespaces | **retained**, on the terms of the row above: each text names the Pine built-in whose argument check failed (`matrix.get`, `map.size`, `line.get_price`), the name a script's author wrote, and is thrown only inside a generated strategy. No kernel translation unit includes these headers, and the gate holds the texts out of the archive and the kernel's own literals. Rewording one is a runtime-diagnostic change for generated code, part of the GAP-15 decision above. | Names the vocabulary does not match but the audits named, with their rulings: diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 995413ee..d32d1b6e 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -4287,16 +4287,26 @@ build type: a `-g` archive's DWARF names every block-scope local, every struct member and every source path, and none of those is a residual surface. It matches a fixed residual vocabulary against whole identifiers only: at least three bytes, delimited by non-identifier bytes, so machine code `strings` prints -as `C0"TV"` is not a name. The vocabulary (an identifier containing `pine` other +as `C0"TV"` is not a name. It reads the installed headers and the kernel's own +string literals as well. The vocabulary (an identifier containing `pine` other than `pineforge`, `tradingview`, `barmerge`, `coof`, `pooc`, `market_admission`, `calc_on_order_fills`, `process_orders_on_close` or a `tv` -segment; a text containing `strategy.`, `ta.`, `request.security`, -`barmerge.` or `__margin_call__`, where `` is a Pine member and not -a C/C++ file suffix — `ta.ema` is a call, `ta.hpp` is this project's header, and -a sanitizer build writes every source path into rodata as a real literal) -requires every match to be listed, -by name, in the first column of the ADR's residual tables — and every listed -match to still be in the archive. The `kernel` profile runs it as the +segment, a `strategy()` parameter word, `syminfo`, `barstate`, the underscore +spelling of a Pine member, the camel-case `Tv` / `TV` spelling, a bare +`islastbar` / `isfirstbar`, `heikinashi`, `renko` or `tradingday`; a text +containing `strategy.`, `ta.`, `request.security`, +`barmerge.`, a dotted `session.` / `barstate.` member, another Pine +namespace such as `timeframe.`, `input.`, `str.`, `math.`, `matrix.` or +`xloc.`, the word `Pine`, or a `__name__` label, where `` is not a C/C++ +file suffix — `ta.ema` is a call, `ta.hpp` is this project's header, and a +sanitizer build writes every source path into rodata as a real literal) +requires every match to be listed in the first column of the ADR's residual +tables — a name by its name, a text by its exact words, so a ruled name never +covers a text that contains it — and every listed match to still be in the +archive, the installed headers or a kernel source literal. A row that says +"no archive symbol" is held to it, and the two counts the key line prints +(the rulings the vocabulary reads) have floors in `scripts/ci_verify.py` +(`ADR_RULED_IDENTIFIERS_MIN`, `ADR_RULED_TEXTS_MIN`). The `kernel` profile runs it as the `kernel-residuals` stage right after the build; every profile runs it as the CTest row `test_kernel_residuals` (with the checker's own must-fail self-tests). What remains is listed with its ruling in diff --git a/scripts/check_kernel_residuals.py b/scripts/check_kernel_residuals.py index 4186ae15..8a870b02 100644 --- a/scripts/check_kernel_residuals.py +++ b/scripts/check_kernel_residuals.py @@ -78,6 +78,26 @@ stays live while any surface carries it, and a vocabulary literal a kernel TU compiles must have a row even where the compiler split it. +R5 lane H-DOCGATES (AUDIT4-opus X12, kres K4/K5/K11) closes what the fourth +audit passed through the gate: + + * a text is ruled by its exact words. A ruled NAME covers the name -- a + symbol, a code identifier, a string that is only the name -- and never a + text around it: the bare words `Pine` and `syminfo`, ruled as names, + passed "match Pine semantics exactly" and "syminfo.tickerid must be set"; + * the vocabulary reads the camel-case `Tv`/`TV` spelling, the bare + session-bar flags, the chart-type and trade-date built-ins, the word + `Pine` in a text and Pine's other dotted namespaces (PINE_NAMESPACES); + * a row whose second column says "no archive symbol" is held to it: its + names and texts pass in the installed headers and fail in the archive or + a kernel source literal; + * the key line counts the rulings the vocabulary reads, not every + first-column token of the section (the mechanism tables' `priority`, + `keep_binding`, ... match no pattern), and --min-ruled-identifiers / + --min-ruled-texts floor those counts (scripts/ci_verify.py's + ADR_RULED_IDENTIFIERS_MIN / ADR_RULED_TEXTS_MIN), so a ruling cannot + leave together with its name unless the change lowers the floor. + Exit 0 when every match is ruled and every ruling is live, 1 on a finding, 2 when the archive, the tools (including the strip tool: the gate fails CLOSED rather than read debug information), the install rule or the ADR section @@ -159,6 +179,17 @@ def _any_of(words: list[str]) -> str: ("calc_on_order_fills", re.compile(r"calc_on_order_fills", re.IGNORECASE)), ("process_orders_on_close", re.compile(r"process_orders_on_close", re.IGNORECASE)), ("tv", re.compile(r"(?:^|_)tv(?:_|$)", re.IGNORECASE)), + # R5 lane H-DOCGATES (AUDIT4-opus X12): the camel-case spelling of the + # platform (`TvRound`, `TVSession`, `kTvStep`), which the `tv` segment + # above never reads. Two characters after the prefix, the first a capital, + # so a machine-code run such as `TvV@` or `TVh` is not a name. + ("Tv/TV camel case", re.compile(r"(?:^|[a-z0-9_])(?:Tv|TV)[A-Z][A-Za-z0-9]")), + # ... Pine's session-bar flags spelled bare (`islastbar`, not only + # `session_islastbar`), and the chart-type and trade-date built-ins + # (`ticker.heikinashi`, `ticker.renko`, `time_tradingday`). + ("session bar flag", re.compile(r"(?:^|_)is(?:first|last)bar(?:_|$)", re.IGNORECASE)), + ("chart-type or trade-date built-in", + re.compile(r"heikin_?ashi|renko|tradingday", re.IGNORECASE)), # R5 lane F6: Pine words with no `pine`/`tv` token. The strategy() # declaration parameters whose spelling has no generic reading ... ("pyramiding", re.compile(r"pyramiding", re.IGNORECASE)), @@ -216,7 +247,21 @@ def _any_of(words: list[str]) -> str: # is the same carve-out `pine(?!forge)` makes for the project's own name, and # it hides no Pine built-in: none is spelled `h`, `hpp`, `cpp`, ... NOT_A_SOURCE_FILE = r"(?!(?:h|hpp|hh|hxx|c|cc|cpp|cxx|inc|ipp)(?![A-Za-z0-9_]))" +# R5 lane H-DOCGATES (AUDIT4-opus X12): Pine's other dotted namespaces, whose +# member texts (`timeframe.change`, `input.int`, `str.format`, `math.sum`, +# `matrix.new`, `line.get_price`, `xloc.bar_index`, `syminfo.tickerid`, +# `format.mintick`, `currency.USD`, `dayofweek.monday`) no pattern above read. +# The namespace must start the dotted run -- `l_switch.table.`, a +# compiler's switch-table label, is not Pine's `table.` -- and the member must +# not be a C/C++ file suffix (`map.hpp`, `color.hpp` are this project's). +PINE_NAMESPACES = ( + "syminfo timeframe input str math matrix map array line linefill label box table " + "polyline xloc yloc format dayofweek currency color chart ticker plot hline").split() PHRASE_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = ( + # R5 lane H-DOCGATES: the word Pine in a text is a claim about the + # language, so a text holding it needs its own row; a ruled identifier + # that happens to be spelled `Pine...` never covers it. + ("Pine prose", re.compile(r"\bpine\b", re.IGNORECASE)), ("strategy.", re.compile(r"\bstrategy\." + NOT_A_SOURCE_FILE + r"[a-z_]{2,}")), ("ta.", re.compile(r"\bta\." + NOT_A_SOURCE_FILE + r"[a-z_]{2,}")), ("request.security", re.compile(r"request\.security")), @@ -227,6 +272,9 @@ def _any_of(words: list[str]) -> str: # "session.start" is not Pine's. ("session.", re.compile(r"\bsession\." + _any_of(SESSION_MEMBERS) + r"\b")), ("barstate.", re.compile(r"\bbarstate\." + _any_of(BARSTATE_MEMBERS) + r"\b")), + ("dotted Pine namespace", re.compile( + r"(? str: class Ruled: identifiers: frozenset[str] phrases: frozenset[str] + # R5 lane H-DOCGATES: the names and texts whose row says they are + # header-only ("... so no archive symbol" in its second column). The + # installed headers carry them; the archive and the kernel's own literals + # must not. + header_only: frozenset[str] = frozenset() + + +# The words a row's second column uses for a name the kernel profile installs +# but never compiles into the archive: the gate holds the row to that claim. +HEADER_ONLY_MARK = "no archive symbol" def ruled_entries(adr_text: str) -> Ruled: """The first column of every table row in the ADR's residual section. A backticked token that is a plain identifier is ruled by name; a token with - spaces or punctuation is a ruled phrase (a text is covered when it contains - it); a token with a path separator is a citation, not a ruling.""" + spaces or punctuation is a ruled text, and it rules exactly that text (a + text is covered only by a row that spells all of it -- R5 lane + H-DOCGATES); a token with a path separator is a citation, not a ruling. A + row whose second column says "no archive symbol" rules its identifiers for + the installed headers only, names and texts alike.""" start = adr_text.find(SECTION_HEADING) if start < 0: raise InfrastructureError("ADR section not found: " + SECTION_HEADING) @@ -302,21 +364,28 @@ def ruled_entries(adr_text: str) -> Ruled: section = section[:following.start()] identifiers: set[str] = set() phrases: set[str] = set() + header_only: set[str] = set() for line in section.splitlines(): if not line.startswith("|"): continue - first = line.split("|")[1] + cells = line.split("|") + first = cells[1] + scoped = len(cells) > 2 and HEADER_ONLY_MARK in cells[2] for token in BACKTICKED.findall(first): token = token.strip() if "/" in token: continue if IDENTIFIER.fullmatch(token): identifiers.add(token) + if scoped: + header_only.add(token) else: phrases.add(token) + if scoped: + header_only.add(token) if not identifiers: raise InfrastructureError("ADR residual section lists no ruled identifier") - return Ruled(frozenset(identifiers), frozenset(phrases)) + return Ruled(frozenset(identifiers), frozenset(phrases), frozenset(header_only)) def scan(lines: list[str], source: str, *, texts: bool = True, @@ -338,16 +407,17 @@ def scan(lines: list[str], source: str, *, texts: bool = True, continue if SENTINEL[1].match(identifier): if texts: - hits.append(Hit("identifier", identifier, SENTINEL[0], source, where)) + hits.append(Hit("identifier", identifier, SENTINEL[0], source, where, line)) continue for name, pattern in IDENTIFIER_PATTERNS: if pattern.search(identifier): - hits.append(Hit("identifier", identifier, name, source, where)) + hits.append(Hit("identifier", identifier, name, source, where, line)) break if texts: + phrase_line = CLONE_SUFFIX.sub("", line) if source in ("strings", "nm") else line for name, pattern in PHRASE_PATTERNS: - if pattern.search(line): - hits.append(Hit("phrase", line, name, source, where)) + if pattern.search(phrase_line): + hits.append(Hit("phrase", line, name, source, where, line)) break return hits @@ -361,6 +431,47 @@ def symbol_name(token: str, source: str) -> str: return token +# The suffixes a compiler appends to a symbol it splits or clones -- GCC's +# `.cold` / `.part.N` / `.isra.N` / `.constprop.N` / `.lto_priv.N`, clang's +# `.cold.N` / `.llvm.N` -- which the ELF and Mach-O string tables print as +# `strategy_set_syminfo_type.cold`: still one symbol's name, never a text +# (x86-64 GCC 13's kernel archive carries seven such names of ruled exports). +CLONE_SUFFIX = re.compile( + r"(?:\.(?:cold|part|isra|constprop|lto_priv|localalias|llvm|clone|specialized)" + r"(?:\.\d+)*)+$") + +# The surfaces whose lines are texts a program prints or compiles in: an archive +# string, a header literal, a kernel-source literal. (A symbol line and a code +# line are names.) +TEXT_SOURCES = frozenset(("strings", "header-literal", "source-literal")) +# The surfaces a header-only ruling must never appear on. +ARCHIVE_SOURCES = frozenset(("strings", "nm", "source-literal")) + + +def is_text(hit: Hit) -> bool: + """A hit inside a text of several words, rather than a line that IS a name + (a symbol-table string, a one-name literal). R5 lane H-DOCGATES: a ruled + name inside such a text never covers it -- `PineMap` or `syminfo` ruled + as names used to pass "match Pine semantics exactly" and "syminfo.tickerid + must be set"; the text is ruled by its own words or not at all.""" + if hit.source not in TEXT_SOURCES: + return False + text = hit.text.strip() + if hit.source == "strings": + text = CLONE_SUFFIX.sub("", text) + return ARCHIVE_IDENTIFIER.fullmatch(text) is None + + +def vocabulary_text(text: str) -> bool: + """Whether the vocabulary reads anything in `text` (the rulings it counts).""" + return bool(scan([text], "strings")) + + +def vocabulary_name(identifier: str) -> bool: + return (any(pattern.search(identifier) for _, pattern in IDENTIFIER_PATTERNS) + or bool(SENTINEL[1].match(identifier))) + + def evaluate(strings_lines: list[str], nm_lines: list[str], ruled: Ruled, header_lines: tuple[list[str], list[str], list[str], list[str]] | None = None, source_literals: tuple[list[str], list[str]] | None = None @@ -383,36 +494,59 @@ def evaluate(strings_lines: list[str], nm_lines: list[str], ruled: Ruled, present: set[str] = set() covered: set[str] = set() reported: set[tuple[str, str]] = set() + + def report(kind: str, token: str, detail: str) -> None: + if (kind, token) not in reported: + reported.add((kind, token)) + findings.append(Finding("unruled", token, detail)) + for hit in hits + header_hits + source_hits: - if hit.kind == "identifier": - name = symbol_name(hit.token, hit.source) - ruled_as = next((candidate for candidate in (hit.token, name) - if candidate in ruled.identifiers), None) - if ruled_as is not None: - present.add(ruled_as) - elif (hit.kind, name) not in reported: - reported.add((hit.kind, name)) - findings.append(Finding( - "unruled", name, - f"identifier matches residual vocabulary {hit.pattern!r} via {hit.source} " - f"and has no ADR-0001 row: {hit.line[:160]}")) + if is_text(hit) or hit.kind == "phrase": + # A text is ruled by its exact words, ends trimmed (a table cell + # cannot carry a trailing space); an nm line is a symbol line, and + # only a phrase of the vocabulary makes it a text. + text = hit.text.strip() + if text in ruled.phrases: + if text in ruled.header_only and hit.source in ARCHIVE_SOURCES: + report("text", text[:160], + f"ruled for the installed headers only (its row says " + f"'{HEADER_ONLY_MARK}'), but {hit.source} carries it") + else: + covered.add(text) + continue + if hit.kind == "identifier": + name = symbol_name(hit.token, hit.source) + if hit.token not in ruled.identifiers and name not in ruled.identifiers: + report("identifier", name, + f"identifier matches residual vocabulary {hit.pattern!r} via " + f"{hit.source} and has no ADR-0001 row: {hit.line[:160]}") + continue + report("text", hit.text[:160], + f"text names the ruled identifier {name!r} via {hit.source}, and a " + "ruled name never covers a text: the text needs its own ADR-0001 " + "row, spelled exactly") + continue + report("text", hit.text[:160], + f"text matches residual vocabulary {hit.pattern!r} via {hit.source} " + "and no ADR-0001 row spells it") + continue + name = symbol_name(hit.token, hit.source) + ruled_as = next((candidate for candidate in (hit.token, name) + if candidate in ruled.identifiers), None) + if ruled_as is None: + report("identifier", name, + f"identifier matches residual vocabulary {hit.pattern!r} via {hit.source} " + f"and has no ADR-0001 row: {hit.line[:160]}") + elif ruled_as in ruled.header_only and hit.source in ARCHIVE_SOURCES: + report("identifier", name, + f"ruled for the installed headers only (its row says " + f"'{HEADER_ONLY_MARK}'), but {hit.source} carries it: {hit.line[:160]}") else: - covering = [phrase for phrase in ruled.phrases if phrase in hit.line] - if covering: - covered.update(covering) - elif (hit.kind, hit.token) not in reported: - reported.add((hit.kind, hit.token)) - findings.append(Finding( - "unruled", hit.token[:160], - f"text matches residual vocabulary {hit.pattern!r} via {hit.source} " - "and contains no ADR-0001 phrase")) - def matched(identifier: str) -> bool: - return (any(pattern.search(identifier) for _, pattern in IDENTIFIER_PATTERNS) - or bool(SENTINEL[1].match(identifier))) + present.add(ruled_as) for identifier in sorted(ruled.identifiers): if identifier in present: continue - if matched(identifier): + if vocabulary_name(identifier): findings.append(Finding( "stale", identifier, "ruled in ADR-0001 but no longer in the archive, the installed headers or " @@ -420,11 +554,11 @@ def matched(identifier: str) -> bool: for phrase in sorted(ruled.phrases): if phrase in covered: continue - if any(pattern.search(phrase) for _, pattern in PHRASE_PATTERNS): + if vocabulary_text(phrase): findings.append(Finding( "stale", phrase, - "ruled in ADR-0001 but no archive, header or kernel source text contains it; " - "drop its row")) + "ruled in ADR-0001 but no archive, header or kernel source text is exactly " + "this; drop its row")) summary = { "hits": len(hits), "identifierHits": sum(1 for hit in hits if hit.kind == "identifier"), @@ -433,8 +567,14 @@ def matched(identifier: str) -> bool: "sourceHits": len(source_hits), "ruledIdentifiersPresent": len(present), "ruledPhrasesCovered": len(covered), - "ruledIdentifiers": len(ruled.identifiers), - "ruledPhrases": len(ruled.phrases), + # R5 lane H-DOCGATES (AUDIT4-opus K5): the rulings the vocabulary + # reads. The mechanism tables' first columns (`priority`, + # `keep_binding`, `NativeRunSpec::event_retention`, ...) are parsed + # too, but no pattern matches them, so they are neither residue nor + # rulings and are not counted. + "ruledIdentifiers": sum(1 for identifier in ruled.identifiers + if vocabulary_name(identifier)), + "ruledPhrases": sum(1 for phrase in ruled.phrases if vocabulary_text(phrase)), "findings": len(findings), } return findings, summary @@ -733,8 +873,29 @@ def read_archive(archive: Path, workdir: Path) -> tuple[list[str], list[str], st return strings_text.splitlines(), nm_text.splitlines(), tool +def floor_findings(summary: dict[str, int], min_identifiers: int | None, + min_texts: int | None) -> list[Finding]: + """R5 lane H-DOCGATES (AUDIT4-opus X13): the ruled counts have a floor. + + A row the vocabulary reads cannot vanish silently -- its name would turn + unruled -- unless the name left the archive in the same change; a floor + makes that drop a stated edit of scripts/ci_verify.py's + ADR_RULED_IDENTIFIERS_MIN / ADR_RULED_TEXTS_MIN, beside the ctest floors.""" + findings: list[Finding] = [] + for key, floor, what in (("ruledIdentifiers", min_identifiers, "identifiers"), + ("ruledPhrases", min_texts, "texts")): + if floor is not None and summary[key] < floor: + findings.append(Finding( + "floor", f"ruled {what}", + f"{summary[key]} vocabulary-matched ruled {what} is below the floor {floor}: " + "a ruling left ADR-0001; restore it, or lower the floor in " + "scripts/ci_verify.py in the change that removes its name")) + return findings + + def check(archive: Path, adr: Path = ADR, *, evidence_dir: Path | None = None, - include_root: Path = INCLUDE_ROOT, source_root: Path = ROOT) -> int: + include_root: Path = INCLUDE_ROOT, source_root: Path = ROOT, + min_identifiers: int | None = None, min_texts: int | None = None) -> int: ruled = ruled_entries(adr.read_text(encoding="utf-8")) cmake_text = (source_root / "CMakeLists.txt").read_text(encoding="utf-8") headers = kernel_profile_headers(include_root, cmake_text) @@ -746,6 +907,8 @@ def check(archive: Path, adr: Path = ADR, *, evidence_dir: Path | None = None, units = len(kernel_translation_units(cmake_text)) findings, summary = evaluate(strings_lines, nm_lines, ruled, header_lines=header_lines, source_literals=source_literals) + findings += floor_findings(summary, min_identifiers, min_texts) + summary["findings"] = len(findings) if evidence_dir is not None: evidence_dir.mkdir(parents=True, exist_ok=True) (evidence_dir / "strings.txt").write_text("\n".join(strings_lines) + "\n") @@ -774,8 +937,10 @@ def check(archive: Path, adr: Path = ADR, *, evidence_dir: Path | None = None, f"{summary['headerHits']} installed-header hits and " f"{summary['sourceHits']} kernel-source hits " f"against {summary['ruledIdentifiers']} ruled identifiers and " - f"{summary['ruledPhrases']} ruled texts in {adr.name}: " - f"{summary['findings']} findings ... {verdict}") + f"{summary['ruledPhrases']} ruled texts in {adr.name}" + + (f" (floors {min_identifiers}/{min_texts})" + if min_identifiers is not None or min_texts is not None else "") + + f": {summary['findings']} findings ... {verdict}") return 1 if findings else 0 @@ -791,10 +956,18 @@ def main(argv: list[str] | None = None) -> int: help="include root whose pineforge/ tree is read as the kernel " "profile installs it -- this tree's include/ or an installed " "prefix's (default: %(default)s)") + parser.add_argument("--min-ruled-identifiers", type=int, default=None, + help="fail when fewer vocabulary-matched identifiers are ruled " + "(ci_verify.py passes ADR_RULED_IDENTIFIERS_MIN)") + parser.add_argument("--min-ruled-texts", type=int, default=None, + help="fail when fewer vocabulary-matched texts are ruled " + "(ci_verify.py passes ADR_RULED_TEXTS_MIN)") args = parser.parse_args(argv) try: return check(args.archive.resolve(), args.adr.resolve(), - evidence_dir=args.evidence_dir, include_root=args.headers.resolve()) + evidence_dir=args.evidence_dir, include_root=args.headers.resolve(), + min_identifiers=args.min_ruled_identifiers, + min_texts=args.min_ruled_texts) except InfrastructureError as error: print("check_kernel_residuals: " + str(error), file=sys.stderr) return 2 diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 6da9cd9c..37b4ee92 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -369,6 +369,17 @@ # test_adapter_security_route_conditions # No release row skips, so 690 registered is 690 run. RELEASE_MIN_TESTS = 690 +# ADR-0001 ruled-count floors, beside the ctest floors (R5 lane H-DOCGATES, +# AUDIT4-opus X13 / docs-a N7). check_kernel_residuals.py counts the rulings +# its vocabulary reads -- 174 identifiers and 45 texts on the lane's tree: the +# 175 AUDIT4 counted, less the bare word `Pine`, and the 3 feed texts plus +# the 42 texts the lane rules by their exact words (6 deprecated-alias texts, +# map.hpp's 4 static_assert texts, the runtime's 32 dotted argument checks). +# A row the vocabulary reads cannot leave silently -- its name turns unruled +# -- unless its name left in the same change; the floor makes that drop an +# edit of this file. A lane that adds rulings raises the floor with them. +ADR_RULED_IDENTIFIERS_MIN = 174 +ADR_RULED_TEXTS_MIN = 45 # PR-only registration floors: the complete CTest populations of the three # excluded profiles at INT25, counted with ctest -N on the integrated tree -- # 653/653/662 at 91d65ad6 (INT24) plus wave G's six rows (C-SURFACE-1 +1, @@ -625,7 +636,9 @@ def twin_parity_command(source: Path) -> list[str]: def kernel_residuals_command(cfg: VerifyConfig) -> list[str]: return [sys.executable, str(cfg.source / 'scripts/check_kernel_residuals.py'), '--archive', str(cfg.build_dir / 'lib' / 'libpineforge_kernel.a'), - '--adr', str(cfg.source / 'docs/adr/0001-kernel-adapter-boundary.md')] + '--adr', str(cfg.source / 'docs/adr/0001-kernel-adapter-boundary.md'), + '--min-ruled-identifiers', str(ADR_RULED_IDENTIFIERS_MIN), + '--min-ruled-texts', str(ADR_RULED_TEXTS_MIN)] def cmake_cache_definitions(cfg: VerifyConfig) -> dict[str, str]: diff --git a/scripts/test_check_kernel_residuals.py b/scripts/test_check_kernel_residuals.py index 8a38459e..a2f36953 100644 --- a/scripts/test_check_kernel_residuals.py +++ b/scripts/test_check_kernel_residuals.py @@ -37,6 +37,17 @@ without the source, live through the TU's literal, still stale when no surface carries it, never live through a comment or an include path; the surface is judged in both directions, and it is exactly the archive's own TU list. + +R5 lane H-DOCGATES (AUDIT4-opus X12, kres K4/K5/K11) closes the reach gaps +the fourth audit probed: a ruled NAME never covers a TEXT (the bare-word +rulings `Pine` and `syminfo` passed "match Pine semantics exactly" and +"syminfo.tickerid must be set"), so a text is ruled by its exact words; the +vocabulary reads Pine's other dotted namespaces (`timeframe.`, `input.`, +`str.`, `math.`, `matrix.`, `line.`, `xloc.`, ...), the camel-case +`Tv`/`TV` spelling, the bare session-bar flags and the chart-type and +trade-date built-ins; a row whose second column says "no archive symbol" is +held to it; the printed counts are the rulings the vocabulary reads; and the +counts have floors that scripts/ci_verify.py passes. """ from __future__ import annotations @@ -151,11 +162,30 @@ def build_probe_archive(directory: Path, text: str = PROBE_SOURCE) -> Path: def table_archive() -> tuple[list[str], list[str]]: - """A synthetic archive holding exactly the ADR's ruled vocabulary.""" - strings = sorted(RULED.identifiers) + sorted(RULED.phrases) + NOISE_STRINGS + """A synthetic archive holding exactly the ADR's ruled vocabulary -- all of + it but the header-only rows (R5 lane H-DOCGATES), which table_headers() + ships where the kernel profile ships them.""" + strings = (sorted(RULED.identifiers - RULED.header_only) + + sorted(RULED.phrases - RULED.header_only) + NOISE_STRINGS) return strings, list(NOISE_NM) +def table_headers() -> tuple[list[str], list[str], list[str], list[str]]: + """The header-only rulings on the installed-header surface: each name as a + code line, each text as a literal (read_headers' four lists).""" + names = sorted(RULED.header_only & RULED.identifiers) + texts = sorted(RULED.header_only & RULED.phrases) + return (names, [f"pineforge/table.hpp:{n}" for n in range(1, len(names) + 1)], + texts, [f"pineforge/table.hpp:{n}" for n in range(1, len(texts) + 1)]) + + +def evaluate_table(strings: list[str], nm: list[str], **surfaces): + """guard.evaluate with the header-only rulings shipped in the headers, + unless the case brings its own header surface.""" + surfaces.setdefault("header_lines", table_headers()) + return guard.evaluate(strings, nm, RULED, **surfaces) + + class RuledTableTests(unittest.TestCase): def test_the_adr_table_rules_every_family_the_audit_named(self) -> None: names = RULED.identifiers @@ -185,7 +215,7 @@ def test_a_missing_section_is_an_infrastructure_error(self) -> None: class EvaluatorTests(unittest.TestCase): def evaluate(self, strings: list[str], nm: list[str]): - return guard.evaluate(strings, nm, RULED) + return evaluate_table(strings, nm) def test_the_table_alone_passes(self) -> None: findings, summary = self.evaluate(*table_archive()) @@ -231,7 +261,12 @@ def test_the_ruled_texts_pass_by_phrase(self) -> None: strings, nm = table_archive() findings, summary = self.evaluate(strings, nm) self.assertEqual(findings, []) - self.assertEqual(summary["ruledPhrasesCovered"], 3) + # (expectation corrected: 3 -> every ruled text the vocabulary reads, + # because R5 lane H-DOCGATES rules each text by its exact words: the + # three feed texts, the deprecated aliases' texts, map.hpp's + # static_assert texts and the runtime's dotted argument checks.) + self.assertEqual(summary["ruledPhrasesCovered"], + sum(1 for p in RULED.phrases if guard.vocabulary_text(p))) def test_a_stale_ruling_fails(self) -> None: strings, nm = table_archive() @@ -320,7 +355,7 @@ class AuditBlindSpotTests(unittest.TestCase): """The probes the third audit's two reviewers passed through the gate.""" def evaluate(self, strings: list[str], nm: list[str]): - return guard.evaluate(strings, nm, RULED) + return evaluate_table(strings, nm) def unruled(self, strings: list[str], nm: list[str]) -> set[str]: findings, _ = self.evaluate(strings, nm) @@ -382,8 +417,10 @@ def test_sanitizer_metadata_is_not_surface(self) -> None: "__func__._ZN5Eigen8internal14aligned_mallocEm"] findings, summary = self.evaluate(strings + metadata, nm) self.assertEqual(findings, [], [str(f) for f in findings]) + # (expectation corrected: the text alone -> the text and its bare + # `islastbar`, which R5 lane H-DOCGATES' vocabulary reads by itself.) self.assertEqual(self.unruled(strings + metadata + ["session.islastbar probe"], nm), - {"session.islastbar probe"}) + {"session.islastbar probe", "islastbar"}) self.assertEqual(self.unruled(strings + ["session.start must precede its end"], nm), set()) @@ -548,14 +585,13 @@ def source_literals(self, text: str) -> tuple[list[str], list[str]]: def test_without_the_source_the_split_label_reads_stale(self) -> None: """Fail-before: PR #278's Linux verdict, reproduced from its strings line.""" - findings, _ = guard.evaluate(*self.split_archive(), RULED) + findings, _ = evaluate_table(*self.split_archive()) self.assertEqual([(f.kind, f.token) for f in findings], [("stale", SPLIT_LABEL)]) def test_a_label_the_compiler_split_is_live_through_its_source_literal(self) -> None: literals = self.source_literals( 'constexpr char kNativeLiquidationLabel[] = "__kernel_liquidation__";\n') - findings, summary = guard.evaluate(*self.split_archive(), RULED, - source_literals=literals) + findings, summary = evaluate_table(*self.split_archive(), source_literals=literals) self.assertEqual(findings, [], [str(f) for f in findings]) self.assertEqual(summary["sourceHits"], 1) @@ -563,7 +599,7 @@ def test_a_ruling_no_surface_carries_is_still_stale(self) -> None: literals = self.source_literals( 'constexpr char kNativeRiskLabel[] = "__kernel_risk__";\n' 'constexpr char kComment[] = "Margin liquidation";\n') - findings, _ = guard.evaluate(*self.split_archive(), RULED, source_literals=literals) + findings, _ = evaluate_table(*self.split_archive(), source_literals=literals) self.assertEqual([(f.kind, f.token) for f in findings], [("stale", SPLIT_LABEL)]) def test_a_comment_or_an_include_path_is_not_a_source_literal(self) -> None: @@ -573,7 +609,7 @@ def test_a_comment_or_an_include_path_is_not_a_source_literal(self) -> None: '/* "__kernel_liquidation__" */\n' 'constexpr char kOther[] = "Margin liquidation";\n') self.assertEqual(literals[0], ["Margin liquidation"]) - findings, _ = guard.evaluate(*self.split_archive(), RULED, source_literals=literals) + findings, _ = evaluate_table(*self.split_archive(), source_literals=literals) self.assertEqual([(f.kind, f.token) for f in findings], [("stale", SPLIT_LABEL)]) def test_a_kernel_source_literal_is_judged_like_an_archive_text(self) -> None: @@ -582,7 +618,7 @@ def test_a_kernel_source_literal_is_judged_like_an_archive_text(self) -> None: literals = self.source_literals('const char* reason = "strategy.entry rejected";\n' 'const char* tag = "tv_probe_source";\n') strings, nm = table_archive() - findings, _ = guard.evaluate(strings, nm, RULED, source_literals=literals) + findings, _ = evaluate_table(strings, nm, source_literals=literals) self.assertEqual(sorted((f.kind, f.token) for f in findings), [("unruled", "strategy.entry rejected"), ("unruled", "tv_probe_source")]) @@ -613,11 +649,144 @@ def test_the_kernel_sources_carry_no_unruled_literal(self) -> None: [Path(unit).name + ".o" for unit in units]) literals = guard.read_source_literals(guard.ROOT, files) strings, nm = table_archive() - findings, summary = guard.evaluate(strings, nm, RULED, source_literals=literals) + findings, summary = evaluate_table(strings, nm, source_literals=literals) self.assertEqual(findings, [], [str(f) for f in findings]) self.assertGreater(summary["sourceHits"], 0) +class ReachTests(unittest.TestCase): + """R5 lane H-DOCGATES (AUDIT4-opus X12, kres K4/K5/K11): the fourth + audit's probes, each of which the lane's base checker (21bdc2e8) passed + unflagged, and the two halves of the header-only and count rules.""" + + def unruled(self, strings: list[str], nm: list[str], **surfaces) -> set[str]: + findings, _ = evaluate_table(strings, nm, **surfaces) + return {f.token for f in findings if f.kind == "unruled"} + + def test_a_ruled_name_never_covers_a_text(self) -> None: + """kres gate_bareword_probe.txt: the bare-word rulings `Pine` + (ADR-0001's PineMap row) and `syminfo` (the symbol-information row) + passed these four texts, and any ruled name passed a text around it.""" + for text in ("match Pine semantics exactly", "Pine requires the chart symbol here", + "syminfo.tickerid must be set", "syminfo.mintick drives the tick", + "strategy_set_syminfo_type rejects this value"): + for surface in ("strings", "source-literal"): + with self.subTest(text=text, surface=surface): + strings, nm = table_archive() + if surface == "strings": + found = self.unruled(strings + [text], nm) + else: + found = self.unruled(strings, nm, + source_literals=([text], ["src/probe.cpp:1"])) + self.assertIn(text, found) + self.assertNotIn("Pine", RULED.identifiers) + # The name alone -- a symbol, a code identifier -- stays ruled by name. + strings, nm = table_archive() + self.assertEqual(self.unruled(strings + ["strategy_set_syminfo_type"], + nm + ["T _strategy_set_syminfo_type"]), set()) + + def test_pine_dotted_namespaces_are_residue(self) -> None: + for text in ("timeframe.change", "input.int", "str.format", "math.sum", + "matrix.new: negative size", "line.get_price requires xloc.bar_index", + "syminfo.tickerid", "format.mintick", "currency.USD", "dayofweek.monday", + "timeframe.isintraday", "array.get index out of range", + "ticker.heikinashi"): + with self.subTest(text=text): + strings, nm = table_archive() + self.assertIn(text, self.unruled(strings + [text], nm)) + self.assertTrue(self.unruled(strings, nm + ["0000000000000000 T " + text])) + # A compiler's switch-table label, a project header, a member of a + # dotted run that is not a namespace: none is Pine's. + for text in ("l_switch.table.strategy_native_cancel_v1", "include/pineforge/map.hpp", + "color.hpp", "matrix.hpp", "x.str.y", "data.exchange", "inline.cold"): + with self.subTest(look_alike=text): + strings, nm = table_archive() + findings, _ = evaluate_table(strings + [text], nm + ["t " + text]) + self.assertEqual(findings, [], [str(f) for f in findings]) + + def test_camel_case_platform_names_and_bare_flags_are_residue(self) -> None: + for name in ("TvRound", "TVSession", "kTvStep", "islastbar", "isfirstbar", + "isfirstbar_regular", "heikinashi_bar", "renko_brick", "time_tradingday"): + with self.subTest(name=name): + strings, nm = table_archive() + self.assertEqual(self.unruled(strings + [name], nm), {name}) + self.assertEqual(self.unruled(strings, nm + ["T " + name]), {name}) + # Machine-code runs that only resemble them are not names. + strings, nm = table_archive() + findings, _ = evaluate_table(strings + ["TvV@", "TVh", 'C0"TV"', "xTVa", "ATVdisp"], nm) + self.assertEqual(findings, [], [str(f) for f in findings]) + + def test_a_compiler_split_symbol_is_still_its_name(self) -> None: + """x86-64 GCC 13's kernel archive prints `strategy_set_syminfo_type.cold` + (the cold half GCC splits off a function) in its string table: one + symbol's name under a clone suffix, ruled with the export, not a text.""" + strings, nm = table_archive() + clones = ["strategy_set_syminfo_type.cold", "_strategy_set_syminfo_type.cold.1", + "strategy_set_syminfo_mintick.part.0", "strategy_position_size.isra.0", + "strategy_set_syminfo_string.constprop.3", "strategy_set_syminfo_session.llvm.42"] + findings, _ = evaluate_table(strings + clones, nm + ["t " + c for c in clones]) + self.assertEqual(findings, [], [str(f) for f in findings]) + # the suffix is a clone marker only at the end of a symbol-shaped line + self.assertEqual(self.unruled(strings + ["pine_probe_fn.cold", "timeframe.change"], nm), + {"pine_probe_fn", "timeframe.change"}) + self.assertIn("syminfo.part: bad", self.unruled(strings + ["syminfo.part: bad"], nm)) + + def test_a_header_only_ruling_is_held_to_its_row(self) -> None: + """A row saying "no archive symbol" rules its names and texts for the + installed headers alone: the same name in the archive or a kernel + literal is a finding (PineMap, round 3's blind probe, now flags there).""" + self.assertIn("PineMap", RULED.header_only) + self.assertIn("map.size: result exceeds int range", RULED.header_only) + self.assertNotIn("tv_carry_qty", RULED.header_only) + for surface in ("strings", "nm", "source-literal"): + with self.subTest(surface=surface): + strings, nm = table_archive() + if surface == "strings": + found = self.unruled(strings + ["PineMap"], nm) + elif surface == "nm": + found = self.unruled(strings, nm + ["T pineforge::PineMap::size()"]) + else: + found = self.unruled(strings, nm, + source_literals=(["PineMap"], ["src/probe.cpp:1"])) + self.assertIn("PineMap", found) + strings, nm = table_archive() + self.assertIn("map.size: result exceeds int range", + self.unruled(strings + ["map.size: result exceeds int range"], nm)) + # Where the row puts them, they pass. + findings, _ = evaluate_table(*table_archive()) + self.assertEqual(findings, [], [str(f) for f in findings]) + + def test_the_printed_counts_are_the_rulings_the_vocabulary_reads(self) -> None: + """kres K5: the mechanism tables' first columns were counted as rulings + (27 of "202" identifiers, 22 of "25" texts at 91d65ad6).""" + self.assertIn("priority", RULED.identifiers) + self.assertFalse(guard.vocabulary_name("priority")) + _, summary = evaluate_table(*table_archive()) + self.assertEqual(summary["ruledIdentifiers"], + sum(1 for name in RULED.identifiers if guard.vocabulary_name(name))) + self.assertLess(summary["ruledIdentifiers"], len(RULED.identifiers)) + self.assertEqual(summary["ruledPhrases"], + sum(1 for text in RULED.phrases if guard.vocabulary_text(text))) + self.assertLess(summary["ruledPhrases"], len(RULED.phrases)) + + def test_a_ruling_that_leaves_with_its_name_reads_below_the_floor(self) -> None: + """docs-a N7: a row and its name removed together pass every other + rule -- no unruled name, no stale row -- and only the floor sees it.""" + _, full = evaluate_table(*table_archive()) + row = next(line for line in ADR_TEXT.splitlines() + if line.startswith("| `tv_carry_qty` |")) + reverted = guard.ruled_entries(ADR_TEXT.replace(row + "\n", "")) + self.assertNotIn("tv_carry_qty", reverted.identifiers) + strings, nm = table_archive() + strings.remove("tv_carry_qty") + findings, summary = guard.evaluate(strings, nm, reverted, header_lines=table_headers()) + self.assertEqual(findings, [], [str(f) for f in findings]) + below = guard.floor_findings(summary, full["ruledIdentifiers"], full["ruledPhrases"]) + self.assertEqual([(f.kind, f.token) for f in below], [("floor", "ruled identifiers")]) + self.assertEqual(guard.floor_findings(full, full["ruledIdentifiers"], + full["ruledPhrases"]), []) + + class StripToolTests(unittest.TestCase): def test_a_host_without_a_stripping_tool_fails_closed(self) -> None: """No strip tool must be exit 2 with a message, never a debug-info read.""" diff --git a/scripts/test_ci_verify.py b/scripts/test_ci_verify.py index c335c666..2f3be297 100644 --- a/scripts/test_ci_verify.py +++ b/scripts/test_ci_verify.py @@ -1222,6 +1222,11 @@ def test_kernel_residuals_command_names_the_kernel_archive_and_the_adr(self): self.assertEqual(Path(argv[argv.index('--archive') + 1]).name, 'libpineforge_kernel.a') self.assertEqual(Path(argv[argv.index('--adr') + 1]).name, '0001-kernel-adapter-boundary.md') + # R5 lane H-DOCGATES: the ruled-count floors travel with the argv. + self.assertEqual(argv[argv.index('--min-ruled-identifiers') + 1], + str(ci_verify.ADR_RULED_IDENTIFIERS_MIN)) + self.assertEqual(argv[argv.index('--min-ruled-texts') + 1], + str(ci_verify.ADR_RULED_TEXTS_MIN)) def test_kernel_residuals_failure_stops_before_ctest(self): code, summary, scripted, _ = self.run_profile('kernel', **{'kernel-residuals': 1}) From e8c33942900347dcaf8f5def1b3250295da90890 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 03:31:14 +0800 Subject: [PATCH 10/83] Gates: a published sentence leaves only when its commit names it -- the doc-revert stage (R5 lane H-DOCGATES, X13) AUDIT4-opus docs-a (f)/N7: no silent doc revert reached main between fd785928 and 91d65ad6, but nothing would have caught one. Lane B-C-SURFACE's rebase kept its own base's pages, dropping V19-D's keep_handle / keep_binding row and putting D2-C's runtime-block wording back; the integration caught it by reading. scripts/check_doc_reverts.py compares each commit with its parent, sentence by sentence (paragraphs, list items, table cells of README.md, CONTRIBUTING.md and docs/**/*.md; anchor digits and content pins normalised). A deleted sentence, or older wording restored over newer (a sentence the page held before its parent), must be named by the commit's message: the lane label or hash of the commit that introduced it, six consecutive words of it, or its table row's key. Fresh rewrites and re-anchors need no name. Range: non-merge commits since the merge base with main; with no main ref (the lab's remote hosts) it walks back from HEAD; a commit whose tree predates the gate is never judged. --base/--head/--message-file judge one replayed change. Acceptance (both run on this tree): - 597a4367's nine pages replayed onto db98990c with 597a4367's message: "17 unnamed sentence deletions or reverts ... FAIL" -- V19-D's keep_handle rows (introduced by c619f5f) and D2-C's ambient-block wording reverted to "thread-local"; - e3e20cb5 with its own message: "0 unnamed ... OK"; the same change with the message "docs: tidy" fails on the 11 PERF-P1 sentences it deleted (introduced by 36a6a7e, cherry-picked from 3bd72fb). Preflight stages doc-reverts and doc-reverts-tests (binding); CTest row test_doc_reverts (10 cases over throwaway git repositories), so the floors rise: KERNEL_MIN_TESTS 272 -> 273, RELEASE_MIN_TESTS 673 -> 674. docs/ci.md, README.md, CONTRIBUTING.md and contributing-llm.md name the gate; the floor citations re-anchored (ci_verify.py:229 -> :231, :332 -> :336). No sentence is deleted by this commit. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 34ae339e3488c2ec22e6724643d65d2ecea73a48) INT26 pick note: the floors conflicted with INT25's and H-MEASURE's; each floor adds this commit's one row to the merged value (KERNEL 278 -> 279, RELEASE 690 -> 691), both comments kept, and the four floor citations name the merged lines (ci_verify.py:249 and :375). hunkcheck: every other hunk of 34ae339e carried unchanged. --- CONTRIBUTING.md | 5 +- README.md | 1 + docs/ci.md | 32 ++- docs/pages/contributing-llm.md | 11 +- scripts/check_doc_reverts.py | 439 ++++++++++++++++++++++++++++++ scripts/ci_preflight.py | 7 + scripts/ci_verify.py | 8 +- scripts/test_check_doc_reverts.py | 197 ++++++++++++++ scripts/test_ci_preflight.py | 3 +- tests/CMakeLists.txt | 4 + 10 files changed, 696 insertions(+), 11 deletions(-) create mode 100644 scripts/check_doc_reverts.py create mode 100644 scripts/test_check_doc_reverts.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 887115b8..ed86fd5a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -144,13 +144,14 @@ What each gate refuses: | `check_doc_anchors.py` | a `file:line` citation that no longer points at the symbol it claims | | `check_doc_lint.py` | a stale epoch, a roadmap label or a "there is no … yet" claim the tree has falsified | | `check_pine_to_native_coverage.py` | a Pine builtin with no row on the migration page | +| `check_doc_reverts.py` | a published sentence deleted, or older wording restored over newer, by a commit whose message does not name it (by the lane label or hash of the commit that wrote it, or by six of its words) | | the CTest row **floors** | a test row that vanished from a profile | ### The floors `ci_verify.py` counts the CTest rows that actually **ran** and fails below a -floor — `KERNEL_MIN_TESTS` ci_verify.py:247 and `RELEASE_MIN_TESTS` -ci_verify.py:371. +floor — `KERNEL_MIN_TESTS` ci_verify.py:249 and `RELEASE_MIN_TESTS` +ci_verify.py:375. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/README.md b/README.md index b42edcf1..83e4c412 100644 --- a/README.md +++ b/README.md @@ -397,6 +397,7 @@ The gates a pull request passes, one line each: | C surface | `scripts/check_c_abi_runtime.py`, `scripts/check_native_c_api_surface.py` | A `PF_API` export added without its inventory row; a public host member with no C spelling and no recorded reason. | | Twin parity | `scripts/check_twin_parity.py` | A frozen assertion quietly rewritten instead of a behaviour change being argued. | | Documentation | `scripts/check_doc_anchors.py`, `scripts/check_doc_lint.py`, `scripts/check_pine_to_native_coverage.py` | A `file:line` citation that no longer points at its symbol; a stale epoch, roadmap label or negative claim; a Pine builtin with no row on the migration page. | +| Doc reverts | `scripts/check_doc_reverts.py` | A published sentence deleted, or older wording restored over newer, by a commit whose message does not name it. | New here? [CONTRIBUTING.md](CONTRIBUTING.md) is the human walkthrough of all of the above; [Contributing as an LLM](docs/pages/contributing-llm.md) is the same diff --git a/docs/ci.md b/docs/ci.md index 89cf62b8..2a33c2e4 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -420,8 +420,8 @@ drifted". ## Documentation guards The documentation guards run fail-closed. Source-only `ci_preflight` stages include -`doc-anchors`, `doc-lint`, `design-inventory`, `doc-pine-coverage`, its self-test, -and the detached-comment ceiling. The migration worked example is an executed +`doc-anchors`, `doc-lint`, `doc-reverts`, `design-inventory`, `doc-pine-coverage`, +their self-tests, and the detached-comment ceiling. The migration worked example is an executed CTest row, and the Doxygen documentation build runs in CI for pull requests, main pushes, tags and manual dispatch (`.github/workflows/docs.yml`). A pull request builds and uploads the site; the Cloudflare Pages deploy step is @@ -475,6 +475,34 @@ python3 scripts/ci_preflight.py # doc-anchors and doc-lint among its stag Their own self-tests are binding as well and register as CTest rows (`test_doc_anchors`, `test_doc_lint`). +`scripts/check_doc_reverts.py` (`doc-reverts`, with `doc-reverts-tests`) holds +what the other two cannot see: a sentence that leaves a published page, or older +text put back over newer text, cites nothing wrong. Each commit it judges is +compared with its parent sentence by sentence — paragraphs, list items and table +cells of `README.md`, `CONTRIBUTING.md` and `docs/**/*.md`, anchor digits and +content pins normalised away — and every sentence the commit deletes, or reverts +to a wording the page held before, must be named by the commit's message: by the +lane label or the hash of the commit that introduced it, by six consecutive +words of it, or by the key of its table row (`OL7`). A fresh rewrite and a +re-anchor need no name. It judges the non-merge commits since the merge base +with `main`; with no `main` ref (the lab's remote hosts) it walks back from +`HEAD`, and a commit whose tree predates the gate is never judged. +`--base R --head R [--message-file F]` judges one replayed change instead: +lane B-C-SURFACE's pages replayed onto `db98990c` (the rebase that dropped +V19-D's `keep_handle` row and put D2-C's runtime-block wording back) fail it, +and `e3e20cb5`, V19-A's stated revert of PERF-P1's continuation view, passes with +its own message. + +```sh +python3 scripts/check_doc_reverts.py # commits since the merge base +python3 scripts/check_doc_reverts.py --base db98990c --head 597a4367 --message-from 597a4367 \ + --files docs/adr/0001-kernel-adapter-boundary.md docs/pages/native-engine.md # a replay +``` + +The kernel-residual gate's ruled counts have floors beside the CTest floors +(`ADR_RULED_IDENTIFIERS_MIN`, `ADR_RULED_TEXTS_MIN` in `scripts/ci_verify.py`), so +a ruling that leaves ADR-0001 together with its name is an edit of that file too. + The third source-only guard is `scripts/check_design_inventory.py` (`design-inventory`, with its own `design-inventory-tests` suite). It was fail-closed from the day it landed: it reads `docs/design/native-feature-parity.md` diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index d481e090..b9bddf14 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -103,11 +103,14 @@ finding to report, not a step to take. file's own docstring), `scripts/check_doc_lint.py` for stale epochs, roadmap labels and falsified negative claims, and `scripts/check_pine_to_native_coverage.py` for the migration page's - coverage claim. + coverage claim. A sentence leaves a page only when the commit's message + names it — the lane or commit that wrote it, or six of its words — + and older wording never silently replaces newer: *Enforced:* + `scripts/check_doc_reverts.py`. 10. **A test row never silently disappears.** Each profile counts the rows that - *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:247 and - `RELEASE_MIN_TESTS` ci_verify.py:371. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:249 and + `RELEASE_MIN_TESTS` ci_verify.py:375. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -253,7 +256,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:247, `RELEASE_MIN_TESTS` ci_verify.py:371). It +(`KERNEL_MIN_TESTS` ci_verify.py:249, `RELEASE_MIN_TESTS` ci_verify.py:375). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/scripts/check_doc_reverts.py b/scripts/check_doc_reverts.py new file mode 100644 index 00000000..72b976d5 --- /dev/null +++ b/scripts/check_doc_reverts.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python3 +"""A documentation sentence may leave only when its commit says so. + +Why this exists +--------------- +The fourth claimed-vs-actual audit (AUDIT4-opus docs-a, finding (f) and N7) +walked every merge from fd785928 to 91d65ad6 and found that no silent doc +revert had reached ``main`` -- and that nothing would have caught one. Its +precedent was real: lane B-C-SURFACE's rebase kept its own base's pages, which +dropped V19-D's ``keep_handle`` / ``keep_binding`` row and put D2-C's +runtime-block wording back to its pre-D2-C text; the integration caught it by +reading, not by a gate. The anchor gate cannot see such a revert (the reverted +text cites nothing wrong), and the residual gate sees it only when the row +carries a name its vocabulary reads. + +What it checks +-------------- +For every commit it judges, the published documentation -- ``README.md``, +``CONTRIBUTING.md`` and ``docs/**/*.md`` -- is read at the commit and at its +parent and split into sentences: paragraphs, list items and table cells, cut at +sentence ends, with anchor digits and content pins normalised away (a +re-anchor is not an edit), and at least ``MIN_CHARS`` long. A sentence the +parent has and the commit does not is + +* a **rewrite** when the commit adds a sentence at least ``REWRITE_RATIO`` + similar to it, and that sentence is new; nothing more is asked; +* a **revert** when that similar sentence is not new: the file held it in its + history before the parent (within ``HISTORY_DEPTH`` versions) and the parent + had dropped it -- older text coming back over newer text; +* a **deletion** otherwise. + +Every revert and every deletion must be NAMED by the commit's message, which +names a sentence when it carries any of: + +* the short hash (``SHA_CHARS`` hex digits or more) of the commit that + introduced the sentence, or of the commit that one was cherry-picked from; +* a lane label of that commit's subject (``R5 lane V19-D`` -> ``V19-D``, + ``INT23``, ``lane F6`` -> ``F6``); +* ``FRAGMENT_WORDS`` consecutive words of the sentence itself; +* the key of the table row the sentence sat in (``OL7``, ``SZ10a``, a + backticked name in the row's first cell). + +Which commits +------------- +``--base REF`` and ``--head REF`` judge the one change base -> head, with the +messages of every commit in ``base..head`` (``--message-from REF`` or +``--message-file PATH`` adds to them): that is the replay mode the audit's +acceptance uses (597a4367's pages replayed onto db98990c must fail; e3e20cb5, +V19-A's stated revert of PERF-P1's view, must pass with its own message). With +``--files`` the pages judged are limited to those given. + +Without ``--head`` the gate walks the non-merge commits of ``base..HEAD`` one +by one, each against its parent and with its own message. ``base`` is +``--base``, else ``$PINEFORGE_DOC_REVERT_BASE``, else the merge base with +``origin/main`` or ``main``; where none resolves (a detached checkout with no +branch refs, as the lab's remote hosts have) the walk starts at ``HEAD`` and +stops at the first commit that predates this gate. A commit whose own tree +has no ``scripts/check_doc_reverts.py`` is never judged: the rule binds from +the commit that introduced it on. At most ``MAX_COMMITS`` commits are walked. + +Exit 0 when every revert and deletion is named, 1 when one is not, 2 when git +or a named ref cannot be read. +""" +from __future__ import annotations + +import argparse +from collections import Counter +from dataclasses import dataclass, field +import difflib +import os +from pathlib import Path +import re +import subprocess +import sys + +ROOT = Path(__file__).resolve().parent.parent +SELF = 'scripts/check_doc_reverts.py' +DOC_ROOTS = ('README.md', 'CONTRIBUTING.md', 'docs') +MIN_CHARS = 30 +REWRITE_RATIO = 0.7 +HISTORY_DEPTH = 400 +MAX_COMMITS = 400 +FRAGMENT_WORDS = 6 +SHA_CHARS = 7 + +FENCE = re.compile(r'^\s*(```|~~~)') +BULLET = re.compile(r'^(?:[-*+]|\d+[.)])\s') +RULE_CELL = re.compile(r'\s*:?-{2,}:?\s*') +# Sentence ends: a terminal mark before a capital, a backtick, an emphasis or +# a bracket; and a colon before a capital (the pages' "Scope: ..."). +SPLIT = re.compile(r'(?<=[.;!?])\s+(?=[A-Z`*(\["])|(?<=:)\s+(?=[A-Z`*])') +PIN = re.compile(r'(?i)sha256:[0-9a-f]{64}') +DIGITS = re.compile(r':\d+(?:-\d+)?(?!\d)') +LANE_LABELS = ( + re.compile(r'\bR5 (?:gap )?lane ([A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])'), + re.compile(r'\blanes? ([A-Z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*)'), + re.compile(r'\b(INT\d+[a-z]?)\b'), +) +CHERRY = re.compile(r'\(cherry picked from commit ([0-9a-f]{7,40})\)') +ROW_KEY = re.compile(r'^(?:[A-Za-z]+[0-9]+[A-Za-z]?(?:-[A-Za-z0-9]+)?|`[^`]+`)$') + + +class GitError(Exception): + """git could not answer; exit status 2.""" + + +def git(*args: str, root: Path = ROOT, check: bool = True) -> str: + result = subprocess.run(['git', '-C', str(root), *args], capture_output=True, text=True) + if check and result.returncode: + raise GitError('git ' + ' '.join(args) + ': ' + result.stderr.strip()) + return result.stdout if result.returncode == 0 else '' + + +def normalise(sentence: str) -> str: + sentence = PIN.sub('sha256:H', sentence) + sentence = DIGITS.sub(':N', sentence) + return re.sub(r'\s+', ' ', sentence).strip() + + +@dataclass(frozen=True) +class Unit: + text: str # normalised + row_key: str # the first cell of the table row it sat in, or '' + + +def units(text: str) -> Counter: + """The sentence units of a page, as a multiset of Unit.""" + out: Counter = Counter() + blocks: list[tuple[str, str]] = [] + buf: list[str] = [] + fenced = False + + def flush() -> None: + if buf: + blocks.append((' '.join(buf), '')) + buf.clear() + + for line in text.split('\n'): + if FENCE.match(line): + fenced = not fenced + flush() + continue + if fenced: + continue + stripped = line.strip() + if not stripped or stripped.startswith('#') or BULLET.match(stripped): + flush() + if stripped.startswith('#'): + blocks.append((stripped, '')) + elif stripped: + buf.append(stripped) + continue + if stripped.startswith('|'): + flush() + cells = [cell.strip() for cell in stripped.strip('|').split('|')] + key = cells[0].strip('* ') if cells else '' + key = key if ROW_KEY.match(key) else '' + for cell in cells: + if cell and not RULE_CELL.fullmatch(cell): + blocks.append((cell, key)) + continue + buf.append(stripped) + flush() + for block, key in blocks: + for sentence in SPLIT.split(block): + sentence = normalise(sentence) + if len(sentence) >= MIN_CHARS: + out[Unit(sentence, key)] += 1 + return out + + +def doc_paths(ref: str | None, root: Path) -> list[str]: + """The published pages at `ref` (None: the working tree).""" + if ref is None: + found = [] + for top in DOC_ROOTS: + path = root / top + if path.is_file(): + found.append(top) + elif path.is_dir(): + found.extend(p.relative_to(root).as_posix() for p in sorted(path.rglob('*.md'))) + return found + listed = git('ls-tree', '-r', '--name-only', ref, '--', *DOC_ROOTS, root=root).split('\n') + return [p for p in listed if p.endswith('.md')] + + +def show(ref: str, path: str, root: Path) -> str: + return git('show', f'{ref}:{path}', root=root, check=False) + + +@dataclass +class Finding: + path: str + kind: str # "deletion" or "revert" + sentence: str + introduced: str # short hash of the commit that introduced it, or '' + subject: str + names: list[str] = field(default_factory=list) + partner: str = '' + + def __str__(self) -> str: + what = (f'{self.kind} of {self.sentence[:150]!r}' + + (f' (older text restored: {self.partner[:90]!r})' if self.partner else '')) + who = (f'introduced by {self.introduced} "{self.subject[:90]}"' + if self.introduced else 'introducer not found in the history read') + return (f'{self.path}: unnamed {what}; {who}; the message names it with any of: ' + + ', '.join(self.names + [f'{FRAGMENT_WORDS} consecutive words of it'])) + + +class History: + """A page's versions reachable from one commit, newest first, read lazily.""" + + def __init__(self, root: Path, tip: str, path: str) -> None: + self.root, self.tip, self.path = root, tip, path + self._commits: list[str] | None = None + self._units: dict[str, Counter] = {} + + def commits(self) -> list[str]: + if self._commits is None: + out = git('rev-list', f'--max-count={HISTORY_DEPTH}', self.tip, '--', self.path, + root=self.root, check=False) + self._commits = out.split() + return self._commits + + def units_at(self, commit: str) -> Counter: + if commit not in self._units: + self._units[commit] = units(show(commit, self.path, self.root)) + return self._units[commit] + + def texts_at(self, commit: str) -> set[str]: + return {unit.text for unit in self.units_at(commit)} + + def introducer(self, text: str) -> str | None: + """The commit that last brought `text` into the page (newest first walk: + the oldest version of the unbroken run of versions that hold it).""" + found = None + for commit in self.commits(): + if text in self.texts_at(commit): + found = commit + elif found is not None: + return found + return found + + def seen_before(self, text: str) -> bool: + """Whether a version older than the tip held `text`.""" + return any(text in self.texts_at(commit) for commit in self.commits()[1:]) + + +def labels(message: str) -> list[str]: + subject = message.split('\n', 1)[0] + found: list[str] = [] + for pattern in LANE_LABELS: + for label in pattern.findall(subject): + if label not in found and label.lower() not in ('lane', 'lanes'): + found.append(label) + return found + + +def message_of(commit: str, root: Path) -> str: + return git('log', '-1', '--format=%B', commit, root=root) + + +def names_for(commit: str | None, root: Path) -> list[str]: + if not commit: + return [] + message = message_of(commit, root) + names = [commit[:SHA_CHARS]] + labels(message) + names += [sha[:SHA_CHARS] for sha in CHERRY.findall(message)] + return names + + +def fold(text: str) -> str: + return re.sub(r'\s+', ' ', re.sub(r'[`*_]', '', text)).strip().lower() + + +def named(message: str, finding: Finding, row_key: str) -> bool: + folded = fold(message) + for name in finding.names: + if re.fullmatch(r'[0-9a-f]+', name): + if re.search(r'\b' + name + r'[0-9a-f]*\b', message): + return True + elif re.search(r'(?= FRAGMENT_WORDS and window in folded: + return True + return False + + +def judge_change(root: Path, parent: str, head_texts: dict[str, str], message: str, + history_tip: str) -> list[Finding]: + """The unnamed deletions and reverts of one change parent -> head. + + `head_texts` maps each page to its text at the head (the pages changed); + `history_tip` is the commit whose history the introducer and revert + questions read (the parent, for a commit).""" + findings: list[Finding] = [] + for path, head_text in sorted(head_texts.items()): + before = units(show(parent, path, root)) + after = units(head_text) + removed = before - after + if not removed: + continue + added_texts = [unit.text for unit in (after - before).elements()] + history = History(root, history_tip, path) + for unit in removed.elements(): + partner, ratio = None, 0.0 + for candidate in added_texts: + matcher = difflib.SequenceMatcher(None, unit.text, candidate, autojunk=False) + if matcher.quick_ratio() <= ratio: + continue + exact = matcher.ratio() + if exact > ratio: + partner, ratio = candidate, exact + kind = 'deletion' + if partner is not None and ratio >= REWRITE_RATIO: + if not history.seen_before(partner): + continue # a fresh rewrite + kind = 'revert' + introduced = history.introducer(unit.text) + finding = Finding(path, kind, unit.text, + introduced[:SHA_CHARS] if introduced else '', + message_of(introduced, root).split('\n', 1)[0] if introduced else '', + names_for(introduced, root), + partner if kind == 'revert' else '') + if not named(message, finding, unit.row_key): + findings.append(finding) + return findings + + +def resolve(ref: str, root: Path) -> str: + out = git('rev-parse', '--verify', '--quiet', ref + '^{commit}', root=root, check=False).strip() + if not out: + raise GitError(f'cannot resolve {ref!r}') + return out + + +def default_base(root: Path) -> str | None: + env = os.environ.get('PINEFORGE_DOC_REVERT_BASE') + if env: + return resolve(env, root) + for ref in ('origin/main', 'main'): + tip = git('rev-parse', '--verify', '--quiet', ref + '^{commit}', root=root, + check=False).strip() + if tip: + base = git('merge-base', 'HEAD', tip, root=root, check=False).strip() + if base: + return base + return None + + +def judged_commits(root: Path, base: str | None) -> list[str]: + spec = [f'{base}..HEAD'] if base else ['HEAD'] + commits = git('rev-list', '--no-merges', f'--max-count={MAX_COMMITS}', *spec, + root=root).split() + kept = [] + for commit in commits: + if not git('cat-file', '-e', f'{commit}:{SELF}', root=root, check=False) and \ + subprocess.run(['git', '-C', str(root), 'cat-file', '-e', f'{commit}:{SELF}'], + capture_output=True).returncode: + if base is None: + break # the walk reached the commits before this gate + continue + kept.append(commit) + return kept + + +def changed_pages(root: Path, parent: str, commit: str) -> list[str]: + out = git('diff', '--name-only', parent, commit, '--', *DOC_ROOTS, root=root) + return [p for p in out.split() if p.endswith('.md')] + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument('--root', type=Path, default=ROOT) + parser.add_argument('--base', help='judge from this commit (default: the merge base with main)') + parser.add_argument('--head', help='judge the one change base -> HEAD-of-this-ref') + parser.add_argument('--files', nargs='*', default=None, + help='with --head: judge only these pages') + parser.add_argument('--message-from', action='append', default=[], + help='also read this commit\'s message (repeatable)') + parser.add_argument('--message-file', type=Path, default=None, + help='also read this file as message text') + args = parser.parse_args(argv) + root = args.root.resolve() + try: + extra = ''.join(message_of(resolve(ref, root), root) + '\n' for ref in args.message_from) + if args.message_file is not None: + extra += args.message_file.read_text(encoding='utf-8') + '\n' + if args.head: + if not args.base: + raise GitError('--head needs --base') + base, head = resolve(args.base, root), resolve(args.head, root) + pages = args.files if args.files is not None else sorted( + set(changed_pages(root, base, head))) + # The messages that may name a deletion: the ones given, or else + # those of every commit the change consists of. + messages = extra or ''.join( + message_of(c, root) + '\n' + for c in git('rev-list', f'{base}..{head}', root=root, check=False).split()) + texts = {path: show(head, path, root) for path in pages} + findings = judge_change(root, base, texts, messages, base) + judged = f'{base[:SHA_CHARS]} -> {head[:SHA_CHARS]} ({len(pages)} pages)' + else: + base = resolve(args.base, root) if args.base else default_base(root) + commits = judged_commits(root, base) + findings = [] + for commit in commits: + parents = git('rev-list', '--parents', '-n', '1', commit, root=root).split()[1:] + parent = parents[0] if parents else None + if parent is None: + continue + pages = changed_pages(root, parent, commit) + if not pages: + continue + texts = {path: show(commit, path, root) for path in pages} + findings += judge_change(root, parent, texts, message_of(commit, root) + extra, + parent) + judged = (f'{len(commits)} commits since ' + + (base[:SHA_CHARS] if base else 'this gate landed')) + except GitError as error: + print(f'check_doc_reverts: {error}', file=sys.stderr) + return 2 + for finding in findings: + print('check_doc_reverts: ' + str(finding), file=sys.stderr) + verdict = 'FAIL' if findings else 'OK' + print(f'doc reverts: {judged}: {len(findings)} unnamed sentence deletions or reverts ' + f'... {verdict}') + return 1 if findings else 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index ebdef01e..c272dc7d 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -223,6 +223,13 @@ def check_commands(source: Path) -> list[tuple]: [sys.executable, str(source / 'scripts/test_check_doc_lint.py')]), ('doc-lint', [sys.executable, str(source / 'scripts/check_doc_lint.py')]), + # R5 lane H-DOCGATES (AUDIT4-opus X13): a published sentence leaves only + # when its commit's message names it -- by the introducing lane or commit, + # or by its words -- and older text never silently replaces newer text. + ('doc-reverts-tests', + [sys.executable, str(source / 'scripts/test_check_doc_reverts.py')]), + ('doc-reverts', + [sys.executable, str(source / 'scripts/check_doc_reverts.py')]), # NOT advisory: the migration page's coverage claim is L14-B's own, and # it is true on this tree. A `strategy.*` name added to the Pine v6 # inventory without a row on that page fails here immediately. diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 37b4ee92..fc019171 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -244,7 +244,9 @@ # +1 H-MEASURE test_native_acid_composite (the G1 acid composite, C++ and C) # Its other eleven rows reach the source layer (see RELEASE_MIN_TESTS). 279 # registered, 278 run: the WebSocket row still skips on a system libcurl. -KERNEL_MIN_TESTS = 278 +# 279 = those 278 plus R5 lane H-DOCGATES' test_doc_reverts (the doc-revert +# gate's self-test, a Python row every profile registers; INT26 pick, summed). +KERNEL_MIN_TESTS = 279 # Release-row floor, the same gate for the default profile. Before lane P7 # only the kernel profile had one, so a row that left release alone (a # source-bound TU dropped from TEST_SOURCES, a deleted twin or ABI row) left a @@ -368,7 +370,9 @@ # test_magnified_aggregated_tape, # test_adapter_security_route_conditions # No release row skips, so 690 registered is 690 run. -RELEASE_MIN_TESTS = 690 +# 691 = those 690 plus R5 lane H-DOCGATES' test_doc_reverts (see +# KERNEL_MIN_TESTS; INT26 pick, summed). +RELEASE_MIN_TESTS = 691 # ADR-0001 ruled-count floors, beside the ctest floors (R5 lane H-DOCGATES, # AUDIT4-opus X13 / docs-a N7). check_kernel_residuals.py counts the rulings # its vocabulary reads -- 174 identifiers and 45 texts on the lane's tree: the diff --git a/scripts/test_check_doc_reverts.py b/scripts/test_check_doc_reverts.py new file mode 100644 index 00000000..0bc24d5e --- /dev/null +++ b/scripts/test_check_doc_reverts.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Self-tests for the doc-revert gate: it must be able to FAIL. + +Each case builds a throwaway git repository whose commits carry the gate +script (so the gate binds them), writes a page, and drives the real +``check_doc_reverts.main`` over it: a sentence deleted without a name fails, +and is passed once the commit's message names it by the introducing lane, by +its commit's hash or by its own words; older text restored over newer text is +a revert and fails the same way; a fresh rewrite and a re-anchor pass without +a name; a commit that predates the gate is not judged; and the replay mode the +audit's acceptance uses reads only the message it is given. + +R5 lane H-DOCGATES (AUDIT4-opus X13, docs-a GAP-2). +""" +from __future__ import annotations + +import contextlib +import io +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +import check_doc_reverts as guard + +GATE = Path(guard.__file__).resolve() + +BASE_PAGE = """# Page + +The kernel books every lot at its fill price and never rounds it. + +| ID | Rule | Note | +|---|---|---| +| OL7 | The deferred form of a request waits for its owner to fill first. | kept | +""" +KEEP_HANDLE = "A replacement that keeps its handle keeps its lineage and its dependents too." + + +class Repo: + """A git repository with the gate script in every commit.""" + + def __init__(self) -> None: + self.dir = tempfile.TemporaryDirectory(prefix='pineforge-doc-reverts-') + self.root = Path(self.dir.name) + self.git('init', '-q', '-b', 'lane') # no `main`: the walk mode, as on a remote host + self.git('config', 'user.email', 'test@example.invalid') + self.git('config', 'user.name', 'doc-revert test') + (self.root / 'scripts').mkdir() + shutil.copyfile(GATE, self.root / 'scripts' / 'check_doc_reverts.py') + (self.root / 'docs' / 'pages').mkdir(parents=True) + self.page = self.root / 'docs' / 'pages' / 'guide.md' + + def git(self, *args: str) -> str: + return subprocess.run(['git', '-C', str(self.root), *args], capture_output=True, + text=True, check=True).stdout.strip() + + def commit(self, text: str, message: str, *, gate: bool = True) -> str: + self.page.write_text(text) + script = self.root / 'scripts' / 'check_doc_reverts.py' + if not gate and script.exists(): + script.unlink() # `git add -A` records the removal + elif gate and not script.exists(): + shutil.copyfile(GATE, script) + self.git('add', '-A') + self.git('commit', '-q', '-m', message) + return self.git('rev-parse', 'HEAD') + + def run(self, *args: str) -> tuple[int, str]: + out, err = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): + code = guard.main(['--root', str(self.root), *args]) + return code, out.getvalue() + err.getvalue() + + def close(self) -> None: + self.dir.cleanup() + + +@contextlib.contextmanager +def repo(): + made = Repo() + try: + yield made + finally: + made.close() + + +def with_keep_handle(page: str = BASE_PAGE) -> str: + return page + '\n' + KEEP_HANDLE + '\n' + + +class MustFail(unittest.TestCase): + def setup_history(self, r: Repo) -> tuple[str, str]: + base = r.commit(BASE_PAGE, 'docs: the guide') + added = r.commit(with_keep_handle(), 'Docs: keep_handle on the guide (R5 lane V19-D)') + return base, added + + def test_an_unnamed_deletion_fails(self) -> None: + with repo() as r: + base, added = self.setup_history(r) + r.commit(BASE_PAGE, 'docs: tidy the guide') + code, out = r.run('--base', added) + self.assertEqual(code, 1, out) + self.assertIn('unnamed deletion', out) + self.assertIn('V19-D', out) # the name that would have passed + self.assertIn(added[:guard.SHA_CHARS], out) + + def test_restoring_older_text_is_a_revert(self) -> None: + """The B-C-SURFACE precedent: a rebase that kept its own base's page put + older wording back over newer wording. Similar text, so not a deletion, + but the text is the page's own past.""" + with repo() as r: + old = 'The ambient default is thread-local, and each scope restores it on exit.' + new = 'The ambient default is the running pump\'s block, and each scope restores it on exit.' + r.commit(BASE_PAGE + '\n' + old + '\n', 'docs: the ambient default') + moved = r.commit(BASE_PAGE + '\n' + new + '\n', 'Docs: the pump block (R5 lane D2-C)') + r.commit(BASE_PAGE + '\n' + old + '\n', 'docs: wording') + code, out = r.run('--base', moved) + self.assertEqual(code, 1, out) + self.assertIn('unnamed revert', out) + self.assertIn('D2-C', out) + + def test_a_deleted_table_row_fails(self) -> None: + with repo() as r: + base = r.commit(BASE_PAGE, 'docs: the guide') + r.commit(BASE_PAGE.replace( + '| OL7 | The deferred form of a request waits for its owner to fill first. | kept |\n', + ''), 'docs: tidy the table') + code, out = r.run('--base', base) + self.assertEqual(code, 1, out) + self.assertIn('deferred form of a request', out) + + def test_replay_reads_only_the_message_it_is_given(self) -> None: + with repo() as r: + base, added = self.setup_history(r) + tidy = r.commit(BASE_PAGE, 'docs: drop the V19-D keep_handle sentence') + message = r.root / 'message.txt' + message.write_text('docs: tidy the guide\n') + code, out = r.run('--base', added, '--head', tidy, '--message-file', str(message)) + self.assertEqual(code, 1, out) + self.assertIn('unnamed deletion', out) + code, out = r.run('--base', added, '--head', tidy) # its own message names it + self.assertEqual(code, 0, out) + + +class MustPass(unittest.TestCase): + def deleted_with(self, message: str) -> tuple[int, str]: + with repo() as r: + r.commit(BASE_PAGE, 'docs: the guide') + added = r.commit(with_keep_handle(), 'Docs: keep_handle on the guide (R5 lane V19-D)') + r.commit(BASE_PAGE, message.replace('', added[:guard.SHA_CHARS])) + return r.run('--base', added) + + def test_named_by_the_introducing_lane(self) -> None: + code, out = self.deleted_with('docs: V19-D\'s keep_handle sentence moves to the ADR') + self.assertEqual(code, 0, out) + + def test_named_by_the_introducing_commit(self) -> None: + code, out = self.deleted_with('docs: drop the sentence added') + self.assertEqual(code, 0, out) + + def test_named_by_its_own_words(self) -> None: + code, out = self.deleted_with('docs: "keeps its handle keeps its lineage and" is gone') + self.assertEqual(code, 0, out) + + def test_a_table_row_named_by_its_key(self) -> None: + with repo() as r: + base = r.commit(BASE_PAGE, 'docs: the guide') + r.commit(BASE_PAGE.replace( + '| OL7 | The deferred form of a request waits for its owner to fill first. | kept |\n', + ''), 'docs: design row OL7 is retired') + code, out = r.run('--base', base) + self.assertEqual(code, 0, out) + + def test_a_fresh_rewrite_and_a_reanchor_need_no_name(self) -> None: + with repo() as r: + base = r.commit(BASE_PAGE + '\nThe host reads `x` engine.hpp:12 for the price.\n', + 'docs: the guide') + r.commit(BASE_PAGE.replace('never rounds it', 'never rounds the price') + + '\nThe host reads `x` engine.hpp:40 for the price.\n', 'docs: wording') + code, out = r.run('--base', base) + self.assertEqual(code, 0, out) + + def test_a_commit_before_the_gate_is_not_judged(self) -> None: + with repo() as r: + r.commit(BASE_PAGE, 'docs: the guide', gate=False) + r.commit(with_keep_handle(), 'Docs: keep_handle (R5 lane V19-D)', gate=False) + r.commit(BASE_PAGE, 'docs: tidy', gate=False) # predates the gate + r.commit(BASE_PAGE + '\nA new sentence that the gate itself brings along.\n', + 'gates: the doc-revert gate lands') + code, out = r.run() # no base: walk to the gate + self.assertEqual(code, 0, out) + self.assertIn('1 commits since this gate landed', out) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/test_ci_preflight.py b/scripts/test_ci_preflight.py index 047c0bbf..0b1c1b1c 100644 --- a/scripts/test_ci_preflight.py +++ b/scripts/test_ci_preflight.py @@ -115,7 +115,8 @@ def test_every_documentation_stage_is_binding(self): # declares advisory by carrying a third element; none of these may. plan = {entry[0]: entry for entry in check_commands(Path('/src'))} for name in ('doc-anchors', 'doc-lint', 'doc-anchors-tests', 'doc-lint-tests', - 'doc-pine-coverage', 'design-inventory', 'design-inventory-tests'): + 'doc-pine-coverage', 'design-inventory', 'design-inventory-tests', + 'doc-reverts', 'doc-reverts-tests'): self.assertIn(name, plan) self.assertEqual(len(plan[name]), 2, name) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bfe0989d..7c0a8268 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2306,6 +2306,10 @@ add_test(NAME test_doc_anchors COMMAND ${Python3_EXECUTABLE} -B ${PROJECT_SOURCE_DIR}/scripts/test_check_doc_anchors.py) add_test(NAME test_doc_lint COMMAND ${Python3_EXECUTABLE} -B ${PROJECT_SOURCE_DIR}/scripts/test_check_doc_lint.py) +# R5 lane H-DOCGATES (AUDIT4-opus X13): the doc-revert gate's must-fail suite, +# over throwaway git repositories; the gate itself is a ci_preflight stage. +add_test(NAME test_doc_reverts COMMAND ${Python3_EXECUTABLE} -B + ${PROJECT_SOURCE_DIR}/scripts/test_check_doc_reverts.py) # R5 F2: the worked migration of docs/pages/pine-to-native.md, executed. The # extractor copies the page's six C++ blocks out verbatim at build time and From cca5c1d6b630eb1ef711e5de0872be5e540553f6 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 03:36:43 +0800 Subject: [PATCH 11/83] Gates: a content pin accompanies a claim, never replaces it; a comma-borrowed citation claims nothing (R5 lane H-DOCGATES, X13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUDIT4-opus docs-a N1/N2 (GAP-1, GAP-5): a backticked sha256 pin before an anchor dropped the symbol claim and was compared alone, so whatever the cited line held passed -- a wrong line (P09), a blank line (P10), a garbage hash before a real one (P11), a ruling row pinned over unrelated code (P16) -- and 44 of the tree's 92 pins held something other than what their sentence claimed. In a citation list, a citation that prose introduced after a comma ("FX curve c_abi.cpp:882, probe c_abi.cpp:882") borrowed the list's first symbol. check_doc_anchors.py: - the nearest non-pin span before a pin is still the claim and is still checked; a ruling row needs its symbol or fragment, pinned or not; - PINLINE (a pin on a single line), PINEMPTY (a pin over blank lines), PINDOUBLE (two pins before one anchor); a pinned comment-only window is COMMENTONLY unless the sentence says it cites a comment; - a citation that prose words introduce after an earlier citation's comma is NOCLAIM until it carries its own backticked symbol. Self-tests 45 -> 53: PinMustFail (P09, P10, P11, P16, the comment window, a pinned range with its claim) and BorrowedClaim. Against 21bdc2e8's checker the six must-fail cases fail and the two controls pass (anchor_selftests.before.txt in the lane scratch). The pages: on HEAD the new rules raised 64 findings (23 PINLINE, 2 PINDOUBLE, 8 COMMENTONLY, 1 SYMMISS, 30 NOCLAIM). Every pin was re-derived on this tree (claim, window, verdict, true location) and re-cited as a symbol claim at its declaration, or as a pinned range whose sentence names the comment it cites; every comma-borrowed citation now names its own symbol at its true line (e.g. design OT5: FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:940, probe `strategy_execution_contract` c_abi.cpp:873; FP1's fill_through enforcement `protect_limit` :6103-6113 and InvalidTerms :4958-4976). The anchor gate reads 1657 anchors, 0 bad. docs/ci.md and the checker's docstring state the new rules. Doc sentences this commit deletes or rewrites as history (named for the doc-revert stage). Most of the pins it removes are B-DOCS' (d3cca9e, cherry-picked from 5c3c9ae), so the unpinned wording that returns in pine-to-native.md's `open_profit` rows and `strategy_create` row, and in design E2's margin sentence, is that commit's pins undone on purpose: - design §2.vi claim rows 2, 3, 6, 8 and 9: their plan-era anchor lists (B-DOCS' pins, 91d65ad6's re-cites) become "at 73817c1 (overtaken ...)"; row 1 is re-cited to today's Trigger/OrderIntent/Owner/Group/PointBudget; - design §2.i "Today: one static library always compiles the source layer" -> "Before L1" plus the tree's current CMake lines (the sentence 1dd5430 wrote, "sits in the *main* list", now "sat"); - design "Its callers were the adapter only (pine_adapter.cpp:2605, ...)" for the deleted admission_retention -> its history (73817c1d); - design "The dead helpers engine.hpp:1255, engine.hpp:1265, ... are the reference arithmetic" -> they are deleted; the kernel's own step is native_matching::apply_slippage; - design P6 "(`sha256` `sha256` native-engine.md:421-435)" -> the page section "Stream limitations (current)"; - design "degenerate equity metrics (engine_metrics.cpp:3)" drops its anchor (pre-L2 behaviour). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 85910410f08abac32922125b4c3b0e2a01db598e) INT26 pick note: the four pages conflicted where INT25 and H-MEASURE had moved or rewritten the rows this commit re-cites. Each merged row keeps both intents: - ADR "Margin": this commit's three claimed citations, then H-MEASURE's measured paragraph unchanged. - design rows 1-3, 5, 6, 9 and P4: this commit's re-citations (symbol claims, or "at `73817c1` (overtaken ...)"); INT25 had only moved their digits or re-hashed their pins. Rows 7 and 8 keep INT25's text (row 8 is C-SURFACE-1's current C-surface statement with its pins); this commit left row 7 alone and marked row 8's old statement overtaken. - design (a) NEUTRALITY: this commit's `PASS` claim; main rewrote the AGENTS.md section it cited (116c461b), so the pin now covers main's "A campaign PASS verdict still binds the exact engine and codegen HEADs" (AGENTS.md:139-140). - OL14 and pine-to-native's `pyramiding` row: word-level merges, INT25's and H-MEASURE's words with this commit's claims. Every other hunk of 85910410 is carried (hunkcheck); citation digits are re-anchored once on the integrated tree (INT26 item vii). --- docs/adr/0001-kernel-adapter-boundary.md | 35 ++--- docs/ci.md | 12 +- docs/design/native-feature-parity.md | 120 +++++++-------- docs/pages/contributing-llm.md | 4 +- docs/pages/native-engine.md | 8 +- docs/pages/pine-to-native.md | 10 +- scripts/check_doc_anchors.py | 185 +++++++++++++++++++---- scripts/test_check_doc_anchors.py | 131 ++++++++++++++++ 8 files changed, 386 insertions(+), 119 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 0aa48f9e..16cb0cbf 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -147,7 +147,7 @@ summary of it. the field comments say so, and `scripts/check_adapter_spec_shadowing.py` holds the adapter to the fields it actually needs. - **Feeding data.** Bars enter through the engine's own entry points: `run(bars, n)` - (`sha256:1eb25aef104b9f79988d37cffef5383089cac9098a163627c4709370ca830540` engine.hpp:1699), the timeframe `run` (`engine.hpp:1711`), the rich begin + (`engine.hpp:1709`), the timeframe `run` (`engine.hpp:1711`), the rich begin (`run` `engine.hpp:1784`), or `stream_begin` (`engine.hpp:1755`) / `stream_push_bar` (`engine.hpp:1761`) / `stream_push_tick(s)` / `stream_advance_time` / `stream_end` (`engine.hpp:1765`). `market_driver.hpp` is driver *types* (`NativeCoordinate` `market_driver.hpp:72`), not a feed API; read @@ -178,7 +178,7 @@ summary of it. `report_open_position_at_end` books the still-open position as a mark-to-market row under `KernelRecorded`. Under `HostRecorded` an unrecorded curve still degenerates — `compute_equity_stats` answers its all-NaN value with no error (`src/engine_metrics.cpp:219`, - consumed at `src/engine_report.cpp:119-139`) — which is why the policy exists. + consumed by `fill_metrics_section`, `src/engine_report.cpp:119-139`) — which is why the policy exists. - **Reproducibility.** Two digests, one wrapping the other. `native_continuation_hash()` (`native_host.hpp:1358`) returns the consumer's event/continuation hash. **A digest names the run's inputs** (rule 2): the timezone identity enters it as the zone's *content* — @@ -224,16 +224,16 @@ kernel and own these quirks, each at its site: `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2318-2319`); calc cadence and language publication are `PineScheduler`'s (`pine_scheduler.hpp:20`). - **Batching and open-order priority.** Same-bar command batching and its deferred queues - (`sha256:27e64b2cd0bbe283a8c83056538d66cd374e53cad072772c775a8529624768ac` pine_adapter.hpp:1793-1824); the retained parent-before-child ordering of live handles — an + (`PendingSameBarCommand` pine_adapter.hpp:1601, `pending_bracket_legs_` pine_adapter.hpp:1985-2003); 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 - `pine_adapter.cpp:1097-1176`). Kernel priority is acceptance/incarnation order. + `update_l4c_priority` `pine_adapter.cpp:1116-1186`). Kernel priority is acceptance/incarnation order. - **Exit-leg lifecycle.** Activation / suspension / revival barriers - (`sha256:fd8fba34d3e8008fe8e844cd6f251df0a343f07106bb48013c0301f77a10f3ff` src/compat/pine/exit_activation.cpp:42-60) and historical birth reach + (`select_exit_activation` src/compat/pine/exit_activation.cpp:42, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:10, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:46) 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:9591-9596`); the half-tick - trigger threshold (`sha256:bbd5b742d2109e4b01f97a199f3883c4037918f4e0e6a0cf8cbd99b98f823b0c` pine_adapter.cpp:341-353) and raw-vs-booked fill spelling, behind the + trigger threshold (`source_trigger_threshold` pine_adapter.cpp:333) and raw-vs-booked fill spelling, behind the adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11671`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:454-460`, twin `tv_money_round` `pine_policy_support.hpp:9-15`). @@ -246,7 +246,7 @@ kernel and own these quirks, each at its site: while a bare host books four 2.8e-17-unit dust rows beside them. - **Close reservations.** `strategy.close` callsite batching, two-call provenance and the entry-id ledger (`close_logical_units_` `pine_adapter.hpp:2023-2034`); the POOC reservation-growth population predicate - (`sha256:f524cf2b9e21e231672451db30ab94184ba3125d8ad6e4f9f614a0f4c538919e` src/compat/pine/reservation_expansion.cpp:9-20). + (`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:14069`) for the scheduling, @@ -263,8 +263,9 @@ kernel and own these quirks, each at its site: stays adapter-side: the `process_orders_on_close` chronology exception (`non_pooc_commissioned_short` `pine_adapter.cpp:16804`) and the 1x-long money call — plus the TV admission scopes - (`src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`:67-72`, - `:79-136`). The pre-open admission slice stays too, though the kernel's `AfterApplied` point + (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed + (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). + The pre-open admission slice stays too, though the kernel's `AfterApplied` point after the open fill measures the same bar. Measured by R5 lane H-MEASURE (`tests/test_adapter_margin_schedule_differential.cpp`: the same bars and orders through the adapter and through a bare host whose hooks answer TradingView's money and slice): the pre-open @@ -324,7 +325,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `src/compat/pine/` unit — `market_admission.cpp` included — is inside `PINEFORGE_SOURCE_LAYER_SOURCES` (`CMakeLists.txt:91`). - **Include independence already holds.** No kernel translation unit reaches a source header — - `sha256:e3312d2dd7ad7067ad41b26dfb3b502293dcdc5b98ed91563bd89f9482bb2b64` engine_aux_security.cpp:5-12, for instance, pulls only `engine_internal.hpp`, `ta.hpp` and std + `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13, for instance, pulls only `engine_internal.hpp`, `ta.hpp` and std headers. No kernel signature names an adapter type either: the rich begin bridge takes the host overrides as a plain `const void*` (`engine.hpp`, `execution_consumer.hpp`) and forwards it as `overrides_opaque` (`native_host.hpp:746`), which the kernel never dereferences; the @@ -618,7 +619,7 @@ Every line number in this section names its symbol on this tree, which | `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:13226-13227`): 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:2360-2362`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end`): report shape, not a mark-to-market row (`sha256:71a6a8622f560fca3a27d37e098ea8b6f68f831abcca0b6c46ad004d3ffe9358` pine_adapter.cpp:2334-2341) | design row RP5; the kernel reads the field under `KernelRecorded` only, which `scripts/check_adapter_spec_shadowing.py` gates | `examples/native/native_sized_report_strategy.cpp`, `tests/test_native_report_truth.cpp` | +| `report_open_position_at_end` | **adapter-policy** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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:2325-2327`) | 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:1583`): 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 runs the predicate leaves out keep every site on the source evaluator: a stream, the chart-timeframe magnifier, an aggregated chart, the KI-55 range-start cut, the historical lookahead projection, an auxiliary feed, a `lookahead_on` or Heikin-Ashi or lower-timeframe site, a forex/cfd intraday daily request (the OTC pins), a registry whose `sec_id`s are not dense from 0, and a pairing the kernel refuses. 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` | @@ -713,16 +714,16 @@ The table records, at the tree the doc wave read, what each block described and | kernel site | the mechanism | (a) generic because | (b) a knob would be | pinned by | |---|---|---|---|---| -| `utc_floor_day_ms` `src/timeframe.cpp:153` (the `TradingView` comment `sha256:f33cac7746bc3d4f59551120fde2a9eec65bab0d4d07a8ff410203711bac044e` :147) | daily boundaries are anchored at symbol-local midnight | the anchor is `spec.timezone`'s data, not a platform's; another venue is driven by naming its zone | a "which midnight" field over a zone the spec already names | `tests/test_native_calendar.cpp`, `tests/test_session_day_anchors.cpp` | -| `session_period_last_traded_close_ms` `src/timeframe.cpp:818` (the `TradingView` comment `sha256:2ed3a35ac4dc73d20a6b9757afb60994ec1cb149b934b75a0de3050c6e151e6c` :809) | a D/W/M period is finished at the last traded close of its last session | every venue calendar must answer "is this period over"; the answer is computed from `spec.session` and `spec.timezone` | a per-run completion-rule switch over a choice the session template already makes | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_session_calendar_extra.cpp` | +| `utc_floor_day_ms` `src/timeframe.cpp:153` (the `TradingView` comment `sha256:1868eeadaab06b664c5ba966a86e70d6de2ad74d3645afe1846ed736d0c2afce` :146-149) | daily boundaries are anchored at symbol-local midnight | the anchor is `spec.timezone`'s data, not a platform's; another venue is driven by naming its zone | a "which midnight" field over a zone the spec already names | `tests/test_native_calendar.cpp`, `tests/test_session_day_anchors.cpp` | +| `session_period_last_traded_close_ms` `src/timeframe.cpp:818` (the `TradingView` comment `sha256:a650d166e35a1e22188ff5c3ee50c5534519730913a3fe713cf0b93b0f66d4ca` :808-817) | a D/W/M period is finished at the last traded close of its last session | every venue calendar must answer "is this period over"; the answer is computed from `spec.session` and `spec.timezone` | a per-run completion-rule switch over a choice the session template already makes | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_session_calendar_extra.cpp` | | the session-day split `crosses_boundary` `src/timeframe.cpp:906-912` | calendar periods split on the session-day clock, not the civil one | continuous-session instruments need a session-day clock whatever platform asks; the class comes from `spec.type` | hash-visible surface for what `spec.type` decides | `tests/test_session_day_anchors.cpp` | | the aggregated bar's label `bar_label_ms` `src/timeframe.cpp:1068-1075` | an aggregated bar is dated by the period it opens | a label rule is needed to key any bucket; this one is the period key | a label-policy field for a key the aggregator owns | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_calendar_wm_open_utc_fastpath.cpp` | | real-end and chart-close completion (`Real-end completion` `src/timeframe.cpp:1195-1216`, `Chart-close completion` `:1240-1259`) | a thin or session-clipped intraday bucket completes on the bar whose end reaches the bucket end, or on the session's last bar | both are "the period is over" tests over feed data; a gap-free 24x7 feed is bit-identical either way, which the comment states and the tests pin | a completion-mode field over a bucket the calendar already closes | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_native_htf_subscriptions.cpp` | -| the native period partition installed by `set_native_periods` `src/timeframe.cpp:1621` (the holiday-pause comment `sha256:61ecbf6c16c7aafec3372da2ab58e0ea27590d406bd11914573ff97f06b8a0a0` :1409) | a session that pauses and reopens the same day is one session day | the rule reads the feed's own stamps; the holiday is data | a holiday table in the kernel | `tests/test_calendar_aggregation_wm.cpp` | +| the native period partition installed by `set_native_periods` `src/timeframe.cpp:1621` (the holiday-pause `TradingView` comment `sha256:7bd87c1e6dfe78729221d412952249a8cb427934ef498d3a4deb6812f7af05d7` :1405-1410) | a session that pauses and reopens the same day is one session day | the rule reads the feed's own stamps; the holiday is data | a holiday table in the kernel | `tests/test_calendar_aggregation_wm.cpp` | | `bar_path_uses_high_first` `src/engine_path_resolve.cpp:28`, `compute_ohlc_path_legs` `src/magnifier.cpp:97`, `entry_stop_first_touch` `src/engine_internal.hpp:101-109` | with only OHLC the intrabar walk order is unknowable, so the open-proximity heuristic picks the first leg | **and the knob exists**: `NativeRunSpec::path_order` (`NativePathOrder` `native_run_spec.hpp:335`) states `HighFirst` / `LowFirst` explicitly | already spelled, and `Auto` is the default that moves no identity | `tests/test_native_run_spec.cpp`, `tests/test_native_price_grid.cpp` | -| `round_to_mintick` `include/pineforge/engine.hpp:753` (the `TradingView` comment `sha256:2186a5ad94d0636ef1bc514a71a3cca607963db91419aaad5188cd7cb58d0ea7` :737) | nearest-tick rounding, ties away from zero | a tick ladder is the instrument's, declared as `price_tick`; the generic surface over it is `NativePriceGrid` + `NativeGridRounding` | TradingView's **per-order-kind** quantization is exactly the knob that would not be generic — it is the measured content of the `price_grid` native-only ruling above | `tests/test_native_price_grid.cpp` | -| `ta::ATR` `include/pineforge/ta.hpp:321` (the `TradingView` comment `sha256:96b8101eff840f3b73bc180f74e43b005f2a1ff61aafb8e93cef3059bc4a9336` :334) | average true range over Wilder's `RMA` | Wilder's ATR is the textbook definition; the comment records which convention and the tape that pinned the recompute behaviour | a seeding/averaging field on an indicator whose definition is the convention | `tests/test_ta_rma_warmup.cpp`, `tests/test_recompute.cpp` | -| `ta::StdDev` `include/pineforge/ta.hpp:430` (the `TradingView` comment `sha256:0a5444777042588c66c618c864464ef3a203839919b1816038767daed211a797` :420) | the population (biased) standard deviation | biased vs sample is a textbook choice, and the kernel states which | a per-instance switch is the *right* answer where two conventions genuinely compete, which is why `ta::EmaSeeding` exists and this one does not need it | `tests/test_ta_indicators_extras.cpp` | +| `round_to_mintick` `include/pineforge/engine.hpp:753` (the `TradingView` comment `sha256:ba90d695d99260d555d1f44bd05110f4977eeb1e54323e6362ea6bd451572399` :737-750) | nearest-tick rounding, ties away from zero | a tick ladder is the instrument's, declared as `price_tick`; the generic surface over it is `NativePriceGrid` + `NativeGridRounding` | TradingView's **per-order-kind** quantization is exactly the knob that would not be generic — it is the measured content of the `price_grid` native-only ruling above | `tests/test_native_price_grid.cpp` | +| `ta::ATR` `include/pineforge/ta.hpp:321` (the `TradingView` comment `sha256:4f6f6939aec2773684b114a13dd002eb58990d6d96c3056ad87361753faf7a0a` :333-340) | average true range over Wilder's `RMA` | Wilder's ATR is the textbook definition; the comment records which convention and the tape that pinned the recompute behaviour | a seeding/averaging field on an indicator whose definition is the convention | `tests/test_ta_rma_warmup.cpp`, `tests/test_recompute.cpp` | +| `ta::StdDev` `include/pineforge/ta.hpp:430` (the `TradingView` comment `sha256:7cc1bbcd32dfe673285d51a8ca7080890ac551858099b523480d998f46408666` :420-429) | the population (biased) standard deviation | biased vs sample is a textbook choice, and the kernel states which | a per-instance switch is the *right* answer where two conventions genuinely compete, which is why `ta::EmaSeeding` exists and this one does not need it | `tests/test_ta_indicators_extras.cpp` | | `float_band_eq` `include/pineforge/ta_compare_band.hpp:38` | an absolute 1e-10 float comparison for indicator equality | an epsilon band is needed by any float comparison; the width is the calibration | a band-width field on a comparison the indicators share | `tests/test_dmi_parity.cpp`, `tests/test_ta_osc_edge.cpp` | | the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8606-8609` (lookahead) and `clear_if_gapped` `:8600-8603` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | | `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2228`, `::gaps` `include/pineforge/native_c_api.h:2232` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:795`, `pf_native_gaps_e` `:806`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | diff --git a/docs/ci.md b/docs/ci.md index 2a33c2e4..24fe08d2 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -432,9 +432,15 @@ line must resolve, qualified symbols must land inside their declared scope, and continuations may inherit a full anchor across a physical line break only within the same paragraph or table row. Source-labelled fenced examples are scanned; unlabelled and output-labelled fences are treated as pasted output. A cited C/C++ -window made entirely of comments fails unless it carries an explicit content hash. -Ruling-table citations require a symbol, fragment or content hash, and a -symbol-less range elsewhere requires a backticked `sha256:<64 hex digits>` hash. +window made entirely of comments fails. A content pin — a backticked +`sha256:<64 hex digits>` hash right before a range — accompanies a claim and never +replaces one: the symbol before it is still checked, a pinned comment window still +fails unless the sentence says it cites a comment, a single line is cited by its +symbol and never pinned, and a pin over blank lines or two pins before one anchor +fail. Ruling-table citations require a symbol or a code fragment, pinned or not, +and a symbol-less range elsewhere requires a pin. In a citation list, a citation +that prose words introduce after a comma (`x.cpp:1, FX curve x.cpp:9`) claims +nothing of the list's earlier symbol and is `NOCLAIM` until it carries its own. `--fix` changes line numbers only when the symbol is uniquely locatable; `--list` shows every verdict. diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index dc3396df..74756495 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -48,10 +48,10 @@ 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:2737-2748; `entry()` pine_adapter.cpp:5289, `order()` pine_adapter.cpp:10480) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5685-5732; `submit` native_host.hpp:1201 | 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:8343 → `Flatten{}` pine_adapter.cpp:8372-8373; `Reduce{ExplicitUnits}` pine_adapter.cpp:7490-7499; `ReverseTo` pine_adapter.cpp:6324-6325) | **yes** — `OrderIntent` native_order.hpp:229, reduce sizes native_order.hpp:229-236, `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 | — | +| OL2 | Intents: flatten, reduce by units, reduce owner-opened units, transact, reverse-to (`close_all()` pine_adapter.cpp:8343 → `Flatten{}` pine_adapter.cpp:8372-8373; `Reduce{ExplicitUnits}` pine_adapter.cpp:7490-7499; `ReverseTo` pine_adapter.cpp:6324-6325) | **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:2707-3914 → `host.replace` pine_adapter.cpp:2723-2803) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4722-4768, `cancel_all` pine_adapter.cpp:18032-18047; `live_by_source_key_` pine_adapter.hpp:1995; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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:4189; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7175-7526) | **yes** — `Capacity` / `PointBudget` native_order.hpp:379-383, `Remaining` native_order.hpp:557-579, allowances native_order.hpp:557-570 | K | — | O:A16 S:O4 | — | +| OL5 | Per-point fill capacity, partial fills, residual working quantity (Pine uses only `ImmediateRemaining` pine_adapter.cpp:4189; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7175-7526) | **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:5513-5548, `cohort_for` pine_adapter.cpp:2501; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7473-7490) | **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:1270, `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:2746-2752) | **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:1092-1093 | K | — | F:A9 | (single-source row; O:F2 cites the same call) | @@ -60,7 +60,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8645-8648; `materialize_relative_exits` pine_adapter.cpp:17322-17363, pine_adapter.hpp:1820-1832) | **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:5611-5645. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19083-19112) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10880-10904), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15631, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15865) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:662-664 | 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:42-88, enforced pine_adapter.cpp:1036-1043; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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 | +| OL14 | *(quirk)* Exit activation / suspension / revival barriers, historical birth reach (`select_exit_activation` exit_activation.cpp:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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 @@ -77,7 +77,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5902-5906, pine_adapter.cpp:5902) | **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:10915-12107) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked native_execution_consumer.cpp:4922-4940, guarded native_execution_consumer.cpp:4922-4956 | 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. | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:10915-12107) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4922-4956 | 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:2595-2650) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4963-4965 **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: rounded equity `source_money_round` pine_adapter.cpp:2547, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2556-2558) | **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:2608, `typed_entry_units` pine_adapter.cpp:2626; `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:2587; `percent_commission_live_equity` pine_adapter.cpp:4328-4368) | **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. | @@ -88,7 +88,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | SZ10 | *(quirk)* Ten-significant-digit money rule (`source_money_round` pine_adapter.cpp:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4443-4458) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | | 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:6706), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7913), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12735), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13331, pine_adapter.cpp:13384) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15679) | 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:2595), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2608, `typed_entry_units` pine_adapter.cpp:2626), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18678) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7175-7526; ledger `close_batch_bar_` pine_adapter.hpp:2038-2049; 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 pine_adapter.cpp:1100-1212) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | +| 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | ### 1.4 Margin, admission, liquidation, risk @@ -115,7 +115,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:881, dispatched native_execution_consumer.cpp:7204-7225 (call site `sha256:fc29ccd415732193f24f2370c6aca8d6f8910bca5b95315e63e88f2befe8200b` native_execution_consumer.cpp:7215) | K | — | F:E1 O:F1 | — | +| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:881, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2318-2319) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7524-7528 | K | — | F:E2 O:F6 S:C1 | — | | CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16118) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7066-7070; `execute_current` legal native_host.hpp:1092 | 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:7009 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7060 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1061 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. | @@ -123,7 +123,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16261, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6920, native_execution_consumer.cpp:6920-6950; commands are legal there `commands_allowed` native_execution_consumer.cpp:1398-1406; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5611-5645, `remaining_path_delivery` native_order.cpp:2069-2077. 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:866 fires per accepted realtime print native_execution_consumer.cpp:7148-7152; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | | 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:7518-7635 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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:2448-2504) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery native_execution_consumer.cpp:7462-7653; `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. | +| CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2448-2504) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:234-320) 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:5757), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1659-1666 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | @@ -132,7 +132,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1483-1486, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1391-1547; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1484 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1124 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1107 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | -| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI c_abi.cpp:861) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1429-1445 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, sole caller pine_strategy_host.cpp:1516; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1447-1452 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:487 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8276 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (`never from the chart's intraday prints` `sha256:f45ad2531e13191bb0e007133c63d1c35ba6ecef4eb5680d78d343d79e35a0a9` engine_aux_security.cpp:49). | +| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI `strategy_set_native_security_feed` c_abi.cpp:839) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1429-1445 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1516; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1447-1452 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:487 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8276 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:46-56). | | HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:637) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:916 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | | HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:21-220, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:241-413; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:514) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:255-336) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | @@ -142,14 +142,14 @@ 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:5685-5732, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6268-6276; `Limit::fill_through` native_order.hpp:239-244 enforced native_execution_consumer.cpp:5069-5082, re-checked after host terms native_execution_consumer.cpp:5069-5089 | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamp native_execution_consumer.cpp:4598 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11946-11955) is A | +| FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:5685-5732, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6268-6276; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11946-11955) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:472-479) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | | FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11962-11971, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4060; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6350-6351; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:1190 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:8706). **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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9032-9037, `exit_limit_trigger` pine_adapter.cpp:9566-9587), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling pine_adapter.cpp:11445-11551, margin-call fill pricing pine_adapter.cpp:14011-14020 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11671) | 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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9032-9037, `exit_limit_trigger` pine_adapter.cpp:9566-9587), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11671) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -160,7 +160,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | RP3 | Equity curve | **no** — `update_equity_extremes` engine.hpp:1362 / `record_equity_point` engine.hpp:1380 are protected and were called only by the adapter **Closed:** `NativeReportPolicy::KernelRecorded` native_run_spec.hpp:63 has the consumer fold the extremes and append one point per script calculation, labelled with the script interval's open; `KernelRecordedAtHostMarks` records the same series at the host's own marks (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I4 S:X2 | S's "partial" folds RP2 in | | RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:219; `fill_metrics_section` engine_report.cpp:119-139 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | | RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1683) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1408) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | -| RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected `sha256:68946f371856f0a13ae5c347b61ab3112f52edcaa3eafbf411177b565ee88260` engine.hpp:1827 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | +| RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | | RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9280, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | | RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1358, native_execution_consumer.cpp:10682; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | | RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:538; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1925 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.8 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | @@ -174,7 +174,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OT2 | Symbol info, sessions, calendars, timezones (`timezone` pine_adapter.cpp:2254-2255, `chart_timezone` pine_adapter.cpp:2295-2297) | **yes** — spec fields `ticker` native_run_spec.hpp:567-577; `parse_session` native_calendar.hpp:173-419; 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:2158, `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:896, FX curve c_abi.cpp:896, probe c_abi.cpp:896, stream `strategy_stream_begin` c_abi.cpp:624 **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:548 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:907 among them (F §2.iii) | +| OT5 | C-level run configuration, stream, FX curve, contract probe | **yes** — `strategy_configure_native_v1` c_abi.cpp:896, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:940, probe `strategy_execution_contract` c_abi.cpp:873, stream `strategy_stream_begin` c_abi.cpp:624 **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:548 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:907 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 58 runtime exports (and eight generated per-strategy exports) (`tests/test_native_c_api.c`, `tests/test_native_c_api_frozen_header.cpp`). | K | L13 | F:J6 O:J5 | R5-7: post-v1. S covers it in its §2.3 without a row. | | OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1454 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7268), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1764 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:857; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | | OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:288-290) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | @@ -185,7 +185,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa ### 2.i Kernel-only build target (L1) -- **Today:** one static library always compiles the source layer (list `sha256:c9ef24f6cbe47b44cd48c83ca7568fde3dad8d3bad9c8623c0d53d2e503616ee` CMakeLists.txt:81-94, `add_library(pineforge STATIC` CMakeLists.txt:153, splice CMakeLists.txt:153) and installs every header, `include/pineforge/source/**` and `include/pineforge/compat/pine/**` included (CMakeLists.txt:334-337). `src/compat/pine/market_admission.cpp` sits in the *main* list (CMakeLists.txt:111) for historical reasons (added by `3b13183`, before the source-layer list existed — F). +- **Before L1:** one static library always compiled the source layer and installed every header, `include/pineforge/source/**` and `include/pineforge/compat/pine/**` included, and `src/compat/pine/market_admission.cpp` sat in the *main* list for historical reasons (added by `3b13183`, before the source-layer list existed — F). **Since L1:** the source layer is its own list (`PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91, `market_admission.cpp` at :94), appended to `pineforge` only when `PINEFORGE_BUILD_SOURCE_LAYER` is ON (`add_library(pineforge STATIC` CMakeLists.txt:164-167), beside the kernel-only archive (`pineforge_kernel` CMakeLists.txt:153), whose header install excludes `source` and `compat` (`PATTERN` CMakeLists.txt:416-421). - **Link feasibility:** no kernel TU and no public kernel header includes `source/` or `compat/pine/` (F, O). The only code coupling is the forward-declared opaque `source::StrategyOverrides*` (§2.ii a), which needs no definition. - **Risk:** the archive member order of `libpineforge.a` changes. Harmless for static linking, but check `PINEFORGE_REQUIRE_ABI_RECEIPTS` (CMakeLists.txt:65); what the receipts pin is unverified (F). @@ -193,9 +193,9 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---| | Option | `PINEFORGE_BUILD_SOURCE_LAYER`, default ON | same | `PINEFORGE_BUILD_KERNEL_ONLY` | `PINEFORGE_BUILD_SOURCE_LAYER` (default ON; OFF = kernel only): 2 of 3, and it names the optional part | | Targets | object libs `pineforge_kernel_objs` + `pineforge_source_objs`; `pineforge_kernel` STATIC, alias `PineForge::kernel`; `pineforge` STATIC = both | `pineforge_kernel` OBJECT; `pineforge` STATIC = kernel objects + conditional source list | `pineforge_kernel` + `pineforge` as the compatibility aggregate | F's shape. `pineforge` keeps its name, alias and install rules, so codegen-built strategies and `find_package(PineForge)` consumers do not move | -| What moves | `compat/pine/market_admission.cpp` → source list | same + delete dead `admission_retention` | same ("a concrete boundary bug") | **Done.** The unit is in `PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91 with the other five `compat/pine/` units, and `admission_retention` is deleted — the name is nowhere in `src/` or `include/`. Its callers were the adapter only (pine_adapter.cpp:2664, `sha256:ddfd09da8827c35417d2ff71e63c7fb94e625439fb0ef2b42189833ea0dc3892` pine_adapter.cpp:14349-14351, `sha256:560e106c433fafa69344365c8f83e8e9b5a33ffcf4544447fbddd2cd47b4a60c` pine_adapter.cpp:14569-14570, `sha256:05c2a313dfe73ccea95254a9a4ea41f08ec5b675eded99c5cdcf40887d90e176` pine_adapter.cpp:15128-15129) and tests, so the move was link-neutral | +| What moves | `compat/pine/market_admission.cpp` → source list | same + delete dead `admission_retention` | same ("a concrete boundary bug") | **Done.** The unit is in `PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91 with the other five `compat/pine/` units, and `admission_retention` is deleted — the name is nowhere in `src/` or `include/`. Its callers were the adapter only and tests (both retired with the legacy loop in R4 slice C, `73817c1d`), so the move was link-neutral | | Installed headers | OFF skips `include/pineforge/source`, `include/pineforge/compat` | install conditional on the option | installed closure with source + compat headers physically removed | Conditional install; S's closure test is the acceptance | -| CI / gate | link the two native examples + the 20 source-free native tests against `pineforge_kernel` only; `nm` assertion in the independence checker (it already forbids `pineforge::source` / `compat::pine` symbols check_native_include_independence.py:72-78) | new CI lane: configure with the source layer OFF, build, run the checker against that prefix | clean kernel-only compile; `nm` shows no source / compat symbol; keep the textual ban (`sha256:f1d18e8965bc7ce517c95a9dbb79bc94801cbd96a74999858b6d6118fc5d004d` test_native_source_guard.py:18-60) as a fast preflight | All three, one CI lane | +| CI / gate | link the two native examples + the 20 source-free native tests against `pineforge_kernel` only; `nm` assertion in the independence checker (it already forbids `pineforge::source` / `compat::pine` symbols check_native_include_independence.py:72-78) | new CI lane: configure with the source layer OFF, build, run the checker against that prefix | clean kernel-only compile; `nm` shows no source / compat symbol; keep the textual ban (`FORBIDDEN_INCLUDE` test_native_source_guard.py:43) as a fast preflight | All three, one CI lane | | Size | M | M | L | M (R5-11) | ### 2.ii Coupling extraction (L11 neutral, L12 hash-visible) — R5-8 @@ -206,12 +206,12 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:29 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:356-359 (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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1836-1839; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1869, `:8767`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2308-2309. 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:260-261. **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:741) | 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:454-460) | 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 native_execution_consumer.cpp:6421 | NEUTRAL | Reuse as the reference arithmetic of the price-grid and sizing lanes, delete the rest | O (single-source) | +| e | Dead helpers in the public kernel header | **Done.** All four are gone from `engine.hpp`: `round_to_mintick_directional` and `apply_slippage` were the pre-R5 spelling of the directional snap and the slippage step and the kernel now fills on `native_matching::grid_round_directional` / `native_matching::apply_slippage` src/native_matching.hpp:259 (the engine.hpp comment that named them went with R5 lane E10's residue sweep); `apply_limit_fill` and `reserve_percent_commission` are deleted outright, and only the adapter still spells the first two names, in its own comments. `round_to_mintick` engine.hpp:753 and `bar_fill_price` engine.hpp:771 stay, live through the call `bar_fill_price` native_execution_consumer.cpp:6421 | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19083-19112 | 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:522, 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 engine_state_hash.cpp:66 and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:548) 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:308-585) 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 | +| 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:522, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:548) 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:308-585) 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:603. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1273 | 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) | | m | `request.security` machinery | `register_security_eval` engine_security.cpp:27, `set_native_security_feed` engine_aux_security.cpp:76-127, `SecurityEvalState` engine.hpp:1156-1360 carry Pine semantics (lookahead, gaps, Heikin-Ashi, KI-55 range start) and are driven only by the source layer | HASH-VISIBLE / measured path | F planned to move both TUs and the state into the source host after native HTF. **R5-6 narrows this:** L6 reuses the feed store and routing, so those stay kernel; only the Pine-only evaluator semantics remain candidates. Do last. **Status (lane L12b): done** — the evaluator semantics are `source::PineSecurityEvalState` + `src/source/pine_security_eval.cpp`; the kernel keeps the registry, aggregator, feed store and one generic step | F S | @@ -262,7 +262,7 @@ PF_API int pf_native_events_v1(pf_strategy_t, uint64_t after_ordinal, pf_native_ | Point | Decision | Src | |---|---|---| | Implementation | One new kernel TU (F `src/native_c_host.cpp`, O `src/c_native_host.cpp`, S `native_c_api.cpp`): `class CCallbackHost final : public NativeStrategyHost` forwarding each virtual to the table. Streaming reuses the existing `strategy_stream_*` symbols unchanged (they take any `BacktestEngine*`) | F O S | -| Errors | A non-zero callback return maps to `NativeFailureCode::CallbackException`; C callbacks must not unwind. The C++ path already latches exceptions `sha256:27ab3bf715fdc1eb1b9183cd41b2b8e1cf0d7cb38c1a313a371b285991bcc0b7` native_execution_consumer.cpp:2961-2971 | F O | +| Errors | A non-zero callback return maps to `NativeFailureCode::CallbackException`; C callbacks must not unwind. The C++ path already latches exceptions as `CallbackException` native_execution_consumer.cpp:7068 | F O | | Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1398-1405): inside a callback, or between realtime inputs | F | | Hardening | Tagged, size-prefixed PODs; `struct_size` + version; unknown-tag refusal; never cast the C request to a C++ variant; a C fixture compiles against the frozen previous header. S puts the API in a separate header `include/pineforge/native_c_api.h` | S | | SOP | Every symbol goes into `src/c_abi.cpp`, `pineforge.h`, `EXPECTED_RUNTIME` + both counts (check_c_abi_runtime.py:19-79), the README table and the Python harnesses, or all CI matrix jobs fail at the "C ABI runtime source check" step (`CLAUDE.md`) | F O S | @@ -282,11 +282,11 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the | Lookahead | none, by construction | `lookahead = false` default, available | `lookahead_on = false`, `gaps_on = true` | open → §4 U1 | 1. **Spec:** `std::vector subscriptions` (Appendix A.5), hashed only when non-empty (digest like `native_intrabar_path_digest`). -2. **Wiring**, all inside the native consumer at Ready → begin and only when `subscriptions` is non-empty (zero work for the adapter): register one evaluator state per subscription through the existing `register_security_eval` (engine.hpp:1295-1297, *fresh*); install `authoritative_bars` into the existing store (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129); call `prepare_native_security_feeds` (engine.hpp:1639, defined engine_aux_security.cpp:128, *fresh*) — the "made native-callable" step: reachable from the native consumer, still refused from host code in-run; pump `feed_security_eval_state` (engine.hpp:1312-1314, *fresh*) from the accepted-input path; deliver each completed bucket through `on_native_timeframe_bar`. -3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1304-1388 and its call order. The per-run evaluator init is today a *source-layer* member (`PineStrategyHost::init_security_eval_states_for_run` pine_scheduler.cpp:9-39, declared pine_strategy_host.hpp:694, *fresh*), so L6 adds a kernel-side twin instead of moving it. +2. **Wiring**, all inside the native consumer at Ready → begin and only when `subscriptions` is non-empty (zero work for the adapter): register one evaluator state per subscription through the existing `register_security_eval` (engine.hpp:1295-1297, *fresh*); install `authoritative_bars` into the existing store (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129); call `prepare_native_security_feeds` (engine.hpp:1639, defined `BacktestEngine::prepare_native_security_feeds` engine_aux_security.cpp:128, *fresh*) — the "made native-callable" step: reachable from the native consumer, still refused from host code in-run; pump `feed_security_eval_state` (engine.hpp:1312-1314, *fresh*) from the accepted-input path; deliver each completed bucket through `on_native_timeframe_bar`. +3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1304-1388 and its call order. The per-run evaluator init is today a *source-layer* member (`PineStrategyHost::init_security_eval_states_for_run` pine_scheduler.cpp:9-39, declared `init_security_eval_states_for_run` pine_strategy_host.hpp:694, *fresh*), so L6 adds a kernel-side twin instead of moving it. 4. **Delivery rule (F):** a subscription bar is delivered on the input bar that completes it — its last contributing input bar, or, for a `LazyComplete` bucket, the next period's first input bar — immediately before that input's script calculation, never earlier — and never before an earlier script interval's lazily sealed calculation has run: the pump is ordered against the script interval, `seal(k) → deliver(i+1) → calc(i+1)` (L6d). 5. **Interim, documented in L0:** `on_native_input` + `TimeframeAggregator` (HT6). Doc notes (O): `set_native_security_feed` is public but inert without evaluators and throws in-run (`guard_native_mutation` engine_aux_security.cpp:78, which latches `UnsupportedSource` native_execution_consumer.cpp:1447); `set_aux_security_feed` returns false in the kernel engine_consumer.cpp:158-160. -6. **Entanglement to put in the lane brief** (*fresh* read of `sha256:ae40c5c462bd2f487afdc0ebbcdc9eef4a8839dd66cdc4f4f737af14e3484a60` engine_aux_security.cpp:48-87): the feed machinery is TV-calibrated. "W" / "M" buckets are built from the installed native daily bars, and the native stamps define the period partition (a holiday session folds into the next trade date's bar). A bare host that supplies `authoritative_bars` inherits those rules. Document them as the contract or gate them behind a partition policy — decide in the lane, not silently (§4 E8). +6. **Entanglement to put in the lane brief** (*fresh* read of `set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:46-78): the feed machinery is TV-calibrated. "W" / "M" buckets are built from the installed native daily bars, and the native stamps define the period partition (a holiday session folds into the next trade date's bar). A bare host that supplies `authoritative_bars` inherits those rules. Document them as the contract or gate them behind a partition policy — decide in the lane, not silently (§4 E8). 7. **HT3 / HT4:** lower-TF arrays come from the L5 sub-bar hook in v1. An auxiliary feed is a subscription whose bars come from the host; the Pine chart-slice mapping is not exposed. Whether v1 accepts a feed *finer* than the input is left to the lane brief (S wants it, F calls it a TV harness construct). **Status (audit lane N7): HT4 done for a bare host; the adapter's auxiliary path retained, measured.** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed{tf, bars}`, strictly finer than `input_tf`, opt-in and hashed only when present) plus `NativeTimeframeSubscription::source = NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars through the existing evaluator step (`register_security_eval` against the feed's timeframe, `feed_security_eval_state` per feed bar — called, not edited); routing is by time alone (a feed bar rides on the first accepted input whose period it opened before), with `declare_auxiliary_feed` at begin, `append_auxiliary_bars` on a realtime stream and a C spelling (`PF_NATIVE_SPEC_EXT_AUXILIARY_FEED` tail, `strategy_native_append_auxiliary_bars_v1`). No epoch moved: `native_run_spec_v3` and `engine_script_run_v18` were open then (no release tag contained them, no frozen provider fixture of either existed), exactly as L6c's `gaps` and begin-time hook were added. `BacktestEngine::set_aux_security_feed` stays the source host's door and still answers a silent false on a bare host (the pre-begin setter pins, `tests/test_native_example_batch.cpp` among them, hold it to that). **Retained, why:** the adapter does not re-lower its auxiliary sites, on three measurements — 0 of 312 corpus probes install an auxiliary feed, so corpus byte-identity cannot witness the move; TradingView's chart slice leaves pre-range feed coverage inert where the kernel folds it by time (two hourly buckets against three over one feed, `tests/test_native_auxiliary_feed_twin.cpp` row B); and the adapter evaluates a chart bar's slice at its calculation, after that bar's matching pass, where the kernel delivers before it (position 1 against 0 at the same bucket, row C), which no predicate over an opaque generated `evaluate_security` can prove neutral. The congruent shape is the same series on both (row A) and can move once a population exercises it. Full contract: `docs/pages/native-engine.md`, "The auxiliary finer feed". @@ -294,7 +294,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). - Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2933-3000) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). -- Update the three places that pin the paths: `sha256:60a689e9bbb16d77370ee18557929c420ee846d162b7d2bcd5472ffd74409e69` runner/CMakeLists.txt:37-45, `sha256:e7b0011fb5e730a159692b9f8ab97b2d43bd386e7c4802f13a4ae4c1db083c80` check_native_include_independence.py:36-39, the tests at `sha256:2c88205b43a02dafe8915e74417743b3b3faa0c65595686ccd305386505bdf55` runner/CMakeLists.txt:98-128. +- Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:45, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). - Ship the native API reference and the standard dual-run harness of §3.1 b (S lane J). @@ -313,15 +313,15 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | # | ADR claim | Correct statement | Evidence | Src | |---|---|---|---|---| -| 1 | Orders are "Market / Limit / Stop" (ADR L72-75) | Also StopLimit and Trail; five intents, five owner kinds, groups, `PointBudget` | `sha256:41883fa1f765ad114524fb6d79d25a169506192bdbe128b3b75df787b54fe126` native_order.hpp:88-96, native_order.hpp:75, `sha256:be94d3674992011da7ec65b5b2238894585b1dda7177377b88e8b37ae76a7990` native_order.hpp:104-119, `sha256:b169add5301f8959eba8e78583fa058a076f291ba31e430120c2f5d315aed270` native_order.hpp:121-128, `sha256:5de5d22b92b971a4900b7eb0bfdd08dda1217fa211033cb39ab10db96d4f2608` native_order.hpp:99-102 | F | -| 2 | "The kernel matches and fills … with sizing, slippage, fees, margin and settlement" (ADR L74-75) | "with slippage, fees, opening admission and settlement; sizing is resolved by the host". `NativeRunSpec` owns fees, FX, close timing, direction / caps and intrabar paths; it does not own percent / cash sizing, maintenance margin, calc-on-fill, every-tick calculation or bare-host security registration | `sha256:ea2e00c9343cec29e05d662b8996caf53774f50020e5a47c34d882b87197661c` native_execution_consumer.cpp:3040-3042, `sha256:4c401fc6bd6ae89db330fea9f609b75539ff3db1f8060b0fdb0541cbbaf478ee` native_run_spec.hpp:19-177, `sha256:f4041a4df37f679d9055e856e1cc247a9ce5e1e80f01a49f9cdd822631f352c6` native_run_spec.hpp:174-175 | O S | -| 3 | "Read results via `metrics.hpp`" (ADR L76-80) | Trade stats yes. Equity curve and equity metrics are empty for a bare host, silently, and an open final position is missing from the report | engine_metrics.cpp:234, `sha256:c253bacbca29683a3ea362558fdb97207565ce3c761f8b122d52a7467940715e` engine_report.cpp:119-139, `sha256:41455d4f7c9276290acb46aa9bb32a002d682777d53d351ba5a19a61e23f1bcb` engine.hpp:1832-1858, `sha256:6f6a1713275dd47d861a48adb4f6e92c5f67d01de7cfaeb55877c8f874caf8e6` pine_strategy_host.cpp:1570-1571 | F O | +| 1 | Orders are "Market / Limit / Stop" (ADR L72-75) | Also StopLimit and Trail; five intents, five owner kinds, groups, `PointBudget` | `Trigger` native_order.hpp:296, `OrderIntent` native_order.hpp:229, `Owner` native_order.hpp:464, `Group` native_order.hpp:473, `PointBudget` native_order.hpp:379 | F | +| 2 | "The kernel matches and fills … with sizing, slippage, fees, margin and settlement" (ADR L74-75) | "with slippage, fees, opening admission and settlement; sizing is resolved by the host". `NativeRunSpec` owns fees, FX, close timing, direction / caps and intrabar paths; it does not own percent / cash sizing, maintenance margin, calc-on-fill, every-tick calculation or bare-host security registration | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | O S | +| 3 | "Read results via `metrics.hpp`" (ADR L76-80) | Trade stats yes. Equity curve and equity metrics are empty for a bare host, silently, and an open final position is missing from the report | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | F O | | 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:1730, `guard_native_mutation` engine_aux_security.cpp:78 | 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). No kernel TU includes a source header | `sha256:e3312d2dd7ad7067ad41b26dfb3b502293dcdc5b98ed91563bd89f9482bb2b64` engine_aux_security.cpp:5-12 | 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 | `sha256:01bc6bc5594caca01f615764f818ab7a60f20d5f5caea0fe387adc0ca2b24d9b` engine.hpp:410-412, native_host.hpp:387, `sha256:4ad9f502746afb2c5c0468ececcc363ccd48bbb8f228fff3b69c5a7cb61588c7` check_native_include_independence.py:42-46 | F O | +| 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). No kernel TU includes a source header | `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13 | 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:739 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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:548; the ledger `close_logical_units_` pine_adapter.hpp:2023-2034; `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) | 58 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 58 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:13933f70987f6bfe37d3015ae065a8d5ee5937c37431d48e2ecd1f17a4c29958` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:862, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:590-709, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:515-567, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:494-511, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:802-833, c_abi.cpp:929 | 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" | CMakeLists.txt:37, `sha256:e7b0011fb5e730a159692b9f8ab97b2d43bd386e7c4802f13a4ae4c1db083c80` check_native_include_independence.py:36-39, `sha256:60a689e9bbb16d77370ee18557929c420ee846d162b7d2bcd5472ffd74409e69` runner/CMakeLists.txt:37-45, `sha256:10d16f3153bc79ab29cfb12b56858a256577d04465dab8a3c84ddf28fa701cf5` CMakeLists.txt:368-369 | F O S | +| 9 | "No examples. `PINEFORGE_BUILD_EXAMPLES` is 'none yet'" (ADR L139) | The option is inert, but two Pine-free `NativeStrategyHost` examples exist, are compiled by the independence checker and tested through the live runner, only under `PINEFORGE_BUILD_LIVE_RUNNER`. Wording: "no top-level examples target" | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | F O S | | 10 | Follow-ups (1)-(4) (ADR L168-170) | Add native reporting, sizing, margin, HTF — and, per R5, calc-timing, price grid and order ergonomics. Without them "usable without Pine" is a build-system claim, not a feature claim | §3.5 | F | | 11 | Build layout | One `pineforge` library always appends the source list; a kernel-only target does not exist yet; `compat/pine/market_admission.cpp` sits in the main list, a boundary bug. **Overtaken:** `PINEFORGE_KERNEL_SOURCES` CMakeLists.txt:113 and `add_library` CMakeLists.txt:153 build `PineForge::kernel`, and the compat unit is in `PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91 | `add_library` CMakeLists.txt:153 | S (F, O agree in §2.i) | | 12 | Host constructors | `NativeStrategyHost` is a zero-argument C++ host with the callbacks and the request API; `PineStrategyHost` is the class that takes `CapAttachment` — do not attribute it to the native constructor | `NativeStrategyHost` native_host.hpp:833, `PineStrategyHost` pine_strategy_host.hpp:242 | S (single-source) | @@ -345,7 +345,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:2204-2337) 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:1641-1645). 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 Cloud Run campaign sweep on the final tree (fixed population, 4190 probes, zero tolerance on the hard band — native-refactor-progress.md:41-47; a fresh exact-HEAD verdict is required, `sha256:0178f02a2a0cd9a592d5182a4b0c6823e9914fccdb8d3dc00efc4d0d9fe9fcc1` `sha256:6f849db2384258dbca1964fa55f7b5b2df9a39895ca1be225326cc14772e21a2` AGENTS.md:132-148). 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:2204-2337) 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:1641-1645). 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 Cloud Run campaign sweep on the final tree (fixed population, 4190 probes, zero tolerance on the hard band — native-refactor-progress.md:41-47; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:139-140). 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 drive the adapter and 14 the native host (O), 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`). | L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1304-1388 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | | L7 | `native_working_requests()`, `cancel_all()`, `cancel_where(label)`, `TriggerAnchor`, `TrailTicks`, zero offset, `ReplaceOptions`; toolkit `submit_bracket`, `OrderBook` → A.6 | new kinds unused by the adapter; anchor defaults to `Absolute`; the adapter's `tick * 0.5` sentinel keeps working | bracket + trailing probe through the builder: exact where TR5 / FP7 are inactive (on-grid levels) (F); the zero-offset trail test asserts the run no longer fails (O); parent-limit + stop sibling, parent rejection, replacement, parent-cycle revival (S) | | L8 | `NativePriceGrid { None, QuantizeFills, QuantizeFillsAndTriggers }`, `NativeGridRounding` → A.7 | `None` for the adapter, permanently — **native-only by ruling, audit lane P6: §3.6.2** | a host with `QuantizeFills` books on-grid prices; a second test pins that `QuantizeFillsAndTriggers` still differs from TV's half-tick threshold (FP5) (O) | -| L9 | `NativeRiskLimits`, `NativeRiskEvent`, `MatchRejectReason::RiskLimit` → A.8 | `risk` unset for the adapter; its ledger (`sha256:f6a0555a8f3f3be7cf2b65146c15b3821e35a184d84a877931bed353e7fc5f89` pine_adapter.hpp:1196-1221) untouched — **retained after measurement, audit lane N12; native-only by ruling, audit lane P6: §3.6.1** | risk probes: same halt bar (F); each limit + the day-boundary basis (O); breach, forced close, cancellation, next-day reset (S) | +| L9 | `NativeRiskLimits`, `NativeRiskEvent`, `MatchRejectReason::RiskLimit` → A.8 | `risk` unset for the adapter; its ledger (`SourceDayLedger` pine_adapter.hpp:1200) untouched — **retained after measurement, audit lane N12; native-only by ruling, audit lane P6: §3.6.1** | risk probes: same halt bar (F); each limit + the day-boundary basis (O); breach, forced close, cancellation, next-day reset (S) | | L10 | `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` (§2.v) | build-only | the examples run in ctest; the independence checker compiles the relocated examples | | L11 | — (renames / moves, §2.ii a-i) | rename / move only; hashed enumerator values pinned; sweep unchanged | — | | L12 | — (§2.ii j-m) | **not neutral**: coordinated sweep, waiver updates, hash-domain plan (the `"pineforge-broker-state/v17"` literal of that wave, since moved to `"pineforge-broker-state/v19"` engine_state_hash.cpp:33 and pinned to one occurrence by check_broker_state_hash_coverage.py:223) | trades identical, hashes re-baselined once | @@ -439,7 +439,7 @@ Flagged: | Tier | Lanes | Why | |---|---|---| -| Day-one blockers (O) | L1, L2, L3, L10 | Without L2 `fill_report` silently returns an empty curve and degenerate equity metrics (`sha256:0f098b88d9904356f3bd88587db6cd0db83c2d8aee3ccedc861164aa4e453f11` engine_metrics.cpp:3) and drops an open final position. Without L3 every non-unit order needs a hand-written `resolve_execution_terms`, and a mistake is a run-terminating `TermsUnresolved` (native_execution_consumer.cpp:4963-4965). Without L1 "kernel" is a claim, not an artifact. L10 is the minimum buildable proof. | +| Day-one blockers (O) | L1, L2, L3, L10 | Without L2 `fill_report` silently returns an empty curve and degenerate equity metrics and drops an open final position. Without L3 every non-unit order needs a hand-written `resolve_execution_terms`, and a mistake is a run-terminating `TermsUnresolved` (native_execution_consumer.cpp:4963-4965). Without L1 "kernel" is a claim, not an artifact. L10 is the minimum buildable proof. | | Required by the promise (R5-11) | L0, L4, L5, L6, L7, L8 | A kernel that cannot liquidate is not a broker model (O); bracket / trailing corner cases are hit immediately (O); calc-timing and HTF are Pine features the user wants natively (R5-5, R5-6); the price grid is generic and missing (R5-3). | | Wanted with v1, not blocking | L11 a, d | removes the last `source::` name from kernel signatures and the orphaned TV commentary (F) | | In scope, 1.x | L9 | R5-9; a toolkit stand-in is acceptable meanwhile (F) | @@ -511,7 +511,7 @@ adapter's own rule acts. **Per rule (STEP 1), assuming the values were known at begin time.** Adapter line numbers are this tree's (683a82f); kernel semantics are -`sha256:8c97d3e1d9d6f036a9be0b50a3a43188f1e2eee23da0384fbed242829108c3c3` native_run_spec.hpp:239-302 and `sha256:efad6b9c7bcd3f7d0e459d434ef088a61b18c546d53cfb107f481a227fbe532b` native_execution_consumer.cpp:3301-3490 +`NativeRiskLimits` native_run_spec.hpp:309 and `risk_evaluate` native_execution_consumer.cpp:3618 (evaluated at the script bar's open, its close calculation and after each applied drain; fills counted at the corresponding adapter checkpoints; the block refuses `would_open`). | Pine rule | adapter mechanism | kernel counterpart | class | measured divergence (test scenario) | ruling | @@ -519,7 +519,7 @@ line numbers are this tree's (683a82f); kernel semantics are | `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2361); 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:13226-13227; 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:16774, `update_risk_state` pine_adapter.cpp:13867-13894): 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:13219), 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:1200-1208): +1 per losing TRADE on a new chart day, reset to 0 by any winning trade at its fill (:1189-1209); 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_cons_loss_days` | `SourceDayLedger` (pine_adapter.hpp:1200-1208): +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:17724-17733); 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:16277); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16389), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14952-14961, `submit_intraday_loss_close` pine_adapter.cpp:14922-14950), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17131); 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:5961, `:7620`, `:8344`, `:8472`, `:11449`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18030-18033) | `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:13858-13860); 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:13764-13785, `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) | @@ -551,7 +551,7 @@ remain the five probes' baseline rows and the `SW` check above. **The P6 decision: native-only.** Re-verified on main `b8e7976e`: the ordering stands (`project()` at pine_strategy_host.cpp:324, `configure_native` at :325, -the `set_pine_risk_*` setters at `sha256:3c56d0cbb6e9a64ea2f797abe9ca619c9590c4a656b16845cfede5f5880a5b9d` pine_strategy_host.cpp:838-859, and corpus 442d497 +the `set_pine_risk_*` setters at pine_strategy_host.cpp:992-1014, and corpus 442d497 `corpus/validation/cap-risk-gates-allow-max-intraday-01`'s generated strategy still emits the three calls inside `on_source_bar`); `rg "spec\.risk|NativeRiskLimits" src/source include/pineforge/source` is still empty; `test_adapter_risk_relower` still @@ -736,8 +736,8 @@ applies literally: the mask cannot be justified without the word "TradingView". And what the adapter keeps is not a duplicate of the kernel mechanism that a re-lowering would delete: `source_trigger_threshold` (pine_adapter.cpp:342), `source_level_on_price_grid` (:328) and the -`nearest_tick` / `source_bar_fill_tick` / `directional_tick` spellings (`sha256:1ca6bb18dab0492e61bbf746b913361cf1c5961fdeba7b30bbcb5c92733f3085` pine_adapter.cpp:262, -:274, `sha256:8d5d9ec456f65dcd58c7452d13772cf4387209d88b719ba9374946ddd2f2ebbf` pine_adapter.cpp:307) are a *different* rule — a half-tick threshold per order kind with +`nearest_tick` (pine_adapter.cpp:257) / `source_bar_fill_tick` (:278) / +`directional_tick` (:302) spellings are a *different* rule — a half-tick threshold per order kind with the fill booked elsewhere (E7) — which is why re-lowering moves outcomes instead of preserving them. @@ -861,7 +861,7 @@ between the halves. The mark itself is the same bar on both. **A dead branch found and reported, not removed.** `fold_exit_trail_peak_` (engine.hpp:545) is initialised NaN, reset to NaN at run start -(engine_run.cpp:109) and **never assigned a finite value anywhere in the +(`fold_exit_trail_peak_` engine_run.cpp:109) and **never assigned a finite value anywhere in the tree**, so all four `if (!std::isnan(fold_exit_trail_peak_))` carries (engine_execution.cpp:1028/:493, the former adapter loop, the shared producer) are inert. The shared producer keeps the carry so it folds exactly what the @@ -1249,15 +1249,15 @@ unchanged after it. | # | Risk | Why it bites | Containment | Src | |---|---|---|---|---| -| E1 | **Spec hashing.** `hash_spec` folds every field unconditionally (native_execution_consumer.cpp:234-256) | 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 `sha256:620926caaa868e64ede32f547ac1dd0bfe81c9758267593a9a924051208df0a4` native_execution_consumer.hpp:957-961); 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`; `sha256:35c0d0669cc979f2823857bc53c07a161f7ccde5e6fad8d24de6e76e7891431c` pine_adapter.cpp:13141-13208, pine_adapter.cpp:13255); its chronology lives at `sha256:bb89768c9a7b2dc9f6de5943c04de8103ed107c21bb6e184446de0e6a84cf4c2` pine_adapter.cpp:13814-13891 and its excursion sample differs by chronology (`sha256:764a1a69f2940863b4f77cbf9d20267a19ec38e86720796c7081a59ac7245427` pine_strategy_host.cpp:711-741) | 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 (`sha256:4efede43f90de7b19954b2ebc50ffae605ae0dfb47f3170bf1ada4d30476becb` pine_adapter.cpp:2355-2395) | 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 (`sha256:ddad8dc42e078838c0d53fd49e50c31060221e3b82e7d5bbaa0420769daa7d31` pine_scheduler_native.cpp:585-637, `coof_next_waypoint` pine_adapter.cpp:5757) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `sha256:86617ca43bb2847380f1845b07cc9ecd27059814ee80fecf80649a9393dfe8c8` native_execution_consumer.cpp:3478-3487 and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290); new behaviour only behind new opt-ins | F | +| E1 | **Spec hashing.** `hash_spec` folds every field unconditionally (native_execution_consumer.cpp:234-256) | 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:1620); 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5757) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290); 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:16118-16147) | 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 (`sha256:6af98c54398c826743ce41dcfd60f830645dce5b36e4c49b1ca9a730cce13e46` pine_strategy_host.cpp:1570-1589) | 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 (`sha256:972f3223c918b8a99bfc939a154dca68ff094ef170588a039aeceb96d1363e16` pine_adapter.cpp:341-389) 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 (`sha256:ae40c5c462bd2f487afdc0ebbcdc9eef4a8839dd66cdc4f4f737af14e3484a60` engine_aux_security.cpp:48-87, *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 `sha256:32f12c2b3abdf3343933edb427d584cafb1e3ecb7ec626f8a462d4a73d390707` pine_strategy_host.cpp:1304-1388; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | -| E9 | **TA numerics are TV-calibrated** (1e-10 band in DMI / MFI / percentrank; conditional-call extremum ring `sha256:c344fc459ea1c1b7132a677736c323c95b3d07a763503d270ccbec8a34d770dd` ta.hpp:60-103) | Non-Pine users may expect IEEE semantics; changing them breaks parity | Rename only (§2.ii b). For an every-bar caller the ring equals a positional window (`sha256:6bc8f00599932461d82a5e99931f89345c9278ad38fa4b3595bb0bdcd6ce5deb` ta.hpp:79-80). Document. → U6 | F O S | +| E6 | **Double equity recording.** The source host already records per bar (history: only its cadence remains, `scheduler_mark_report_point` pine_strategy_host.cpp:1951) | 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:333) 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:46-78, *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:1620; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | +| E9 | **TA numerics are TV-calibrated** (1e-10 band in DMI / MFI / percentrank; conditional-call extremum ring `ExtremeRing` ta.hpp:134) | Non-Pine users may expect IEEE semantics; changing them breaks parity | Rename only (§2.ii b). For an every-bar caller the ring equals a positional window (the ring comment `sha256:fd1f92206558863ad8df33fd3ef3b8cc8b9bd6678a25fc98ca866168c1a81aca` ta.hpp:87-88). Document. → U6 | F O S | | E10 | **Dead-but-hashed lot flags** (§2.ii j) | Deleting them changes `broker_state_hash` | Separate, explicitly non-neutral lane (L12) | F O | | E11 | **OCA and ownership.** Pine's default-sized OCA and `from_entry` reservation rules are source policies | Source IDs, callsite tokens or Pine labels leaking into `WorkingRequestCore` | Opaque keys and receipts only (R5-4) | S | | E12 | **Legacy input tolerance.** `LegacyTolerant` changes bar-structure and slot-label admission (`NativeSlotLabelPolicy` native_run_spec.hpp:324, `NativeFeedTolerance` native_run_spec.hpp:345; `NativeLegacyTolerance` native_run_spec.hpp:357 is the deprecated spelling) | A rename must preserve the Pine route's hash and failure shape | Values pinned (§2.ii c); strict stays the `NativeRunSpec` default | S | @@ -1269,11 +1269,11 @@ unchanged after it. | P1 | **Epoch bumps** move the frozen C++ ABI fixtures and the settlement ABI checker `CLAUDE.md` names; a neutral-refactor worker may not edit frozen headers | Bump lanes are not neutral-refactor lanes; the brief lists the header extension (§3.1 c) | F O S | | P2 | **Stale fence.** `CLAUDE.md` names "measured sources" `engine_strategy_commands.cpp`, `engine_fills.cpp`, `engine_risk.cpp`, `engine_market_admission.cpp`; none exists in `src/` at `73817c1` (only `engine_orders.cpp` and `engine_run.cpp` do) | The supervisor restates the fence for the post-R4-C tree (likely `native_execution_consumer.cpp`, `native_order.cpp`, `engine_execution.cpp`, `src/source/*`) | F | | P3 | **Corpus gate state.** S's audit found `scripts/run_corpus.sh` stopping at the corpus strategy compile: committed `generated.cpp` files no longer match the engine (8/8 drifted from the pinned codegen commit under `VERIFY=1 ONLY=validation/analyzer scripts/regen_corpus_cpp.sh`) | §3.1 a relies on that script. Re-pin the engine / codegen corpus tuple before L1. Not re-verified in this merge (no builds were run) | S (single-source) | -| P4 | **Hash compatibility.** The source hash covers hundreds of adapter and scheduler fields (`sha256:bfc55e4f797e90093a003267c3d540ef62dbb5b578438c5a38ddb54e5b176267` pine_state_hash.cpp:207-672); L4 / L9 add hashed state, L12 removes some | A hash mismatch is a diagnostic until the trade diff is known, never a reason to relax parity (S). Open: does the campaign gate compare broker-state hashes across releases, or only trades (O)? → Q3 | O S | +| P4 | **Hash compatibility.** The source hash covers hundreds of adapter and scheduler fields (`PineExecutionAdapter::hash_state` pine_state_hash.cpp:237, `PineScheduler::hash_state` pine_state_hash.cpp:613); L4 / L9 add hashed state, L12 removes some | A hash mismatch is a diagnostic until the trade diff is known, never a reason to relax parity (S). Open: does the campaign gate compare broker-state hashes across releases, or only trades (O)? → Q3 | O S | | P5 | **Test asymmetry:** 430 adapter test files vs 14 native | native-only tests in every lane (§3.1 b) | O | -| P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, non-empty FX curves, security feeds and source setters (`sha256:de7e074b6a82f71018015f842ca51f4cba526d5b258a4c9b4d6c914ee12c0541` `sha256:b263f6923ef8095700dc254259434739b63a9fb043773b8139c77ef5e1df91b9` native-engine.md:425-439) | L5 and L6 must state which refusals are generic and which are deliberate source limits | S | +| P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, non-empty FX curves, security feeds and source setters (`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:298) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `sha256:81dee89712544bf03ed75e83b6eb379d3707a6c920a59e368d0574d6a5b928ba` pine_strategy_host.cpp:806-807 → pine_adapter.cpp:17569 (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:298) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:992 → `set_risk_direction` pine_adapter.cpp:18293 (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) @@ -1282,7 +1282,7 @@ unchanged after it. | Q1 | Does `engine_script_run_v18` open at L4 and close after L5 ∥ L6, or does L4's sizing hook ship with that batch (§3.4 row 4)? | — (raised by this merge) | | Q2 | Is a hash-domain bump (needed by §2.ii j) acceptable before the R5 final audit (phase table, native-refactor-progress.md:23, *fresh*), or must L12 wait? | F asks; none | | Q3 | Does the 4190-probe gate compare broker-state hashes across releases, or only trades? If hashes, L4 and L12 need a hash-version plan | O asks; none | -| Q4 | `SizeTime::AtAcceptance` needs a current execution point (`sha256:00d2682bf92be87d76eea9d03004abad74aee8b5ad03e3f611dbb0d6539a99c4` native_host.hpp:1295); between realtime inputs there is none — reject or fall back to `AtMatch`? | F: reject with `InvalidQuantityBasis` | +| Q4 | `SizeTime::AtAcceptance` needs a current execution point (`current_execution_point` native_execution_consumer.cpp:2816); between realtime inputs there is none — reject or fall back to `AtMatch`? | F: reject with `InvalidQuantityBasis` | | Q5 | Liquidation check granularity: `PathAdverseExtreme` default; is a cheaper `CalculationOnly` worth shipping in v1? Pure spec value or injectable policy object (S)? | F: path-accurate default; merged A.3 = spec value + host sizing hook | | Q6 | Does a native series return the current value, a retained series object, or both? It decides C ABI ownership and hash size | S asks; A.5 offers push + current-bar pull | | Q7 | Is a close-reservation key stable across host restarts or only within one `RunIdentity`? | S: identity rules suggest run scope | @@ -1338,7 +1338,7 @@ struct Sized { // O's name; F: SizedOpen Side side; SizeBasis basis; SizeTime time = SizeTime::AtMatch; - ExecutionGridPolicy grid_policy = ExecutionGridPolicy::SnapToGrid; // existing enum, `sha256:c0b6c4c98cd457180d8c9375ad6b8c6e9171d2a477fb6f4e944a11d7f0ef7ec1` native_order.hpp:442-456 + ExecutionGridPolicy grid_policy = ExecutionGridPolicy::SnapToGrid; // existing enum, `ExecutionGridPolicy` native_order.hpp:82 bool reserve_percent_fee = false; // divide cash by (1 + fee) for NativeFeeKind::Percent }; using OrderIntent = std::variant; @@ -1347,7 +1347,7 @@ struct ScopeFraction { double fraction = 1.0; ScopeClaim claim = ScopeClaim::Gro using ReductionSize = std::variant; // F ``` -- Resolution: `units = cash / (price * point_value * fx)`, `cash` = the value or `fraction * marked_equity(price)`, `price` = the candidate's `default_resolved_price`. It plugs in where `HostSized` units resolve today (native_execution_consumer.cpp:684; terms resolution `sha256:b06b928e9f12217d7d3036f6adf77250c691c2ab1ad69d48e87417dbdfe1adc7` native_execution_consumer.cpp:2953-2999). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:877); validation reuses `RequestRejectReason::InvalidQuantityBasis` (native_order.hpp:827). +- Resolution: `units = cash / (price * point_value * fx)`, `cash` = the value or `fraction * marked_equity(price)`, `price` = the candidate's `default_resolved_price`. It plugs in where `HostSized` units resolve today (native_execution_consumer.cpp:684; terms resolution `resolve_execution_terms` native_execution_consumer.cpp:4816). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:877); validation reuses `RequestRejectReason::InvalidQuantityBasis` (native_order.hpp:827). - `NetOfSiblings`: a fraction-sized reduce claims the bound scope minus the units already claimed by live sibling reduces on it; keys are opaque, never source IDs (S). - Not adopted: S's spec-level `default_sizing` + virtual `resolve_native_sizing` (duplicates the per-request intent, costs an epoch); S's `PercentFreeMargin` (needs L4's free-margin definition) and `EveryCalculation` timing; O's spec-level `size_net_of_fees` (per-request flag, 2 of 3); O's `PercentOfScope` basis (expressed as `ScopeFraction`); S's `ReplaceByKey` / `Fifo` modes (callsite semantics → adapter, R5-4). - TUs: `native_order.{hpp,cpp}`, `native_execution_consumer.cpp`, `native_run_spec.cpp`, the definition hash. @@ -1374,7 +1374,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:13137-13158). 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 (`sha256:ed6f4ff668195caf3c953c8ef5dd448c6010949353f7b9ef44d50b764f7488db` native_execution_consumer.cpp:1898-1911), 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:13137-13158). 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:2575), 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:4662-4675 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). @@ -1397,8 +1397,8 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): 1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1630-1631) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6982-6994) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). -2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`sha256:d2bc11d04fb37fc9c5d8b87bef2f5438be7a5f5c8ff72da5fcf255500d92f5cb` native_execution_consumer.cpp:3491-3500). `born_on_remaining_path` (native_execution_consumer.cpp:5616-5620) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1659-1666) is calibrated against it. -3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is native_execution_consumer.cpp:6835-6842). `on_native_tick` stays the observation hook (S). +2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5616-5620) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1659-1666) is calibrated against it. +3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6835-6842). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2557); valid in bar-open, applied and tick callbacks. 5. The kernel never attempts language-state rollback; that stays Pine's (`snapshot / restore / commit_coof_script_state`, pine_scheduler_native.cpp:52-104) (O). @@ -1430,14 +1430,14 @@ struct NativeWorkingRequest { native_order::DefinitionRef definition; native_ord native_order::TriggerState trigger_state; }; std::vector native_working_requests() const; // F: owning snapshot (S: NativePendingView) std::size_t cancel_all(); // F, O: one CancelledEvent per live request -std::size_t cancel_where(std::string_view label); // O: optional kernel label -> live-handle index (labels exist, `sha256:9816233d620312c84ef7dd99588b2af179b9a11b654fa0ecee5b6be4efef805f` native_order.hpp:141) +std::size_t cancel_where(std::string_view label); // O: optional kernel label -> live-handle index (labels exist, `Request::label` native_order.hpp:485) // native_order.hpp — native_order_v6, shared with L3 struct Absolute {}; struct FromOwnerFill { double offset = 0.0; bool ticks = false; }; // O: signed, adverse = negative using TriggerAnchor = std::variant; // Request::anchor = Absolute{} struct TrailTicks { double ticks; }; // O: offset spelling resolved against price_tick -struct ReplaceOptions { bool retain_trigger_state = false; }; // O: keeps TrailTrack::best (`sha256:89394716e7f759ed0ea8b2f289a272fb2e6f42c83ce09345e25075bd82fe8365` native_order.hpp:281-283) -// Trail::offset == 0.0 is accepted: "exit on the first adverse move past the best" (today the run fails, `sha256:d3232e815d3bf01205a28eb91aad33baccd7326713f96bd01333963a2e2fdd82` native_execution_consumer.cpp:3547-3552) +struct ReplaceOptions { bool retain_trigger_state = false; }; // O: keeps `TrailTrack::best` (native_order.hpp:651) +// Trail::offset == 0.0 is accepted: "exit on the first adverse move past the best" (now accepted: `valid_trail_offset` native_order.cpp:479) // include/pineforge/native_toolkit.hpp — header-only, Pine-free, covered by the include-independence checker (F; shapes from S P1) struct BracketSpec { native_order::RequestHandle parent; std::optional take_profit, stop_loss, trail; @@ -1465,7 +1465,7 @@ NativeGridRounding grid_rounding = NativeGridRounding::HalfUp; ``` - `QuantizeFills`: the kernel rounds `resolved_price` onto `price_tick` **before** slippage (`HalfUp`) or in the direction that favours the resting order (`Directional`). `QuantizeFillsAndTriggers`: the matcher additionally compares the quantized path against the raw level. -- TUs: `native_run_spec.cpp` (validation), `native_execution_consumer.cpp` (resolve path native_execution_consumer.cpp:3949-3971, `execute_current` path native_execution_consumer.cpp:3769-3779), `native_matching.hpp` (threshold form). The dead helpers engine.hpp:1255, engine.hpp:1265, `sha256:e0b3be5fe845e3a132c5b49967946649a4c2b32a67f9f240c44bd553a6a99b69` engine.hpp:1304 are the reference arithmetic (§2.ii e). +- TUs: `native_run_spec.cpp` (validation), `native_execution_consumer.cpp` (resolve path native_execution_consumer.cpp:3949-3971, `execute_current` path native_execution_consumer.cpp:3769-3779), `native_matching.hpp` (threshold form). The dead helpers were the reference arithmetic and are deleted (§2.ii e); the kernel's own step is `native_matching::apply_slippage` native_matching.hpp:259. ### A.8 L9 — risk limits (F P3 + O G7 + S P5) @@ -1482,7 +1482,7 @@ struct NativeRiskLimits { 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:1317) (O) or a callback `on_native_risk_event` (S — a virtual, so only inside an epoch batch). The day key uses the existing calendar (`sha256:af190cea22828f37fb9b832bcb29c9d50029da09280a8ddd3fc11149729a7142` native_calendar.hpp:363-366), not a chart clock. +- 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:1317) (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:373), 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:13690-13788); 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:4662-4675 and the per-point walk), `native_order.hpp`, the state hash. diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index b9bddf14..134cda4d 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -76,7 +76,7 @@ finding to report, not a step to take. site — and holds it against the ruling table. 5. **Every public host member has a C spelling or a recorded reason.** The - COVERAGE block at `sha256:993ee68af70ee77c00489a30fe13e94d4cdc631cc572cbc520ee165f51e4d24d` native_c_api.h:45 lists them; a member added without a + COVERAGE comment block (`COVERAGE` `sha256:71e3e35c1a2409524f511b5ee0e02df496852dc5ff629728a78c5c0401cb8ad0` native_c_api.h:52-55) lists them; a member added without a row, or a row naming a member that no longer exists, fails. *Enforced:* `scripts/check_native_c_api_surface.py`. @@ -244,7 +244,7 @@ policy layer that *uses* kernel features. Where every TradingView rule lives. **front door** — one of the three ways in: PineScript through codegen, C++ through `NativeStrategyHost` native_host.hpp:833, or C through the -`strategy_native_*` surface `sha256:1a4d41202e1b32a33ab75c2e42a36285ccb1d061087a8db1c19162b244c5b29f` native_c_api.h:2641. +`strategy_native_*` surface (`strategy_native_host_create_v1` native_c_api.h:2598). **twin** — a test unit compiled twice, once against a frozen historical header closure and once against the current one, so a behaviour change has to be diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index d32d1b6e..736a733b 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1876,7 +1876,7 @@ in-run facts is populated is answered by three `constexpr` predicates rather than by reading the tag yourself: `native_failure_has_cause` (`native_host.hpp:145`), `native_failure_has_recipient` (`native_host.hpp:149`) and -`native_failure_has_cursor` (`sha256:57ded95e69a90ce0e561eadbd0f81e8b130356dd7b480519e1904552d4647957` native_host.hpp:121) — each taking either the +`native_failure_has_cursor` (`native_host.hpp:153`) — each taking either the kind or the whole context. `native_failure_context_in_run` (`native_host.hpp:207`) builds one; `native_failed_run_identity` reads back the `RunIdentity` the failed spec carried, and a foreign run is dropped @@ -2919,7 +2919,7 @@ These are existing refusals, not implied future features: confirmed bars carry those series A C host has the same stream and the same commands. Streaming needs no new -symbol — `strategy_stream_begin` and its family (`sha256:2e963d6ab1630db1535bd944dc7406ba649e9d14a589065db25347569fbad150` native_c_api.h:37-39) take +symbol — `strategy_stream_begin` and its family (`pineforge.h:748`) take a `pf_strategy_t` from `strategy_native_host_create_v1` unchanged — and `strategy_native_submit_v1` (`native_c_api.h:2680`) obeys the one legality rule its C++ spelling does. @@ -4190,7 +4190,7 @@ constructor/member cut has since landed: `engine.hpp` has **zero** references to `CapAttachment`, `OrderPriority` or `IntradayCap`. `NativeStrategyHost` is zero-argument (`native_host.hpp:833`); the `CapAttachment` constructor belongs to `source::PineStrategyHost` (`pine_strategy_host.hpp:241-244`), and the cap -type itself lives in the adapter (`intraday_cap.hpp:18`). +type itself lives in the adapter (`IntradayCap` `intraday_cap.hpp:83`). Nor is there a build-level one. The two source sets are disjoint: `PINEFORGE_SOURCE_LAYER_SOURCES` (`CMakeLists.txt:91`) holds all six @@ -4201,7 +4201,7 @@ thirty-five, which are what `add_library` (`CMakeLists.txt:153`) compiles into `PINEFORGE_BUILD_SOURCE_LAYER` is ON, which is the default; the kernel archive exists either way. The installed-header closure is clean too, which the independence checker proves -(`sha256:7ae64b598bf26ee68b06a7746c27c3ae62f620b0443b6c8c12781b20fe20bcd5` check_native_include_independence.py:36-46). See +(`ROOT_HEADERS` check_native_include_independence.py:392). See `docs/adr/0001-kernel-adapter-boundary.md`. ### Building the kernel only diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 2932aa24..99a3b476 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -126,7 +126,7 @@ rather than defaulted. | `commission_value` | `fee_value` native_run_spec.hpp:589 | `fee_value` pineforge.h:540 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | | `slippage` | `slippage_ticks` native_run_spec.hpp:583 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3958-3959. TradingView's round-then-slip order is adapter-only. | | `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7524-7525, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | -| `pyramiding` | `max_open_lots` native_run_spec.hpp:595 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots `sha256:87bd0ff848901478840870d95eb356b35d7d368555ce8eadee8571b8e17f86ec` native_execution_consumer.cpp:2121-2124. The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). | +| `pyramiding` | `max_open_lots` native_run_spec.hpp:595 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:2914). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). | | `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:344 with `size_basis` native_c_api.h:2104 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | | `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2108 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | | `calc_on_order_fills` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2278 | `native_calc_on_fills_strategy.cpp` | `BarCloseAndFills` recalculates once at the cursor of each applied execution, bounded by `max_recalculations_per_point` native_run_spec.hpp:626. The generic cadence only: TradingView's waypoint-only refill, its two-fills-at-open rule and its script-state rollback stay in the adapter. | @@ -530,8 +530,8 @@ range-end reporting is off. | `strategy.grossprofit_percent` | `grossprofit_percent` engine.hpp:975 | `pf_metrics_t` pineforge.h:358 | `native_sized_report_strategy.cpp` | Of initial capital. | | `strategy.grossloss` | `gross_loss` engine.hpp:969 | `pf_trade_stats_t::gross_loss` pineforge.h:246 | `native_open_lots_strategy.cpp` | For booked closed trades the protected C++ accessor is signed (negative for losses), while the C report stores a positive loss magnitude. With range-end reporting off, `-C.gross_loss == gross_loss()` and `netprofit == grossprofit - C.gross_loss`. With it on, C trade statistics include the report-only range-end rows; filter `open_at_end == 0` before comparing to Pine's closed-trade statistics. | | `strategy.grossloss_percent` | `grossloss_percent` engine.hpp:978 | `pf_metrics_t` pineforge.h:358 | `native_sized_report_strategy.cpp` | | -| `strategy.openprofit` | `open_profit` `sha256:0490e46ca24b81c82c9549ce38af81be1ea888a60277263108cd26bf3f01e0c7` engine.hpp:1024 | `strategy_native_marked_equity_v1` native_c_api.h:3017 less realized balance, plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine's open profit is gross of remaining entry fees. The native marked equity and open-lot `unrealized_pnl` are net of them; use the same explicit mark for every term. | -| `strategy.openprofit_percent` | derive from `open_profit` `sha256:0490e46ca24b81c82c9549ce38af81be1ea888a60277263108cd26bf3f01e0c7` engine.hpp:1024 | — | `native_open_lots_strategy.cpp` | Pine's denominator is the realized equity. | +| `strategy.openprofit` | `open_profit` engine.hpp:1025 | `strategy_native_marked_equity_v1` native_c_api.h:3017 less realized balance, plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine's open profit is gross of remaining entry fees. The native marked equity and open-lot `unrealized_pnl` are net of them; use the same explicit mark for every term. | +| `strategy.openprofit_percent` | derive from `open_profit` engine.hpp:1025 | — | `native_open_lots_strategy.cpp` | Pine's denominator is the realized equity. | | `strategy.max_drawdown` | `max_drawdown_` engine.hpp:581 | `max_equity_drawdown` pineforge.h:296 | `native_open_lots_strategy.cpp` | Folded under **every** report policy — see the rule above. The scalar is the run's own; the `pf_equity_stats_t` figure derived from the recorded curve still needs `KernelRecorded` native_run_spec.hpp:63. | | `strategy.max_drawdown_percent` | `max_drawdown_percent` engine.hpp:1507 | `max_equity_drawdown_pct` pineforge.h:296 | `native_sized_report_strategy.cpp` | Same rule. | | `strategy.max_runup` | `max_runup_` engine.hpp:582 | `max_equity_runup` pineforge.h:300 | `native_open_lots_strategy.cpp` | Same rule. | @@ -768,7 +768,7 @@ the price and opening-shape halves of `resolve_execution_terms` native_host.hpp:935 `on_execution_terms`, `native_sized_units` native_host.hpp:1302 `strategy_native_sized_units_v1` and the `hash_host_extension` seam `on_hash_extension`; only its deprecated spelling -`hash_source_extension` has none. The `COVERAGE` block at `sha256:395b1137106e8be4a592796a7559d475edc75866c1632213ffd71a1f0f7c253d` native_c_api.h:52 +`hash_source_extension` has none. The `COVERAGE` comment block at `sha256:71e3e35c1a2409524f511b5ee0e02df496852dc5ff629728a78c5c0401cb8ad0` native_c_api.h:52-55 lists every public member with either its C spelling or that reason, and `scripts/check_native_c_api_surface.py` proves the list is exactly that class's public surface. @@ -1026,7 +1026,7 @@ PINEFORGE_EXPORT_NATIVE_STRATEGY(MyHost); ``` `PINEFORGE_EXPORT_NATIVE_STRATEGY` native_module.hpp:271 defines -`strategy_create` `sha256:bdc1d58cf27e0aaacfa19c793899d70108d6ac472ca4f29373d6d279a2f20f25` pineforge.h:618, `strategy_free`, `strategy_set_input` +`strategy_create` pineforge.h:597, `strategy_free`, `strategy_set_input` pineforge.h:680, `strategy_set_override` pineforge.h:690, `strategy_set_magnifier_volume_weighted` pineforge.h:698, `run_backtest` pineforge.h:631, `run_backtest_full` pineforge.h:648 and `report_free` diff --git a/scripts/check_doc_anchors.py b/scripts/check_doc_anchors.py index 6c5f1f92..7091be6b 100644 --- a/scripts/check_doc_anchors.py +++ b/scripts/check_doc_anchors.py @@ -121,12 +121,36 @@ qualifier its span names. ``NOCLAIM`` a ruling-table anchor, or a symbol-less range anywhere, carries no verifiable claim. -``COMMENTONLY`` the complete cited C/C++ window is comment-only. +``COMMENTONLY`` the complete cited C/C++ window is comment-only (a pinned one + too, unless the sentence says it cites a comment). +``HASHMISS`` the cited lines no longer hash to the content pin. +``PINLINE`` a content pin on a single line: a line is cited by its symbol. +``PINEMPTY`` a content pin over blank lines only (the empty string's hash). +``PINDOUBLE`` two or more pins before one anchor. The cited window is exactly ``[first, last]``. A symbol on the next line is -outside the citation. Ranges without a symbol use a backticked content hash, +outside the citation. A range without a symbol uses a backticked content hash, and the hash covers the cited source lines joined with ``\n``. +Content pins (R5 lane H-DOCGATES) +--------------------------------- +A pin ```sha256:<64 hex>``` written right before an anchor ACCOMPANIES the +claim; it never replaces it. The nearest backticked span before the pin is +still the claim and is still checked against the window, a ruling row still +needs its symbol or fragment, and a pinned comment window is ``COMMENTONLY`` +unless the sentence says "comment". Until this lane a pin dropped the claim and +was compared alone, so whatever the line held passed -- a wrong line, a blank +line, a garbage hash before a real one (the fourth audit's probes P09, P10, +P11 and P16, and 44 of the tree's 92 pins held something else than their +sentence claimed). + +A citation that prose words introduce after an earlier citation's comma +(```a` x.cpp:1, FX curve x.cpp:9``) claims nothing of the list's earlier +symbol; outside the ruling tables too, it is ``NOCLAIM`` until it carries its +own backticked symbol. A comma inside the sentence before the list's first +citation is not a list comma, and ``and`` / ``or`` / ``at`` and the other +``LIST_JOINERS`` do not introduce a new thing. + ``--fix`` --------- Rewrites only the digits of a BAD anchor, and only when the file resolves and @@ -231,6 +255,26 @@ SCOPE_BREAKS = ('|', '\n\n', '. ', '; ', '\n- ', '\n* ', '\n> ', ':\n') #: A backticked span that is nothing but an anchor: a citation, never a claim. ANCHOR_ONLY_RE = re.compile(r'^\s*(?:' + _PATH + r')?:\d+(?:-\d+)?\s*$') +#: The run of content pins written right before an anchor, and one pin in it. +PIN_RUN_RE = re.compile(r'((?:`sha256:[0-9a-f]{64}`\s*)+)$', re.I) +PIN_RE = re.compile(r'`sha256:([0-9a-f]{64})`', re.I) +#: The hash of the empty string: a pin over a blank line pins nothing. +EMPTY_SHA256 = hashlib.sha256(b'').hexdigest() +#: Words that join a citation list without naming a new thing ("`a` x.cpp:1, +#: and :7"); any other prose word after the list's last comma introduces a +#: citation of its own ("`a` x.cpp:1, FX curve x.cpp:9"), which then claims +#: nothing unless it carries its own backticked symbol. +LIST_JOINERS = frozenset('and or at in on plus then with also via from to line lines ' + 'its the its and/or'.split()) + + +def pins_before(text: str, start: int) -> tuple[int, list[str]]: + """Where the pin run before ``start`` begins, and its hashes (lowercase).""" + lookback = max(0, start - 1200) + match = PIN_RUN_RE.search(text, lookback, start) + if not match or match.end() != start: + return start, [] + return match.start(1), [h.lower() for h in PIN_RE.findall(match.group(1))] class Anchor: @@ -245,11 +289,12 @@ def __init__(self, page: Path, start: int, end: int, doc_line: int, self.digits = digits # (offset, length) of the ``line`` or ``a-b`` text self.symbol, self.span, self.mode = symbol, span, mode self.qualifier = qualifier_of(span) if symbol and span else None - self.content_hash = None - if symbol is None and span: - match = re.fullmatch(r'sha256:([0-9a-f]{64})', span.strip(), re.I) - if match: - self.content_hash = match.group(1).lower() + # The content pins written immediately before the anchor (R5 lane + # H-DOCGATES): a pin accompanies the claim, it never replaces it. + self.pins: list[str] = [] + self.content_hash: str | None = None + self.context = '' # the anchor's scope, up to the anchor + self.borrowed = False # a prose citation after a comma, claimless self.orphan = False self.ruling = False # set by collect(): a row of a ruling table self.fragment: str | None = None # a ruling row's code-fragment claim @@ -397,11 +442,14 @@ def scope_start(text: str, start: int) -> int: def claimed_symbol(text: str, anchor_start: int) -> tuple[str | None, str | None, str]: - """The nearest backticked span before the anchor, and the symbol it claims.""" + """The nearest backticked span before the anchor, and the symbol it claims. + + A content pin is never the claim (R5 lane H-DOCGATES): the nearest span + that is not a pin is. See borrowed_claim() for the comma rule.""" begin = scope_start(text, anchor_start) nearest = None for match in SPAN_RE.finditer(text, begin, anchor_start + 1): - if match.end() <= anchor_start: + if match.end() <= anchor_start and not HASH_RE.fullmatch(match.group(1).strip()): nearest = match if nearest is None: return None, None, 'word' @@ -409,6 +457,39 @@ def claimed_symbol(text: str, anchor_start: int) -> tuple[str | None, str | None return symbol, nearest.group(1), mode +def borrowed_claim(text: str, anchor_start: int, span: str | None) -> bool: + """Whether the anchor would only borrow ``span``'s claim across a list comma. + + AUDIT4-opus docs-a N2: in ```strategy_configure_native_v1` c_abi.cpp:882, + FX curve c_abi.cpp:882, probe c_abi.cpp:882`` the second and third + citations name other things ("FX curve", "probe") yet passed under the + first one's symbol, and re-anchoring moved them onto its line. A citation + that follows an earlier citation of the same list, after a comma and prose + words, claims nothing of its own (R5 lane H-DOCGATES). A comma inside the + sentence that precedes the list's first citation is not a list comma.""" + if span is None: + return False + begin = scope_start(text, anchor_start) + nearest = None + for match in SPAN_RE.finditer(text, begin, anchor_start + 1): + if match.end() <= anchor_start and not HASH_RE.fullmatch(match.group(1).strip()): + nearest = match + if nearest is None: + return False + between = PIN_RE.sub(' ', text[nearest.end():anchor_start]) + cited = [m for m in list(ANCHOR_RE.finditer(between)) + list(CONT_RE.finditer(between))] + if cited: + after = between[max(m.end() for m in cited):] + elif ANCHOR_ONLY_RE.match(nearest.group(1)): + after = between # the nearest span IS the list's last citation + else: + return False + if ',' not in after: + return False + words = [w.lower() for w in re.findall(r'[A-Za-z][A-Za-z-]+', after.rsplit(',', 1)[1])] + return any(w not in LIST_JOINERS for w in words) + + def collect(page: Path, text: str) -> list[Anchor]: """Every anchor on the page, in source order, with its symbol claim.""" skip = fenced_spans(text) @@ -437,15 +518,18 @@ def doc_line(offset: int) -> int: first = int(match.group('a')) last = int(match.group('b') or match.group('a')) digit_start = match.start() + len(match.group('path')) + 1 - symbol, span, mode = claimed_symbol(text, match.start()) - immediate = re.search(r'`(sha256:[0-9a-f]{64})`\s*$', text[:match.start()], re.I) - if immediate: - symbol, span, mode = None, immediate.group(1), 'word' - elif span and re.fullmatch(r'sha256:[0-9a-f]{64}', span.strip(), re.I): + claim_end, pins = pins_before(text, match.start()) + symbol, span, mode = claimed_symbol(text, claim_end) + borrowed = borrowed_claim(text, claim_end, span) + if borrowed: symbol, span, mode = None, None, 'word' - found.append(Anchor(page, match.start(), match.end(), doc_line(match.start()), - match.group('path'), first, last, - (digit_start, match.end() - digit_start), symbol, span, mode)) + anchor = Anchor(page, match.start(), match.end(), doc_line(match.start()), + match.group('path'), first, last, + (digit_start, match.end() - digit_start), symbol, span, mode) + anchor.pins, anchor.content_hash = pins, (pins[-1] if pins else None) + anchor.borrowed = borrowed + anchor.context = text[scope_start(text, claim_end):match.start()] + found.append(anchor) # Continuations inherit the last full anchor in the same paragraph or table # row. A physical wrap is harmless; a blank line, heading or next table row @@ -460,11 +544,10 @@ def doc_line(offset: int) -> int: prior.sort(key=lambda a: a.end) first = int(match.group('a')) last = int(match.group('b') or match.group('a')) - symbol, span, mode = claimed_symbol(text, match.start()) - immediate = re.search(r'`(sha256:[0-9a-f]{64})`\s*$', text[:match.start()], re.I) - if immediate: - symbol, span, mode = None, immediate.group(1), 'word' - elif span and re.fullmatch(r'sha256:[0-9a-f]{64}', span.strip(), re.I): + claim_end, pins = pins_before(text, match.start()) + symbol, span, mode = claimed_symbol(text, claim_end) + borrowed = borrowed_claim(text, claim_end, span) + if borrowed: symbol, span, mode = None, None, 'word' if prior and span is not None and ANCHOR_ONLY_RE.match(span): # "`sym` x.cpp:1730, `:1755`, `:7695`": the nearest span is the @@ -474,6 +557,9 @@ def doc_line(offset: int) -> int: prior[-1].path if prior else '', first, last, (match.start() + 1, match.end() - match.start() - 1), symbol, span, mode) + anchor.pins, anchor.content_hash = pins, (pins[-1] if pins else None) + anchor.borrowed = borrowed + anchor.context = text[scope_start(text, claim_end):match.start()] anchor.orphan = not bool(prior) found.append(anchor) found.sort(key=lambda a: a.start) @@ -643,18 +729,30 @@ def judge(anchor: Anchor, tree: Tree) -> None: if anchor.first < 1 or anchor.last < anchor.first or anchor.last > len(lines): anchor.verdict = 'NOLINE' anchor.detail = f'{path.relative_to(tree.root)} has {len(lines)} lines' - if anchor.verdict == 'OK' and path.suffix in CXX_SUFFIXES and anchor.content_hash is None: + if anchor.pins and anchor.verdict == 'OK': + judge_pins(anchor, tree, path) + if anchor.bad: + return + elif anchor.verdict == 'OK' and path.suffix in CXX_SUFFIXES: code = tree.code_lines(path) if not any(line.strip() for line in code[anchor.first - 1:anchor.last]): anchor.verdict = 'COMMENTONLY' anchor.detail = f'lines {anchor.first}-{anchor.last} contain no C/C++ code' return + if anchor.borrowed and anchor.verdict == 'OK': + anchor.verdict = 'NOCLAIM' + anchor.detail = ('a citation that prose words introduce after a comma claims nothing, ' + 'not the list\'s earlier symbol: put the symbol it cites in backticks ' + 'before it') + return if anchor.symbol is None: - if anchor.content_hash is not None and anchor.verdict == 'OK': - content = '\n'.join(lines[anchor.first - 1:anchor.last]).encode() - actual = hashlib.sha256(content).hexdigest() - if actual != anchor.content_hash: - anchor.verdict, anchor.detail = 'HASHMISS', f'content hash is {actual}' + if anchor.pins: + if anchor.ruling and anchor.fragment is None: + anchor.verdict = 'NOCLAIM' + anchor.detail = ('a pin is not a claim: a ruling-table anchor names the symbol ' + '(or the code fragment) it cites in backticks, pinned or not') + elif anchor.fragment is not None: + judge_fragment(anchor, tree, path) elif anchor.fragment is not None: judge_fragment(anchor, tree, path) elif (anchor.length > 1 and anchor.span is None and anchor.verdict == 'OK'): @@ -722,6 +820,37 @@ def aim(target: int) -> None: f'on {len(hits)} lines: {shown}; not fixed)') +def judge_pins(anchor: Anchor, tree: Tree, path: Path) -> None: + """A content pin accompanies a claim; it never stands in for one (R5 lane + H-DOCGATES, AUDIT4-opus docs-a N1: P09-P11, P16). A pin covers a range -- + a single line is cited by its symbol -- once, over content that is not + blank, and the window must still hash to it. A pinned C/C++ window that is + only comment is COMMENTONLY unless the sentence says it cites a comment.""" + lines = tree.lines(path) + window = lines[anchor.first - 1:anchor.last] + if len(anchor.pins) > 1: + anchor.verdict = 'PINDOUBLE' + anchor.detail = (f'{len(anchor.pins)} pins before one anchor; only the last was ever ' + 'compared: keep one') + elif anchor.pins[0] == EMPTY_SHA256 or not any(line.strip() for line in window): + anchor.verdict = 'PINEMPTY' + anchor.detail = 'the pin covers blank lines only, so it pins nothing' + elif anchor.first == anchor.last: + anchor.verdict = 'PINLINE' + anchor.detail = ('a single line is cited by the symbol it holds, not by a pin: drop ' + 'the pin and keep (or add) the backticked symbol') + else: + actual = hashlib.sha256('\n'.join(window).encode()).hexdigest() + if actual != anchor.content_hash: + anchor.verdict, anchor.detail = 'HASHMISS', f'content hash is {actual}' + elif (path.suffix in CXX_SUFFIXES + and not any(line.strip() for line in tree.code_lines(path)[anchor.first - 1:anchor.last]) + and not re.search(r'\bcomment', anchor.context, re.I)): + anchor.verdict = 'COMMENTONLY' + anchor.detail = (f'lines {anchor.first}-{anchor.last} contain no C/C++ code, and ' + 'the sentence does not say it cites a comment') + + def judge_fragment(anchor: Anchor, tree: Tree, path: Path) -> None: """A ruling row's code fragment must appear, whitespace aside, in the window.""" lines = tree.lines(path) diff --git a/scripts/test_check_doc_anchors.py b/scripts/test_check_doc_anchors.py index 6a9f9e7a..749eb87d 100644 --- a/scripts/test_check_doc_anchors.py +++ b/scripts/test_check_doc_anchors.py @@ -23,6 +23,13 @@ fails, and a code fragment there is verified verbatim; a continuation list shares the claim of the anchor that heads it, and a continuation takes the path of the last *full* anchor before it, never of another continuation. + +Lane H-DOCGATES (AUDIT4-opus X13) pins the pin rules (``PinMustFail``, the +audit's probes P09/P10/P11/P16): a pin accompanies the claim and never replaces +it, a single line is not pinned, a blank or doubled pin is rejected, a pinned +ruling row still needs its claim, and a pinned comment window must say it cites +a comment. ``BorrowedClaim``: a citation prose words introduce after a list +comma claims nothing of the list's earlier symbol. """ from __future__ import annotations @@ -478,5 +485,129 @@ def test_source_fence_reproduces_item_33(self) -> None: self.assertIn('SYMMISS', out) + +def pin(first: int, last: int, text: str = HEADER) -> str: + """The content pin of lines first..last of `text`, as a page writes it.""" + lines = text.splitlines() + import hashlib + return 'sha256:' + hashlib.sha256('\n'.join(lines[first - 1:last]).encode()).hexdigest() + + +COMMENT_HEADER = """#pragma once +// A comment block the pages cite as a comment: +// its lines hold no code at all, +// only prose about resolve_terms. +int resolve_terms(int units); +""" + + +class PinMustFail(unittest.TestCase): + """R5 lane H-DOCGATES (AUDIT4-opus docs-a N1, probes P09-P11 and P16): a + content pin accompanies a claim and never replaces it. Before the lane + each of these citations read OK, because the pin dropped the symbol claim + and was compared alone.""" + + def test_p09_a_pin_over_the_wrong_line_does_not_save_the_claim(self) -> None: + # The audit's P09: `strategy_create` pinned over the line after it. + with tree(f'`resolve_terms` `{pin(11, 11)}` native_host.hpp:11 answers.\n') as t: + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn('native_host.hpp:11 [PINLINE]', out) + # A range pinned exactly but holding another symbol: the claim is still checked. + with tree(f'`resolve_terms` `{pin(16, 20)}` native_host.hpp:16-20 answers.\n') as t: + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn('native_host.hpp:16-20 [SYMMISS]', out) + + def test_p10_a_pin_over_a_blank_line_pins_nothing(self) -> None: + empty = 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + self.assertEqual(pin(4, 4), empty) + with tree(f'`resolve_terms` `{empty}` native_host.hpp:4 answers.\n') as t: + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn('native_host.hpp:4 [PINEMPTY]', out) + with tree(f'The gap `{pin(27, 27)}` native_host.hpp:27 is blank.\n') as t: + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn('[PINEMPTY]', out) + + def test_p11_a_doubled_pin_is_rejected(self) -> None: + garbage = 'sha256:' + '0' * 64 + with tree(f'The struct `{garbage}` `{pin(5, 8)}` native_host.hpp:5-8 holds it.\n') as t: + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn('native_host.hpp:5-8 [PINDOUBLE]', out) + + def test_p16_a_pin_is_no_claim_in_a_ruling_row(self) -> None: + body = ('## Residual TradingView-named surface\n\n' + '| name | where | ruling |\n|---|---|---|\n' + f'| `probe_ruling_h` | a row pinned over code `{pin(16, 20)}` ' + 'native_host.hpp:16-20 | probe |\n') + with ruling(body) as t: + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn('native_host.hpp:16-20 [NOCLAIM]', out) + # With the symbol the window holds, the same pinned row passes. + body = body.replace('a row pinned over code', '`NativeStrategyHost`') + with ruling(body) as t: + code, out = t.run() + self.assertEqual(code, 0, out) + + def test_a_pinned_comment_window_must_say_it_cites_a_comment(self) -> None: + with tree(f'`resolve_terms` `{pin(2, 4, COMMENT_HEADER)}` comments.hpp:2-4 decides.\n') as t: + (t.root / 'include/pineforge/comments.hpp').write_text(COMMENT_HEADER) + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn('comments.hpp:2-4 [COMMENTONLY]', out) + with tree(f'The comment `{pin(2, 4, COMMENT_HEADER)}` comments.hpp:2-4 says so.\n') as t: + (t.root / 'include/pineforge/comments.hpp').write_text(COMMENT_HEADER) + code, out = t.run() + self.assertEqual(code, 0, out) + + def test_a_pinned_range_with_its_claim_passes(self) -> None: + with tree(f'The class `NativeStrategyHost` `{pin(16, 20)}` native_host.hpp:16-20.\n') as t: + code, out = t.run() + self.assertEqual(code, 0, out) + with tree(f'The class `{pin(16, 20)}` native_host.hpp:16-20 opens here.\n') as t: + code, out = t.run() + self.assertEqual(code, 0, out) # prose outside ruling rows: the pin alone + with tree(f'The class `{pin(16, 19)}` native_host.hpp:16-20 moved.\n') as t: + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn('[HASHMISS]', out) + + +class BorrowedClaim(unittest.TestCase): + """AUDIT4-opus docs-a N2 / GAP-5: a citation that prose words introduce after + an earlier citation's comma claims nothing; it does not borrow the list's + symbol (design:177's "FX curve" and "probe" rode `strategy_configure_native_v1`).""" + + def test_a_prose_citation_after_a_list_comma_is_noclaim(self) -> None: + with tree('`resolve_terms` native_host.hpp:14, other native_host.hpp:14.\n') as t: + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn('[NOCLAIM]', out) + listed = t.run('--list')[1] + self.assertRegex(listed, r'OK +native_host\.hpp:14 +`resolve_terms`') + self.assertRegex(listed, r'NOCLAIM +native_host\.hpp:14 +\(no symbol\)') + with tree('`resolve_terms` `native_host.hpp:14`, and its body `native_host.hpp:28`.\n') as t: + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn('native_host.hpp:28 [NOCLAIM]', out) + + def test_a_list_continuation_and_a_sentence_comma_still_share_the_claim(self) -> None: + for body in ('`resolve_terms` native_host.hpp:14, :28.\n', + '`resolve_terms` native_host.hpp:14, and native_host.hpp:28.\n', + '`resolve_terms`, the one function, native_host.hpp:28.\n', + '`resolve_terms` (native_host.hpp:14, native_host.hpp:28).\n'): + with self.subTest(body=body): + with tree(body) as t: + code, out = t.run() + self.assertEqual(code, 0, out) + with tree('`resolve_terms` native_host.hpp:14, other `resolve_terms` native_host.hpp:28.\n') as t: + code, out = t.run() + self.assertEqual(code, 0, out) # its own claim + + if __name__ == '__main__': unittest.main() From 33de3402d7539b4a7ca66c4be9152bbeb9ea26ce Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 03:41:07 +0800 Subject: [PATCH 12/83] Gates: the doc lint reads native-*/v, the ABI numbers, VERSION and src/include paths (R5 lane H-DOCGATES, X13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUDIT4-opus docs-a GAP-6 (probes P19, P20, P29-P32): the lint was blind to a dead `src/` or `include/` path, to a stale `native-consumer/v8` or `native-driver/v4` domain, to "`PF_ABI_VERSION` is 3" and to "PineForge 0.13.1, the current release". - rule 3's live hash domains also read every "native-/v" literal the tree declares (market_driver.hpp: native-driver/v5, native-consumer/v9, native-calendar/v1); rule 7 judges a marker on one; - rule 5 reads `src/` and `include/` paths to a C/C++ file or directory; a `git show :path` object path and a file its clause calls generated are not dead; - rule 8 (new): a stated PF_ABI_VERSION / PF_NATIVE_API_VERSION must be the header's #define; a higher number is a forward reference unless stated as current, a lower one needs the marker and a history framing; - rule 9 (new): a PineForge release stated as the current one must be VERSION's. Self-tests 55 -> 67 (MustFailH: P19, P20, P29, P30, P31, P32, a marker on a native domain stated as now, a forward ABI number stated as current; MustNotFireH: live values, forward/historical numbers, git object paths, a generated header, a foreign crate's src/main.rs, a native domain as history). Against 21bdc2e8's lint the eight must-fail cases fail and the four controls pass (lint_selftests.before.txt in the lane scratch). The tree had three offenders under the new rules, all in the design record: row o (§2.ii) now says the admission journal "was" include/pineforge/market_admission.hpp + src/market_admission.cpp until N14 moved both under source/ (marked history), and §2.iii's O proposal is spelled `c_native_host.cpp` like S's, not as a tree path. The lint reads 0 offenders; docs/ci.md lists the new families. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 845b07a5a345312bfa85a71fbfd9b5aeab514724) --- docs/ci.md | 8 +- docs/design/native-feature-parity.md | 4 +- scripts/check_doc_lint.py | 128 ++++++++++++++++++++++++--- scripts/test_check_doc_lint.py | 85 +++++++++++++++++- 4 files changed, 204 insertions(+), 21 deletions(-) diff --git a/docs/ci.md b/docs/ci.md index 24fe08d2..20874f4f 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -454,7 +454,9 @@ python3 scripts/check_doc_anchors.py --fix # re-anchor what is provable label on a published page, a `there is no … yet / in this slice / today` claim whose line carries no `` marker, a stale epoch or hash domain, a relative markdown link whose file or `#anchor` is missing, a cited -`tests/…`, `examples/…` or `scripts/…` path that does not exist, a count the +`tests/…`, `examples/…`, `scripts/…`, `src/…` or `include/…` path that does not +exist, a `PF_ABI_VERSION` / `PF_NATIVE_API_VERSION` number the headers do not +define or a "current release" that is not `VERSION`'s, a count the tree derives stated wrong (the `PF_API` declarations of the two C headers, `KERNEL_MIN_TESTS` / `RELEASE_MIN_TESTS`, the example manifest of `check_native_include_independence.py`), and the marker itself sitting on a @@ -462,8 +464,8 @@ sentence that states a non-live epoch as today's. The marker on the line above is the escape hatch itself: this sentence names the pattern rather than claiming it, and the guard has no way to tell those apart, so somebody has to say so where the diff will show it. The -live epoch set is read out of the tree — every `inline namespace _v` -and every `"pineforge-…/v"` domain — so the rule needs no edit when an epoch +live epoch set is read out of the tree — every `inline namespace _v`, +every `"pineforge-…/v"` domain and every `"native-…/v"` domain — so the rule needs no edit when an epoch bumps and does not flag a live epoch of a family whose other members moved on. ```sh diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 74756495..215bf30b 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -216,7 +216,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1273 | 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) | | m | `request.security` machinery | `register_security_eval` engine_security.cpp:27, `set_native_security_feed` engine_aux_security.cpp:76-127, `SecurityEvalState` engine.hpp:1156-1360 carry Pine semantics (lookahead, gaps, Heikin-Ashi, KI-55 range start) and are driven only by the source layer | HASH-VISIBLE / measured path | F planned to move both TUs and the state into the source host after native HTF. **R5-6 narrows this:** L6 reuses the feed store and routing, so those stay kernel; only the Pine-only evaluator semantics remain candidates. Do last. **Status (lane L12b): done** — the evaluator semantics are `source::PineSecurityEvalState` + `src/source/pine_security_eval.cpp`; the kernel keeps the registry, aggregator, feed store and one generic step | F S | | n | S-only proposals, not adopted | A generic `FillModel` interface with a TV model installed by the adapter (for the TradingView money / tick / path comment blocks in `engine.hpp` and the dead helpers of item e); "emit generic FIFO facts and let an adapter policy produce the KI-62 scratch" | — | Superseded by R5-3 (price-grid lane) and R5-1 (TV fill specifics stay behind `resolve_execution_terms`); the dead-helper part is item e; there is no scratch code left to re-lower (item d) | S | -| o | `pineforge::admission` (market-admission journal) — **N14 addition** | `include/pineforge/market_admission.hpp` (275 lines, installed by a kernel-only build) + `src/market_admission.cpp` (kernel TU): the observation journal of TradingView admission reviews; every `Configuration` field is a `strategy()` declaration parameter. No kernel TU consumed it (engine.hpp included it and used nothing; the hash helper in `src/broker_state_hash_internal.hpp` had one caller, `src/source/pine_state_hash.cpp`) | NEUTRAL (pure move; the fold is unchanged, so no hash domain moves) | **Status (N14): done** — moved whole to `include/pineforge/source/market_admission.hpp` + `src/source/market_admission.cpp` (`PINEFORGE_SOURCE_LAYER_SOURCES`); `hash_admission_field` moved verbatim into the source hash TU; `check_market_admission_schema.py` re-pointed; `MarketAdmissionDraft` / `MarketAdmissionJournal` spellings and the `market_admission_v2` inline namespace unchanged. ADR-0001 no longer calls it a generic stem | audit (Opus N14) | +| o | `pineforge::admission` (market-admission journal) — **N14 addition** | was `include/pineforge/market_admission.hpp` (275 lines, installed by a kernel-only build) + `src/market_admission.cpp` (kernel TU) until N14 moved both under `source/` (`include/pineforge/source/market_admission.hpp`, `src/source/market_admission.cpp`): the observation journal of TradingView admission reviews; every `Configuration` field is a `strategy()` declaration parameter. No kernel TU consumed it (engine.hpp included it and used nothing; the hash helper in `src/broker_state_hash_internal.hpp` had one caller, `src/source/pine_state_hash.cpp`) | NEUTRAL (pure move; the fold is unchanged, so no hash domain moves) | **Status (N14): done** — moved whole to `include/pineforge/source/market_admission.hpp` + `src/source/market_admission.cpp` (`PINEFORGE_SOURCE_LAYER_SOURCES`); `hash_admission_field` moved verbatim into the source hash TU; `check_market_admission_schema.py` re-pointed; `MarketAdmissionDraft` / `MarketAdmissionJournal` spellings and the `market_admission_v2` inline namespace unchanged. ADR-0001 no longer calls it a generic stem | audit (Opus N14) | | p | TradingView trail tick arithmetic — **N14 addition** | `src/engine_internal.hpp`: `kTrailPointsCeilEps` (5e-5 tolerant ceil of `trail_points`), `trail_points_to_ticks`, `trail_offset_to_ticks`, `snap_trail_level_to_tick_grid`; every value a `lab tv` calibration; readers only in `src/source/` and `src/compat/pine/` (the kernel's trail is `TrailTicks`, L7) | NEUTRAL (bodies byte-identical) | **Status (N14): done** — moved verbatim to `include/pineforge/compat/pine/trail_ticks.hpp` (`pineforge::compat::pine`); nine call sites re-qualified; `test_trail_fill_snap_l4c` includes the compat header (CHECK texts unchanged, twin parity OK) and is therefore a source-layer row | audit (Opus N14) | | q | OT8 live-tail / probe-suppress overrides — **N14 addition** | `BacktestEngine::realtime_tail_`, `realtime_tail_horizon_bars_`, `probe_suppress_tail_logic_` with public kernel setters and the horizon walk `apply_realtime_tail_horizon` (engine_run.cpp); every reader in `src/source/`; two frozen `PF_API` setters in `c_abi.cpp` | HASH-WAIVED (configuration before and after; no domain moves) | **Status (N14): done** — as 2.ii k: state, setters, readers and the walk move verbatim to `source::PineStrategyHost`; the C exports stay in `c_abi.cpp` and reach the host through two kernel virtual seams (`virtual bool set_realtime_tail`, `virtual bool set_probe_suppress_tail_logic`) whose kernel defaults are accepted-and-inert (answer `false`, no error: the L1 pre-begin ingress contract `test_native_example_batch` pins on a native module); v16→v18 relocation manifest rows added, epoch v18 still open, no bump; kernel-only pin `test_native_tail_override_seam` | audit (Opus N14) | | r | Lower-timeframe merge-flag rule — **N14 addition** | `internal::ensure_supported_lower_tf_emulation_flags` (engine_lower_tf.cpp) threw "request.security lower TF emulation only supports lookahead=barmerge.lookahead_off and gaps=barmerge.gaps_off" from a kernel TU — the one executable Pine/`barmerge` string in `libpineforge_kernel.a` | NEUTRAL (message and refusal unchanged) | **Status (N14): done** — the predicate is a file-local helper in `src/source/pine_security_eval.cpp`; the kernel TU keeps the generic primitives (fixed intraday TF parser, input : requested ratio, evenly sampled sub-bars), which have kernel-only tests (`test_lower_tf_parse_extra`, `test_lower_tf_seconds_suffix`) and source-only production callers — retained as a generic capability | audit (Opus N14, Codex N4) | @@ -261,7 +261,7 @@ PF_API int pf_native_events_v1(pf_strategy_t, uint64_t after_ordinal, pf_native_ | Point | Decision | Src | |---|---|---| -| Implementation | One new kernel TU (F `src/native_c_host.cpp`, O `src/c_native_host.cpp`, S `native_c_api.cpp`): `class CCallbackHost final : public NativeStrategyHost` forwarding each virtual to the table. Streaming reuses the existing `strategy_stream_*` symbols unchanged (they take any `BacktestEngine*`) | F O S | +| Implementation | One new kernel TU (F `src/native_c_host.cpp`, O `c_native_host.cpp`, S `native_c_api.cpp`): `class CCallbackHost final : public NativeStrategyHost` forwarding each virtual to the table. Streaming reuses the existing `strategy_stream_*` symbols unchanged (they take any `BacktestEngine*`) | F O S | | Errors | A non-zero callback return maps to `NativeFailureCode::CallbackException`; C callbacks must not unwind. The C++ path already latches exceptions as `CallbackException` native_execution_consumer.cpp:7068 | F O | | Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1398-1405): inside a callback, or between realtime inputs | F | | Hardening | Tagged, size-prefixed PODs; `struct_size` + version; unknown-tag refusal; never cast the C request to a C++ variant; a C fixture compiles against the frozen previous header. S puts the API in a separate header `include/pineforge/native_c_api.h` | S | diff --git a/scripts/check_doc_lint.py b/scripts/check_doc_lint.py index 505822fa..6d97b529 100644 --- a/scripts/check_doc_lint.py +++ b/scripts/check_doc_lint.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Keep seven kinds of rot out of the published documentation. +"""Keep nine kinds of rot out of the published documentation. Why a second script rather than a mode of ``check_doc_anchors.py`` ------------------------------------------------------------------ @@ -12,8 +12,8 @@ and this one's offender list reads as a work list rather than as noise inside another report. -The seven rules ---------------- +The nine rules +-------------- 1. ``lane L`` in a published page (``docs/pages/*.md``, ``README.md``). A roadmap label is a promise about the future. Fifteen of them in ``pine-to-native.md`` describe work that landed campaigns ago, so a reader @@ -29,8 +29,10 @@ 3. A stale epoch or hash-domain token anywhere under ``docs/`` or in ``README.md``. The live set is read out of the tree itself - every - ``inline namespace _v`` and every ``"pineforge-…/v"`` hash - domain - and any *other* version of a live family is stale. Deriving it + ``inline namespace _v``, every ``"pineforge-…/v"`` hash + domain and, since R5 lane H-DOCGATES, every ``"native-…/v"`` domain + (``native-consumer/v9``, ``native-driver/v5``) - and any *other* version of + a live family is stale. Deriving it beats a hardcoded range: it needs no edit when an epoch bumps, and it does not flag ``native_order_v1``, which is the live identity epoch even though ``native_order.hpp`` is at v6. The same ```` marker @@ -49,7 +51,10 @@ link (``[`docs/ci.md`](docs/ci.md)``) is still read as one. 5. A cited ``tests/…``, ``examples/…`` or ``scripts/…`` path that does not - exist, anywhere the rules above look. A migration table's "Runs in" + exist, anywhere the rules above look -- and, since R5 lane H-DOCGATES, a + ``src/…`` or ``include/…`` path to a C/C++ source or header (a ``git show + :path`` object path is history by its spelling, and a file the + clause calls *generated* exists only in a build or an install). A migration table's "Runs in" column and a reference page's "pinned by" clause are the reader's way to the witness; a file that never existed sends them nowhere (four of them on ``pine-to-native.md`` did). A ```` or ``*`` in a path is a @@ -88,6 +93,17 @@ marker from exempting it. A version *above* every live one is a forward reference (``removed at lifecycle_v2``) and is judged by its framing only. +8. A stated ``PF_ABI_VERSION`` or ``PF_NATIVE_API_VERSION`` that is not the + number the header defines (R5 lane H-DOCGATES). A lower number is history + and needs the marker and a history framing; a higher one is a forward + reference (``removed at PF_ABI_VERSION 5``) unless the clause states it as + today's (``is now``, ``current``). + +9. A PineForge release stated as the current one (``the current release``, + ``the latest release``, ``this tree's version``) that is not the ``VERSION`` + file's (R5 lane H-DOCGATES). A version framed as history (``since``, + ``was``, a release-notes link) is not a claim about this tree. + Exit status ----------- 0 when no offender is found, 1 otherwise. ``ci_preflight`` runs it as a @@ -124,13 +140,31 @@ CODE_SPAN_RE = re.compile(r'(`+)(.+?)\1') INLINE_NS_RE = re.compile(r'\binline namespace\s+([a-z][a-z0-9_]*_v[0-9]+)') LITERAL_DOMAIN_RE = re.compile(r'"pineforge-([a-z-]+)/v([0-9]+)') +# Rule 3's second domain family (R5 lane H-DOCGATES): the kernel's own +# `native-/v` hash domains (market_driver.hpp). +NATIVE_DOMAIN_RE = re.compile(r'(?+\-]*[A-Za-z0-9_*/>])') +CITED_PATH_RE = re.compile(r'(?+\-]*[A-Za-z0-9_*/>])') +# A src/ or include/ citation is this tree's only when it names a C/C++ file or +# a directory (`src/main.rs` on the Rust page is the example crate's). +TREE_SOURCE_RE = re.compile(r'^(?:src|include)/(?:.*\.(?:cpp|hpp|h|c|cc|inc|ipp)|.*/)$') # Words that say a path is gone on purpose (rule 5), or that a clause is about # the past (rule 7). HISTORY_RE = re.compile( @@ -218,9 +252,70 @@ def live_epochs(root: Path) -> tuple[set[str], set[str]]: namespaces.update(INLINE_NS_RE.findall(text)) domains.update(f'pineforge-{family}/v{number}' for family, number in LITERAL_DOMAIN_RE.findall(text)) + domains.update(f'native-{family}/v{number}' + for family, number in LITERAL_NATIVE_DOMAIN_RE.findall(text)) return namespaces, domains +def domain_tokens(line: str): + """(token, family key, version) of every hash-domain token on a line.""" + for match in DOMAIN_RE.finditer(line): + yield match, f'pineforge-{match.group(1)}', int(match.group(2)) + for match in NATIVE_DOMAIN_RE.finditer(line): + yield match, f'native-{match.group(1)}', int(match.group(2)) + + +def abi_numbers(root: Path) -> dict[str, int]: + """Rule 8: the ABI numbers the C headers #define.""" + found: dict[str, int] = {} + for rel in ('include/pineforge/pineforge.h', 'include/pineforge/native_c_api.h'): + path = root / rel + if path.is_file(): + for name, number in ABI_DEFINE_RE.findall(path.read_text(errors='replace')): + found[name] = int(number) + return found + + +def release_version(root: Path) -> str | None: + path = root / 'VERSION' + return path.read_text().strip() if path.is_file() else None + + +def stale_numbers(page: Path, text: str, abi: dict[str, int], + version: str | None) -> list[Offender]: + """Rules 8 and 9 over the page's prose.""" + out: list[Offender] = [] + for number, line in code_free(text): + verified = VERIFIED in line + for match in ABI_STATED_RE.finditer(line): + name, stated = match.group(1), int(match.group(2)) + live = abi.get(name) + if live is None or stated == live: + continue + clause = re.sub(r'', ' ', clause_around(line, match.start(), match.end())) + if stated > live: + if not re.search(r'\b(?:is now|current(?:ly)?)\b', clause, re.I): + continue # a forward reference + elif verified and HISTORY_RE.search(clause): + continue # history, said so and marked + out.append(Offender(page, number, 'stale-abi', line.strip(), + f'`{name}` is {live} in the header; the line states {stated}')) + if version is None: + continue + for match in RELEASE_RE.finditer(line): + if match.group(1) == version: + continue + clause = re.sub(r'', ' ', clause_around(line, match.start(), match.end())) + if not (CURRENT_RELEASE_RE.search(clause) and PINEFORGE_RE.search(clause)): + continue + if HISTORY_RE.search(clause) or re.search(r'\bsince\b', clause, re.I): + continue + out.append(Offender(page, number, 'stale-release', line.strip(), + f'{match.group(0)} is stated as the current release; ' + f'`VERSION` is {version}')) + return out + + def families(tokens: set[str], splitter) -> dict[str, set[str]]: out: dict[str, set[str]] = {} for token in tokens: @@ -387,8 +482,13 @@ def dead_paths(root: Path, page: Path, text: str) -> list[Offender]: for number, line in code_free(text): for match in CITED_PATH_RE.finditer(line): cited = match.group(1) + if cited.startswith(('src/', 'include/')) and not TREE_SOURCE_RE.match(cited): + continue # not a C/C++ file of this tree if cited.endswith('/') and (root / cited).is_dir(): continue + if re.search(r'\bgenerated\b', clause_around(line, match.start(), match.end()), + re.I): + continue # exists in a build or an install pattern = re.sub(r'<[^<>]*>', '*', cited) if '*' in pattern: if any(root.glob(pattern.rstrip('/'))): @@ -432,12 +532,11 @@ def verified_live_claims(page: Path, body: str, live_ns_families: dict[str, set[ if live and match.group(0) not in live: newest = max(int(t.rsplit('_v', 1)[1]) for t in live) tokens.append((match, version > newest)) - for match in DOMAIN_RE.finditer(line): - key = f'pineforge-{match.group(1)}' + for match, key, version in domain_tokens(line): live = live_domain_families.get(key) if live and match.group(0) not in live: newest = max(int(t.rsplit('/v', 1)[1]) for t in live) - tokens.append((match, int(match.group(2)) > newest)) + tokens.append((match, version > newest)) for match, forward in tokens: at, until = offsets[index] + match.start(), offsets[index] + match.end() clause = re.sub(r'', ' ', clause_around(body, at, until)) @@ -463,10 +562,12 @@ def check(root: Path) -> list[Offender]: published = set(pages(root, PUBLISHED_GLOBS)) anchors_cache: dict[Path, set[str]] = {} counts = derived_counts(root) + abi, version = abi_numbers(root), release_version(root) for page in pages(root, DOC_GLOBS): text = page.read_text(errors='replace') anchors_cache[page] = headings(text) + found.extend(stale_numbers(page, text, abi, version)) for number, line in code_free(text): verified = VERIFIED in line if page in published and LANE_RE.search(line): @@ -481,9 +582,8 @@ def check(root: Path) -> list[Offender]: page, number, 'stale-epoch', line.strip(), f'`{token}` is not live; the tree declares ' + ', '.join(sorted(live_ns_families[family])))) - for family, number_text in DOMAIN_RE.findall(line): - token = f'pineforge-{family}/v{number_text}' - key = f'pineforge-{family}' + for match, key, _ in domain_tokens(line): + token = match.group(0) if key in live_domain_families and token not in live_domain_families[key]: found.append(Offender( page, number, 'stale-epoch', line.strip(), @@ -565,7 +665,7 @@ def main(argv: list[str] | None = None) -> int: if not found: print('\nno lane labels, stale negatives, stale epochs, dead links, dead paths, ' - 'stale counts or markers on a stale present') + 'stale counts, stale ABI numbers or releases, or markers on a stale present') return 0 print(f'\n{len(found)} offenders:') for offender in found: diff --git a/scripts/test_check_doc_lint.py b/scripts/test_check_doc_lint.py index fe3be2af..7c458eaf 100644 --- a/scripts/test_check_doc_lint.py +++ b/scripts/test_check_doc_lint.py @@ -401,9 +401,11 @@ def test_a_clean_tree_says_so(self) -> None: code, out = t.run() self.assertEqual(code, 0, out) # expectation corrected: "no lane labels, stale negatives, stale epochs or dead - # links" -> the line below, because lane F2 added three rules to the summary. + # links" -> the line below, because lane F2 added three rules to the summary, + # and R5 lane H-DOCGATES the ABI-number and release rules. self.assertIn('no lane labels, stale negatives, stale epochs, dead links, dead ' - 'paths, stale counts or markers on a stale present', out) + 'paths, stale counts, stale ABI numbers or releases, or markers on ' + 'a stale present', out) def test_cited_paths_that_exist(self) -> None: body = ('Pinned by `tests/test_native_order_core.cpp`; the hosts live in ' @@ -473,6 +475,85 @@ def test_a_marker_on_a_forward_epoch(self) -> None: self.assertEqual(code, 0, out) + +# R5 lane H-DOCGATES: a tree that declares the ABI numbers, the release and +# the kernel's `native-*` hash domains the new families read. +H_FILES = { + 'include__pineforge__pineforge.h': '#pragma once\n#define PF_ABI_VERSION 4\n', + 'include__pineforge__native_c_api.h': '#pragma once\n#define PF_NATIVE_API_VERSION 1\n', + 'include__pineforge__market_driver.hpp': + '#pragma once\nconstexpr const char* kDriver = "native-driver/v5";\n' + 'constexpr const char* kConsumer = "native-consumer/v9";\n', + 'VERSION': '0.14.0\n', +} + + +def h_tree(page: str, **extra: str): + return tree(**H_FILES, **{PAGE: page}, **extra) + + +class MustFailH(unittest.TestCase): + """AUDIT4-opus docs-a GAP-6: the lint probes P19, P20 and P29-P32 passed.""" + + def assert_offends(self, page: str, rule: str, **extra: str) -> None: + with h_tree(page, **extra) as t: + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn(f'[{rule}]', out) + + def test_p19_a_dead_src_path(self) -> None: + self.assert_offends('The mechanism is `src/does_not_exist_probe.cpp`.\n', 'dead-path') + + def test_p20_a_dead_include_path(self) -> None: + self.assert_offends('The header is `include/pineforge/does_not_exist_probe.hpp`.\n', + 'dead-path') + + def test_p29_a_stale_continuation_domain(self) -> None: + self.assert_offends('The continuation domain is `native-consumer/v8`.\n', 'stale-epoch') + + def test_p30_a_stale_driver_domain(self) -> None: + self.assert_offends('The driver domain is `native-driver/v4`.\n', 'stale-epoch') + + def test_p31_a_stale_abi_number(self) -> None: + self.assert_offends('`PF_ABI_VERSION` is 3.\n', 'stale-abi') + self.assert_offends('The C API reports PF_NATIVE_API_VERSION == 0 here.\n', 'stale-abi') + + def test_p32_a_stale_release(self) -> None: + self.assert_offends('Install PineForge 0.13.1, the current release.\n', 'stale-release') + + def test_a_marker_on_a_native_domain_stated_as_now(self) -> None: + self.assert_offends('The consumer now hashes under `native-consumer/v8`. ' + '\n', 'verified-stale-epoch') + + def test_a_forward_abi_number_stated_as_current(self) -> None: + self.assert_offends('The current `PF_ABI_VERSION` is now 5.\n', 'stale-abi') + + +class MustNotFireH(unittest.TestCase): + def assert_clean(self, page: str, **extra: str) -> None: + with h_tree(page, **extra) as t: + code, out = t.run() + self.assertEqual(code, 0, out) + + def test_live_numbers_domains_and_paths(self) -> None: + self.assert_clean('`PF_ABI_VERSION` is 4 and `PF_NATIVE_API_VERSION` is 1; the ' + 'continuation domain is `native-consumer/v9` and the driver\'s ' + '`native-driver/v5`. Install PineForge 0.14.0, the current release. ' + 'The header is `include/pineforge/pineforge.h`.\n') + + def test_forward_and_historical_numbers(self) -> None: + self.assert_clean('The alias is removed at `PF_ABI_VERSION` 5. The layout was frozen ' + 'at `PF_ABI_VERSION` 3 until the append. \n' + 'The codegen tracks it since v0.6.4.\n') + + def test_git_object_paths_generated_files_and_foreign_crates(self) -> None: + self.assert_clean('The retired rule is `git show ab9714be:src/source/pine_risk.cpp`. ' + 'The build writes `include/pineforge/version.h`, generated version ' + 'macros.\n\n## src/main.rs\n') + + def test_a_native_domain_recorded_as_history(self) -> None: + self.assert_clean('The baseline stood at `native-consumer/v7`. \n') + class Derivation(unittest.TestCase): """The live epoch set and the stated counts come from the tree, never from a list.""" From e7dfe7c3b650e5896d27281e1fe87df83f8396c1 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 03:47:11 +0800 Subject: [PATCH 13/83] Tests: one random draw per statement at the fifteen sites the compiler ordered, and a lint that keeps the class out (R5 lane H-DOCGATES, X17) AUDIT4-opus perf N-2: XPLAT1's bug class (e8bc3237) came back. C++ leaves a call's argument order unspecified and a binary operator's operands unsequenced; GCC on x86-64 draws right to left, AppleClang left to right, so ten call sites in five test files (P7's and L2's fixtures, TA1's tests) built a different random battery on each compiler. The lint finds five more, in unsequenced `*` operands (test_adapter_live_state_equivalence, test_native_direct_mutation x2, test_native_handle_stable_replace x2). Each of the fifteen sites now takes one draw per statement into a named local, in the order the arguments are written (AppleClang's), so no Mac output moves: - the 15 affected test executables, Release, AppleClang 17 (arm64): stdout before vs after identical in every non-timing byte (15/15); - the same 15 in Docker xplat1-gcc13:amd64 (GCC 13, x86-64): before, the printed batteries differed from the Mac's in 7 tests (test_adapter_purge_index "14844 commands" vs 14575, test_native_settlement_carry, _fused_settlement, _in_place_reads, _precommit_hook, _quiet_point, _direct_mutation); after, x86-64 prints the Mac's bytes in all 15. Every test passes on both, before and after. So the only output that moves is x86-64's random battery, onto the one the Mac already runs; no pinned value, hash or fixture moves (nothing in these tests pins a draw-dependent count, or x86 CI would have failed before). scripts/check_rng_draw_order.py (binding preflight stages rng-draw-order and rng-draw-order-tests): a draw is a call that advances a std:: engine or a class whose members apply a PRNG step, found transitively through drawing helpers and lambdas; it reports ARG (two draw-bearing arguments of one call), OPERAND (two draw-bearing operands of an unsequenced operator) and BYVALUE (a generator parameter copied while other arguments are evaluated), and leaves the sequenced forms alone (one per statement or declarator, braced-init lists, && || ?: and the comma, C++17's << >> [] = orders, lambda bodies). 21bdc2e8's tests: 15 findings; this tree: 0; self-test 5 cases (a must-fail fixture of XPLAT1's three shapes and each kind, a must-pass fixture of every sequenced form). CONTRIBUTING.md lists the gate. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 7536bf01e0a4cbfdb32141b9065891048f4366a6) --- CONTRIBUTING.md | 1 + scripts/check_rng_draw_order.py | 1430 +++++++++++++++++ scripts/ci_preflight.py | 7 + scripts/test_check_rng_draw_order.py | 235 +++ tests/adapter_lookup_index_fixture.hpp | 23 +- tests/native_fused_settlement_fixture.hpp | 10 +- tests/test_adapter_live_state_equivalence.cpp | 6 +- tests/test_native_direct_mutation.cpp | 7 +- tests/test_native_handle_stable_replace.cpp | 7 +- tests/test_ta_kc_basis.cpp | 11 +- tests/test_ta_kc_range.cpp | 7 +- tests/test_ta_pivot_point_levels.cpp | 13 +- 12 files changed, 1735 insertions(+), 22 deletions(-) create mode 100644 scripts/check_rng_draw_order.py create mode 100644 scripts/test_check_rng_draw_order.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed86fd5a..181f024d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,6 +141,7 @@ What each gate refuses: | `check_native_cpp_versions.py`, `check_aggregate_cpp_versions.py` | an internal C++ epoch moved without its consumers | | `check_adapter_spec_shadowing.py` | the adapter setting a kernel field it is ruled not to set | | `check_twin_parity.py` | a frozen test assertion rewritten instead of a behaviour change being argued | +| `check_rng_draw_order.py` | two random draws in one call's arguments or one operator's operands, whose order the compiler picks (x86-64 GCC and AppleClang would test different batteries) | | `check_doc_anchors.py` | a `file:line` citation that no longer points at the symbol it claims | | `check_doc_lint.py` | a stale epoch, a roadmap label or a "there is no … yet" claim the tree has falsified | | `check_pine_to_native_coverage.py` | a Pine builtin with no row on the migration page | diff --git a/scripts/check_rng_draw_order.py b/scripts/check_rng_draw_order.py new file mode 100644 index 00000000..2dd18fc5 --- /dev/null +++ b/scripts/check_rng_draw_order.py @@ -0,0 +1,1430 @@ +#!/usr/bin/env python3 +"""Refuse two random draws whose relative order C++17 leaves to the compiler. + +Why this exists +--------------- +C++ leaves the order in which a call's arguments are evaluated unspecified, +and the two operands of most binary operators unsequenced. GCC on x86-64 +evaluates a call's arguments right to left; AppleClang, and GCC on aarch64, +left to right (``take(draw(1), draw(2), draw(3))`` prints ``321`` with GCC 13.3 +on x86-64 and ``123`` with AppleClang 17). A test that takes two draws of one +generator inside one argument list therefore builds a different random +battery on each compiler. XPLAT1 (e8bc3237) met exactly that: +test_adapter_quiet_bar matched 92 of its 272 pins on x86-64 and all 272 on +arm64, because three call sites of its random scenario drew two to four values +inside one call's arguments. The fix was one draw per statement, taken left +to right; the pins were harvested with that order. Nothing kept the class +out, and ten call sites came back. This checker is that guard. + +What counts as a draw +--------------------- +A *generator* is a ``std::`` random engine (``mt19937``, ``mt19937_64``, +``minstd_rand`` ...), or a class the scanned code defines whose member +functions advance a data member with a PRNG step (``s ^= s << 13``, +``s ^= s >> 12``, ``state += 0x9e3779b97f4a7c15``, ``s = s * K + C``) -- the +tests' ``Rng``, ``Random`` and ``SplitMix``. A *draw* is a call expression +that advances one: + +- a call on a generator object: ``rng()``, ``(*rng)()``, ``rng.next()``, + ``r.unit()``, ``s.below(3)``, ``rng_->percent(50)``; +- a call of a *drawer*: a function, member function or named lambda whose + body draws, found transitively over the translation unit (``long_id()``, + ``units()``, ``opening(...)``, ``pick(rng, ...)``, ``walk(r, ...)``), or + whose body applies a PRNG step to state it does not declare (a lambda over a + captured ``state``, a function over a global ``mix_state``); +- a call that receives a generator as an argument (``dist(rng)``, + ``random_text(rng, n)``, ``std::shuffle(b, e, rng)``): the callee may draw. + +The translation unit is the file plus the quoted headers it includes, +transitively; a header is checked under every unit that includes it. + +What is reported +---------------- +``ARG`` a call (function, member function, constructor, macro) with two + or more arguments that each contain a draw. A generator passed + bare (``walk(r, 40 + r.below(20))``) is not a draw of that + argument: the callee draws only after every argument is + evaluated, so the order is fixed. +``OPERAND`` two operands of one unsequenced binary operator + (``+ - * / % & | ^ < > <= >= == !=``) that each contain a draw, + e.g. ``(r.percent(50) ? 1 : -1) * r.between(4, 16)``. +``BYVALUE`` a function or lambda parameter of generator type taken by value: + its copy would be made while the other arguments are evaluated. + +What is sequenced, and never reported +------------------------------------- +- one draw per statement, and one per declarator + (``const int a = r.below(3), b = r.below(4);``); +- the elements of a braced-init-list, ``{a(), b()}`` and ``T{a(), b()}``, + which are evaluated left to right even when a constructor is called + ([dcl.init.list]/4); +- ``&&``, ``||``, ``?:`` and the built-in comma (left before right); +- C++17's P0145 orders: ``<<`` and ``>>`` (left before right), ``a[b]``, the + postfix of ``a.f(b)`` before ``b``, and ``=`` / compound assignment (right + before left); +- a nested call's arguments before its body + (``rng.quarter(rng.percent(3) ? -4 : 0, 12)``); +- draws inside a lambda body: they run when the lambda is called. + +Limits (by design) +------------------ +Draws hidden behind a macro, a virtual callback or a function defined in +another translation unit are not seen, and neither are two draws of two +different generators (their order does not change either battery). Preprocessor +lines are blanked before scanning. + +Usage +----- + check_rng_draw_order.py # scan the default directories + check_rng_draw_order.py PATH... # files or directories + check_rng_draw_order.py --root DIR # another checkout + check_rng_draw_order.py --vocabulary # print what each unit counts as a draw + +Exit 0 when nothing is found, 1 on findings, 2 on a usage error. +""" +from __future__ import annotations + +import argparse +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Iterable + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_DIRS = ('tests', 'examples', 'benchmarks', 'tools', 'src', 'include', 'runner', + 'tutorial') +# Frozen previous-epoch header snapshots: compiled only as ABI witnesses, never +# as tests, and never holding a generator. +EXCLUDED_PREFIXES = ('tests/fixtures/',) +SUFFIXES = frozenset(('.cpp', '.cc', '.cxx', '.hpp', '.h', '.hh', '.hxx', '.inc', '.ipp')) + +STD_ENGINES = frozenset(( + 'mt19937', 'mt19937_64', 'minstd_rand', 'minstd_rand0', 'ranlux24', 'ranlux48', + 'ranlux24_base', 'ranlux48_base', 'knuth_b', 'default_random_engine', 'random_device', + 'linear_congruential_engine', 'mersenne_twister_engine', 'subtract_with_carry_engine', + 'discard_block_engine', 'independent_bits_engine', 'shuffle_order_engine')) +# A unit that spells none of these cannot draw; it is skipped unread. +HINT = re.compile( + r'\b(?:' + '|'.join(sorted(STD_ENGINES)) + r')\b' + r'|(\w+)\s*\^=\s*\(?\s*\1\s*(?:<<|>>)' + r'|\+=\s*0x9[eE]3779[bB]97[fF]4[aA]7[cC]15' + r'|(\w+)\s*=\s*\2\s*\*\s*\d{6,}') + +KEYWORDS = frozenset(( + 'alignas', 'alignof', 'and', 'asm', 'auto', 'bool', 'break', 'case', 'catch', 'char', + 'class', 'const', 'const_cast', 'constexpr', 'continue', 'decltype', 'default', 'delete', + 'do', 'double', 'dynamic_cast', 'else', 'enum', 'explicit', 'extern', 'false', 'float', + 'for', 'friend', 'goto', 'if', 'inline', 'int', 'long', 'mutable', 'namespace', 'new', + 'noexcept', 'not', 'nullptr', 'operator', 'or', 'private', 'protected', 'public', + 'reinterpret_cast', 'return', 'short', 'signed', 'sizeof', 'static', 'static_assert', + 'static_cast', 'struct', 'switch', 'template', 'this', 'throw', 'true', 'try', + 'typedef', 'typeid', 'typename', 'union', 'unsigned', 'using', 'virtual', 'void', + 'volatile', 'while', 'override', 'final', 'co_await', 'co_return', 'co_yield')) +VALUE_WORDS = frozenset(('this', 'true', 'false', 'nullptr')) +# A '(' after one of these is a condition or an operand, never a call. +CONTROL_WORDS = frozenset(( + 'if', 'while', 'switch', 'for', 'catch', 'sizeof', 'alignof', 'alignas', 'decltype', + 'noexcept', 'static_assert', 'typeid', 'return', 'throw', 'case', 'co_return', + 'co_yield', 'co_await', 'defined', 'and', 'or', 'not', 'else', 'do', 'new', 'delete')) +# Keywords that end one piece of an expression region and start the next. +SEPARATOR_WORDS = frozenset(( + 'return', 'if', 'else', 'while', 'for', 'do', 'switch', 'case', 'default', 'throw', + 'goto', 'break', 'continue', 'co_return', 'co_yield', 'co_await', 'and', 'or', 'try', + 'catch')) +# Sequenced (or statement-level) punctuators: a piece of a region ends here. +SEPARATOR_OPS = frozenset(( + ';', ',', '?', ':', '&&', '||', '=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', + '<<=', '>>=', '<<', '>>', '->*', '.*')) +UNSEQUENCED_BINARY = frozenset(( + '+', '-', '*', '/', '%', '&', '|', '^', '<', '>', '<=', '>=', '==', '!=', '<=>')) +DECL_TRAILERS = frozenset(('const', 'volatile', 'override', 'final', 'mutable', 'constexpr', + '&', '&&', 'noexcept', 'throw', 'try')) +HEAD_WORDS = frozenset(('struct', 'class', 'union', 'enum', 'namespace', 'extern', 'concept')) + +TOKEN_RE = re.compile(r''' + (?P(?:u8|u|U|L)?R?"[^"]*"|(?:u8|u|U|L)?'[^']*') + | (?P[A-Za-z_]\w*) + | (?P\.?\d(?:[\w.']|[eEpP][+-])*) + | (?P\.\.\.|->\*|<=>|<<=|>>=|::|->|\+\+|--|<<|<=|>=|==|!=|&&|\|\||\+=|-=|\*=|/=|%= + |&=|\|=|\^=|\.\*|>>|[{}\[\]()<>;:,.?~!%^&*+\-=/|]) +''', re.X) +INCLUDE_RE = re.compile(r'^[ \t]*#[ \t]*include[ \t]*"([^"\n]+)"', re.M) +BIG_LITERAL = re.compile(r'^(?:0[xX][0-9a-fA-F\']{8,}|\d[\d\']{5,})[uUlL]*$') + + +# --------------------------------------------------------------------------- +# Lexing +# --------------------------------------------------------------------------- + +def strip_source(text: str) -> str: + """Blank comments, the contents of string and character literals, and + preprocessor directives, keeping every offset and newline.""" + out = list(text) + n = len(text) + + def blank(a: int, b: int) -> None: + for k in range(a, min(b, n)): + if out[k] != '\n': + out[k] = ' ' + + i = 0 + line_start = True + while i < n: + c = text[i] + if c == '\n': + line_start = True + i += 1 + continue + if c in ' \t\r\f\v': + i += 1 + continue + if line_start and c == '#': + j = i + while j < n: + if text.startswith('/*', j): + end = text.find('*/', j + 2) + j = n if end < 0 else end + 2 + continue + if text[j] == '\n': + back = j - 1 + while back > i and text[back] in ' \t\r': + back -= 1 + if text[back] == '\\': + j += 1 + continue + break + j += 1 + blank(i, j) + i = j + continue + line_start = False + if text.startswith('//', i): + j = text.find('\n', i) + j = n if j < 0 else j + blank(i, j) + i = j + elif text.startswith('/*', i): + j = text.find('*/', i + 2) + j = n if j < 0 else j + 2 + blank(i, j) + i = j + elif c == '"': + k = i - 1 + while k >= 0 and (text[k].isalnum() or text[k] == '_'): + k -= 1 + prefix = text[k + 1:i] + if prefix.endswith('R') and prefix in ('R', 'u8R', 'uR', 'UR', 'LR'): + m = re.match(r'"([^()\\\s]{0,16})\(', text[i:]) + if m: + closing = ')' + m.group(1) + '"' + end = text.find(closing, i + m.end()) + end = n if end < 0 else end + len(closing) + blank(i + 1, end - 1) + i = end + continue + j = i + 1 + while j < n and text[j] != '"' and text[j] != '\n': + j += 2 if text[j] == '\\' else 1 + blank(i + 1, j) + i = j + 1 + elif c == "'": + k = i - 1 + while k >= 0 and (text[k].isalnum() or text[k] in "_'."): + k -= 1 + run = text[k + 1:i] + if run and run[0].isdigit(): # a digit separator: 1'000'000 + i += 1 + continue + j = i + 1 + while j < n and text[j] != "'" and text[j] != '\n': + j += 2 if text[j] == '\\' else 1 + blank(i + 1, j) + i = j + 1 + else: + i += 1 + return ''.join(out) + + +@dataclass +class Tok: + kind: str # 'id', 'num', 'str', 'op' + text: str + line: int + joined: bool = False # the second '>' of a split '>>' + + +def tokenize(clean: str) -> list[Tok]: + toks: list[Tok] = [] + line = 1 + last = 0 + for m in TOKEN_RE.finditer(clean): + line += clean.count('\n', last, m.start()) + last = m.start() + kind = m.lastgroup + text = m.group(0) + if kind == 'op' and text == '>>': + toks.append(Tok('op', '>', line)) + toks.append(Tok('op', '>', line, joined=True)) + continue + toks.append(Tok(kind, text, line)) + return toks + + +# --------------------------------------------------------------------------- +# Structure: brackets, template arguments, group kinds +# --------------------------------------------------------------------------- + +CALL, GROUP, CONTROL, PARAMS, BLOCK, INIT, SUBSCRIPT, CAPTURE, NAME = ( + 'call', 'group', 'control', 'params', 'block', 'init', 'subscript', 'capture', 'name') + + +class Structure: + """Bracket pairs and what each group is, for one file's tokens.""" + + def __init__(self, toks: list[Tok]): + self.toks = toks + self.close: dict[int, int] = {} + self.open_of: dict[int, int] = {} + self.template_open: dict[int, int] = {} + self.template_close: set[int] = set() + self.kind: dict[int, str] = {} + self.parent: dict[int, int] = {} + self._templates() + self._brackets() + for i in sorted(self.close): + self.kind[i] = self._classify(i) + + # -- template argument lists ------------------------------------------ + def _templates(self) -> None: + toks = self.toks + for i, t in enumerate(toks): + if t.text != '<' or i == 0 or i in self.template_close: + continue + p = toks[i - 1] + if p.kind != 'id' or (p.text in KEYWORDS and p.text not in ('template',)): + if not (p.text == '>' and (i - 1) in self.template_close): + continue + j = self._template_end(i) + if j is not None: + self.template_open[i] = j + self.template_close.add(j) + + def _template_end(self, i: int) -> int | None: + toks = self.toks + depth = 1 + j = i + 1 + while j < len(toks) and j - i < 80: + t = toks[j] + x = t.text + if x == '<': + if toks[j - 1].kind != 'id': + return None + depth += 1 + elif x == '>': + depth -= 1 + if depth == 0: + return j + elif t.kind in ('id', 'num'): + if t.kind == 'id' and x in ('return', 'if', 'while', 'for', 'case', 'new', + 'delete', 'throw', 'else'): + return None + elif x in ('::', ',', '*', '&', '...'): + pass + elif x == '(': + # A function type, std::function: its own parens, + # holding type words only. + k = j + 1 + while k < len(toks) and toks[k].text != ')': + if toks[k].kind not in ('id',) and toks[k].text not in ( + '::', ',', '*', '&', '<', '>', '...'): + return None + k += 1 + j = k + else: + return None + j += 1 + return None + + # -- (), [], {} pairs ------------------------------------------------ + def _brackets(self) -> None: + stack: list[int] = [] + pairs = {')': '(', ']': '[', '}': '{'} + for i, t in enumerate(self.toks): + x = t.text + if t.kind != 'op': + continue + if x in '([{' and len(x) == 1: + if stack: + self.parent[i] = stack[-1] + stack.append(i) + elif x in pairs and len(x) == 1: + while stack and self.toks[stack[-1]].text != pairs[x]: + stack.pop() # tolerate an unbalanced macro spelling + if stack: + o = stack.pop() + self.close[o] = i + self.open_of[i] = o + + # -- helpers ----------------------------------------------------------- + def prev(self, i: int) -> Tok | None: + return self.toks[i - 1] if i > 0 else None + + def operand_end(self, i: int) -> bool: + """Whether toks[i] can end an operand (so a following '-' is binary).""" + if i < 0: + return False + t = self.toks[i] + if t.kind in ('num', 'str'): + return True + if t.kind == 'id': + return t.text not in KEYWORDS or t.text in VALUE_WORDS + if t.text in (')', ']', '}'): + return True + if t.text == '>' and i in self.template_close: + return True + if t.text in ('++', '--'): + return self.operand_end(i - 1) + return False + + def is_lambda_capture(self, i: int) -> bool: + if i + 1 < len(self.toks) and self.toks[i + 1].text == '[': + return False # [[attribute]] + return not self.operand_end(i - 1) + + def after_params(self, close_idx: int) -> int: + """Index of the first token after a parameter list's trailers.""" + toks = self.toks + j = close_idx + 1 + while j < len(toks): + x = toks[j].text + if x in DECL_TRAILERS: + if x in ('noexcept', 'throw') and j + 1 < len(toks) and toks[j + 1].text == '(': + j = self.close.get(j + 1, j + 1) + 1 + continue + j += 1 + continue + if x == '->': + j += 1 + while j < len(toks) and toks[j].text not in ('{', ';', '=') and not ( + toks[j].text == ',' or toks[j].text == ')'): + if toks[j].text == '<' and j in self.template_open: + j = self.template_open[j] + j += 1 + continue + break + return j + + def _classify(self, i: int) -> str: + toks = self.toks + x = toks[i].text + p = self.prev(i) + if x == '(': + if p is None: + return GROUP + if p.kind == 'id' and p.text in CONTROL_WORDS: + return CONTROL + if p.kind == 'id' and p.text == 'operator': + return NAME + callee_like = ( + (p.kind == 'id' and (p.text not in KEYWORDS or p.text in ( + 'this', 'static_cast', 'const_cast', 'reinterpret_cast', 'dynamic_cast'))) + or p.text in (')', ']') + or (p.text == '>' and (i - 1) in self.template_close) + or (p.text == '}' and self._brace_is_expression(i - 1))) + if not callee_like: + return GROUP + if p.text == ']': + o = self.open_of.get(i - 1) + if o is not None and self.is_lambda_capture(o): + return PARAMS + j = self.after_params(self.close[i]) + nxt = toks[j].text if j < len(toks) else '' + if nxt == '{': + return PARAMS + if nxt == ':' and j + 1 < len(toks) and toks[j + 1].kind == 'id' \ + and toks[j + 1].text not in KEYWORDS: + return PARAMS # a constructor's mem-initializer list follows + return CALL + if x == '[': + if self.is_lambda_capture(i): + if i + 1 < len(toks) and toks[i + 1].text == '[': + return NAME # attribute + return CAPTURE + return SUBSCRIPT + # '{' + return INIT if self._brace_is_init(i) else BLOCK + + def _brace_is_expression(self, close_idx: int) -> bool: + o = self.open_of.get(close_idx) + if o is None: + return False + if self._brace_is_init(o): + return True + # a lambda body: [..](..) {..} or [..] {..} + p = o - 1 + while p >= 0 and self.toks[p].text in DECL_TRAILERS: + p -= 1 + if p >= 0 and self.toks[p].text == ')': + q = self.open_of.get(p) + if q is not None and q > 0 and self.toks[q - 1].text == ']': + return True + if p >= 0 and self.toks[p].text == ']': + q = self.open_of.get(p) + return q is not None and self.is_lambda_capture(q) + return False + + def _brace_is_init(self, i: int) -> bool: + toks = self.toks + p = self.prev(i) + if p is None: + return False + x = p.text + if x in ('=', 'return', ',', '(', '[', '?', '{') and x != '{': + return True + if x == '{': + # {{...}} nested init inside an init; a block inside a block + par = self.parent.get(i) + return par is not None and self._brace_is_init(par) + if x in (')', ';', '}', 'else', 'do', 'try', 'mutable', 'noexcept', 'const', + 'override', 'final', 'volatile', '&', '&&'): + return False + if x == ']': + o = self.open_of.get(i - 1) + return not (o is not None and self.is_lambda_capture(o)) + if x == ':': + par = self.parent.get(i) + return par is not None and toks[par].text == '(' # range-for over a list + if p.kind == 'id' or (x == '>' and (i - 1) in self.template_close): + if p.kind == 'id' and x in KEYWORDS and x not in HEAD_WORDS: + return x not in ('else', 'do', 'try') + # scan the statement head for struct/class/namespace/enum or a + # trailing return type + k = i - 1 + saw_arrow = False + while k >= 0: + t = toks[k] + if t.text in (';', '{', '}') and k not in self.close and k not in self.open_of: + break + if t.text in (')', ']', '}') and k in self.open_of: + k = self.open_of[k] - 1 + continue + if t.text in ('(', '[', '{') and k in self.close: + break # the head is inside an enclosing group + if t.text == '>' and k in self.template_close: + # skip back over the template argument list + for o, c in self.template_open.items(): + if c == k: + k = o - 1 + break + else: + k -= 1 + continue + if t.kind == 'id' and t.text in HEAD_WORDS: + return False + if t.text == '->': + saw_arrow = True + if t.text == ':' and k > 0 and toks[k - 1].kind == 'id' and \ + toks[k - 1].text in ('public', 'private', 'protected'): + break + k -= 1 + return not saw_arrow + return True + + +# --------------------------------------------------------------------------- +# Per-file facts +# --------------------------------------------------------------------------- + +@dataclass +class Function: + name: str + body: tuple[int, int] # token range inside the braces + locals: set[str] # parameters by value and local variables + owner: str | None # enclosing class, if any + steps: set[str] = field(default_factory=set) # non-locals a PRNG step advances + mixes: set[str] = field(default_factory=set) # non-locals updated from an input + returns_value: bool = False + qualifier: str | None = None # Class of an out-of-line Class::name definition + + @property + def stepper(self) -> bool: + """Advances state it does not own, from that state alone, and returns + a value: a generator's next(), a lambda over a captured seed. A hash + fold (h = (h ^ v) * K) mixes an input and is not one.""" + return bool(self.steps - self.mixes) and self.returns_value + + +@dataclass +class ClassDef: + name: str + body: tuple[int, int] + has_base: bool = False + bases: set[str] = field(default_factory=set) + methods: set[str] = field(default_factory=set) + + +@dataclass +class FileFacts: + path: Path + rel: str + toks: list[Tok] + st: Structure + includes: list[Path] + functions: list[Function] + classes: list[ClassDef] + declared: list[tuple[str, str, int]] # (type name, variable name, token index) + hinted: bool + + +UPDATE_OPS = frozenset(('^=', '+=', '-=', '*=', '|=', '&=', '=')) +GEN_NAME_RE = re.compile(r'(?i)rng|random|splitmix|xorshift|xoshiro|xoroshiro|pcg|lcg|prng') + + +def body_updates(toks: list[Tok], st: Structure, lo: int, hi: int) -> tuple[set[str], set[str]]: + """(stepped, mixed): identifiers a PRNG step advances from their own value + alone (s ^= s << 13; s ^= s >> 7; state += 0x9e37...; s = s * K + C), and + identifiers updated from anything else (h ^= byte; h = (h ^ v) * K).""" + stepped: set[str] = set() + mixed: set[str] = set() + for k in range(lo, hi - 2): + t = toks[k] + if t.kind != 'id' or t.text in KEYWORDS: + continue + if k > lo and toks[k - 1].text in ('.', '::', '->') and not ( + toks[k - 1].text == '->' and toks[k - 2].text == 'this'): + continue + op = toks[k + 1].text + if op not in UPDATE_OPS: + continue + if op == '=' and k > lo and (toks[k - 1].kind == 'id' and toks[k - 1].text not in ( + 'return',) or toks[k - 1].text in ('*', '&', '>')): + continue # a declaration's initializer, not an update + rhs: list[Tok] = [] + j = k + 2 + depth = 0 + while j < hi: + x = toks[j].text + if x == '<' and j in st.template_open: + j = st.template_open[j] + 1 + continue + if x in ('(', '[', '{'): + depth += 1 + elif x in (')', ']', '}'): + if depth == 0: + break + depth -= 1 + elif x in (';', ',') and depth == 0: + break + rhs.append(toks[j]) + j += 1 + name = t.text + ids = {r.text for n, r in enumerate(rhs) + if r.kind == 'id' and r.text not in KEYWORDS + and not (n + 1 < len(rhs) and rhs[n + 1].text == '::')} + texts = [r.text for r in rhs] + shift = '<<' in texts or any( + texts[m] == '>' and m + 1 < len(texts) and rhs[m + 1].joined + for m in range(len(texts))) + big = any(r.kind == 'num' and BIG_LITERAL.match(r.text) for r in rhs) + self_only = ids <= {name} + prng_like = ((op == '^=' and name in ids and shift) + or (op == '+=' and big and len(rhs) == 1) + or (op == '*=' and big and len(rhs) == 1) + or (op == '=' and name in ids and big and '*' in texts)) + if self_only and prng_like: + stepped.add(name) + elif ids - {name} or (op == '=' and name not in ids): + mixed.add(name) + return stepped, mixed + + +def body_locals(toks: list[Tok], st: Structure, params: tuple[int, int] | None, + lo: int, hi: int) -> set[str]: + """Names a function owns: its by-value parameters and its local variables. + A reference parameter is the caller's state (splitmix(std::uint64_t& s)).""" + out: set[str] = set() + + def scan(a: int, b: int, is_params: bool) -> None: + for k in range(a, b): + t = toks[k] + if t.kind != 'id' or t.text in KEYWORDS: + continue + if k + 1 >= len(toks): + continue + nxt = toks[k + 1].text + p = toks[k - 1] if k > 0 else None + if p is None: + continue + typeish = (p.kind == 'id' and (p.text not in KEYWORDS or p.text in ( + 'auto', 'int', 'long', 'double', 'float', 'bool', 'char', 'unsigned', + 'signed', 'short', 'const'))) or p.text in ('*', '&', '&&') or ( + p.text == '>' and (k - 1) in st.template_close) + if not typeish: + continue + if is_params and p.text in ('&', '&&'): + continue + if nxt in ('=', ';', ',', '(', '{', '[', ')', ':') or (is_params and nxt == ')'): + out.add(t.text) + + if params: + scan(params[0], params[1], True) + scan(lo, hi, False) + return out + + +def base_names(toks: list[Tok], st: Structure, lo: int, hi: int) -> set[str]: + """The base classes a class head toks[lo:hi] names (last component of each).""" + out: set[str] = set() + k = lo + while k < hi and toks[k].text != ':': + k += 1 + last = None + k += 1 + while k <= hi: + t = toks[k] if k < hi else Tok('op', ',', 0) + if t.text == '<' and k in st.template_open: + k = st.template_open[k] + 1 + continue + if t.text == ',': + if last: + out.add(last) + last = None + elif t.kind == 'id' and t.text not in KEYWORDS: + last = t.text + k += 1 + return out + + +def finish_function(fn: Function, toks: list[Tok], st: Structure) -> None: + lo, hi = fn.body + stepped, mixed = body_updates(toks, st, lo, hi) + fn.steps = stepped - fn.locals + fn.mixes = mixed - fn.locals + fn.returns_value = any(toks[k].text == 'return' and toks[k + 1].text != ';' + for k in range(lo, hi - 1)) + + +def collect_facts(path: Path, rel: str, raw: str) -> FileFacts: + clean = strip_source(raw) + toks = tokenize(clean) + st = Structure(toks) + includes = [path.parent / m.group(1) for m in INCLUDE_RE.finditer(raw)] + functions: list[Function] = [] + classes: list[ClassDef] = [] + declared: list[tuple[str, str, int]] = [] + + # class / struct bodies + for o, c in st.close.items(): + if toks[o].text != '{' or st.kind.get(o) != BLOCK: + continue + k = o - 1 + name = None + while k >= 0 and toks[k].text not in (';', '{', '}'): + if toks[k].kind == 'id' and toks[k].text in ('struct', 'class', 'union'): + if k + 1 < o and toks[k + 1].kind == 'id': + name = toks[k + 1].text + break + if toks[k].text in (')', ']') and k in st.open_of: + k = st.open_of[k] + k -= 1 + if name: + texts = [t.text for t in toks[k:o]] + classes.append(ClassDef(name, (o + 1, c), has_base=':' in texts, + bases=base_names(toks, st, k, o))) + + def owner_of(idx: int) -> str | None: + best = None + for cd in classes: + if cd.body[0] <= idx < cd.body[1]: + if best is None or cd.body[0] > best.body[0]: + best = cd + return best.name if best else None + + # function definitions and named lambdas + for o, kind in st.kind.items(): + if kind != PARAMS: + continue + c = st.close[o] + j = st.after_params(c) + if j < len(toks) and toks[j].text == ':': + # constructor: skip the mem-initializer list to the body + k = j + 1 + while k < len(toks): + if toks[k].text == '{' and st.kind.get(k) == BLOCK: + break + if toks[k].text in ('(', '{') and k in st.close: + k = st.close[k] + 1 + continue + k += 1 + j = k + if j >= len(toks) or toks[j].text != '{' or j not in st.close: + continue + body = (j + 1, st.close[j]) + name = None + p = o - 1 + if toks[p].text == ']': + # lambda: auto name = [caps](params) {...} + q = st.open_of.get(p) + if q is not None and q >= 2 and toks[q - 1].text == '=' and toks[q - 2].kind == 'id': + name = toks[q - 2].text + elif toks[p].kind == 'id': + name = toks[p].text + elif toks[p].text == ')' and p in st.open_of and toks[st.open_of[p] - 1].text == 'operator': + name = 'operator()' + if not name: + name = '' + fn = Function(name, body, body_locals(toks, st, (o + 1, c), body[0], body[1]), + owner_of(o)) + if toks[p].kind == 'id' and p >= 2 and toks[p - 1].text == '::' \ + and toks[p - 2].kind == 'id': + fn.qualifier = toks[p - 2].text + finish_function(fn, toks, st) + functions.append(fn) + # lambdas without a parameter list: auto name = [caps] {...} + for o, kind in st.kind.items(): + if kind != CAPTURE: + continue + c = st.close[o] + j = st.after_params(c) + if j < len(toks) and toks[j].text == '{' and j in st.close: + name = None + if o >= 2 and toks[o - 1].text == '=' and toks[o - 2].kind == 'id': + name = toks[o - 2].text + body = (j + 1, st.close[j]) + fn = Function(name or '', body, + body_locals(toks, st, None, body[0], body[1]), owner_of(o)) + finish_function(fn, toks, st) + functions.append(fn) + for fn in functions: + for cd in classes: + if fn.owner == cd.name: + cd.methods.add(fn.name) + + # declarations: TYPE [&*] NAME followed by ( { = ; , ) [ + for k in range(len(toks) - 1): + t = toks[k] + if t.kind != 'id' or t.text in KEYWORDS: + continue + if k > 0 and toks[k - 1].text in ('struct', 'class', 'union', 'enum'): + continue + j = k + 1 + if j < len(toks) and toks[j].text == '<' and j in st.template_open: + j = st.template_open[j] + 1 # Type name + while j < len(toks) and toks[j].text in ('&', '*', '&&', 'const'): + j += 1 + if j + 1 >= len(toks) or toks[j].kind != 'id' or toks[j].text in KEYWORDS: + continue + if toks[j + 1].text in ('(', '{', '=', ';', ',', ')', '['): + declared.append((t.text, toks[j].text, j)) + # auto NAME = TYPE(...) / TYPE{...} + for k in range(len(toks) - 4): + if toks[k].text == 'auto' and toks[k + 1].text in ('&', '&&'): + continue + if toks[k].text == 'auto' and toks[k + 1].kind == 'id' and toks[k + 2].text == '=': + m = k + 3 + while m + 1 < len(toks) and toks[m + 1].text == '::': + m += 2 + if m + 1 < len(toks) and toks[m].kind == 'id' and toks[m + 1].text in ('(', '{'): + declared.append((toks[m].text, toks[k + 1].text, k + 1)) + return FileFacts(path, rel, toks, st, includes, functions, classes, declared, + bool(HINT.search(raw))) + + +# --------------------------------------------------------------------------- +# Vocabulary of one translation unit +# --------------------------------------------------------------------------- + +@dataclass +class Vocabulary: + gen_types: set[str] = field(default_factory=set) + gen_objects: set[str] = field(default_factory=set) + gen_methods: dict[str, set[str]] = field(default_factory=dict) # class -> methods + # (owner class or None, name): free functions and named lambdas have no owner + drawers: set[tuple[str | None, str]] = field(default_factory=set) + classes: set[str] = field(default_factory=set) + bases: dict[str, set[str]] = field(default_factory=dict) + obj_types: dict[str, set[str]] = field(default_factory=dict) + + def lineage(self, cls: str | None) -> set[str | None]: + if cls is None: + return set() + out: set[str | None] = set() + pending = [cls] + while pending: + c = pending.pop() + if c in out: + continue + out.add(c) + pending.extend(self.bases.get(c, ())) + return out + + def drawer(self, name: str, owners: set[str | None] | None) -> bool: + """Whether `name` draws when owned by one of `owners` (None: any class).""" + if owners is None: + return any(n == name and o is not None for o, n in self.drawers) + return any((o, name) in self.drawers for o in owners) + + def names(self) -> list[str]: + return sorted({(o + '::' if o else '') + n for o, n in self.drawers}) + + +def resolved_owner(fn: Function, classes: set[str]) -> str | None: + if fn.qualifier and fn.qualifier in classes: + return fn.qualifier + return fn.owner + + +def build_vocabulary(unit: list[FileFacts]) -> Vocabulary: + v = Vocabulary() + v.gen_types |= STD_ENGINES + # A generator class: one of its methods is a stepper, and it is named as + # a generator or stands alone (no base class). A host class with a + # private draw() is not a generator; its draw() is a drawer below. + for ff in unit: + for cd in ff.classes: + steppers = [fn for fn in ff.functions if fn.owner == cd.name and fn.stepper] + if steppers and (GEN_NAME_RE.search(cd.name) or not cd.has_base): + v.gen_types.add(cd.name) + v.gen_methods.setdefault(cd.name, set()).update(cd.methods) + for ff in unit: + for cd in ff.classes: + v.classes.add(cd.name) + v.bases.setdefault(cd.name, set()).update(cd.bases) + for ff in unit: + for type_name, var, _ in ff.declared: + if type_name in v.gen_types: + v.gen_objects.add(var) + elif type_name in v.classes: + v.obj_types.setdefault(var, set()).add(type_name) + # drawers: free functions, lambdas and non-generator methods whose body + # draws, or which are steppers themselves; to a fixpoint. + candidates = [(ff, fn, resolved_owner(fn, v.classes)) for ff in unit for fn in ff.functions + if fn.name != ''] + candidates = [(ff, fn, owner) for ff, fn, owner in candidates if owner not in v.gen_methods] + for ff, fn, owner in candidates: + if fn.stepper: + v.drawers.add((owner, fn.name)) + checkers = {id(ff): Checker(ff, v) for ff in unit} + changed = True + while changed: + changed = False + for ff, fn, owner in candidates: + if (owner, fn.name) in v.drawers: + continue + checker = checkers[id(ff)] + checker._draw_cache.clear() + lo, hi = fn.body + if checker.region_draws(lo, hi): + v.drawers.add((owner, fn.name)) + changed = True + return v + + +# --------------------------------------------------------------------------- +# Findings +# --------------------------------------------------------------------------- + +@dataclass(frozen=True, order=True) +class Finding: + rel: str + line: int + kind: str + detail: str + + +class Checker: + def __init__(self, ff: FileFacts, v: Vocabulary): + self.ff = ff + self.toks = ff.toks + self.st = ff.st + self.v = v + self.findings: set[Finding] = set() + self._draw_cache: dict[int, bool] = {} + self.gen_class_ranges = [(cd.body, cd.name) for cd in ff.classes + if cd.name in v.gen_methods] + self.scopes = [(cd.body[0], cd.body[1], cd.name) for cd in ff.classes] + self.scopes += [(fn.body[0], fn.body[1], resolved_owner(fn, v.classes)) + for fn in ff.functions if fn.name != ''] + + # -- what is a draw ------------------------------------------------------ + def split_args(self, o: int) -> list[tuple[int, int]]: + c = self.st.close[o] + args = [] + start = o + 1 + k = o + 1 + while k < c: + x = self.toks[k].text + if x in ('(', '[', '{') and k in self.st.close: + k = self.st.close[k] + 1 + continue + if x == '<' and k in self.st.template_open: + k = self.st.template_open[k] + 1 + continue + if x == ',': + args.append((start, k)) + start = k + 1 + k += 1 + if start < c or args: + args.append((start, c)) + return args + + def bare_generator(self, lo: int, hi: int) -> bool: + texts = [t.text for t in self.toks[lo:hi]] + if texts and texts[0] in ('*', '&'): + texts = texts[1:] + if len(texts) >= 2 and texts[0] == 'this' and texts[1] == '->': + texts = texts[2:] + if len(texts) >= 4 and texts[:3] == ['std', '::', 'ref'] and texts[3] == '(': + texts = [x for x in texts[4:] if x != ')'] + return len(texts) == 1 and texts[0] in self.v.gen_objects + + def in_gen_class(self, idx: int) -> str | None: + for (lo, hi), name in self.gen_class_ranges: + if lo <= idx < hi: + return name + return None + + def is_draw_call(self, o: int) -> bool: + if o in self._draw_cache: + return self._draw_cache[o] + result = self._is_draw_call(o) + self._draw_cache[o] = result + return result + + def _is_draw_call(self, o: int) -> bool: + toks = self.toks + v = self.v + args = self.split_args(o) + if any(self.bare_generator(a, b) for a, b in args): + # the callee may draw from it -- unless this is a declaration + # copying it (Rng copy(rng)), which reads but does not advance + if not (o >= 2 and toks[o - 1].kind == 'id' and toks[o - 2].kind == 'id' + and toks[o - 2].text not in KEYWORDS): + return True + p = o - 1 + if p < 0: + return False + pt = toks[p] + if pt.text == ')' and p in self.st.open_of: + inner = [t.text for t in toks[self.st.open_of[p] + 1:p]] + if inner and inner[0] == '*': + inner = inner[1:] + return len(inner) == 1 and inner[0] in v.gen_objects + if pt.kind != 'id': + return False + name = pt.text + q = toks[p - 1] if p >= 1 else None + here = self.owner_at(o) + if q is not None and q.text in ('.', '->'): + obj = toks[p - 2] if p >= 2 else None + if obj is None or obj.kind != 'id': + return v.drawer(name, None) # a call result's member: any class + if obj.text == 'this': + return v.drawer(name, v.lineage(here)) or self._gen_method(name, o) + chained = p >= 3 and toks[p - 3].text in ('.', '->') and not ( + p >= 4 and toks[p - 4].text == 'this') + if obj.text in v.gen_objects and not chained: + return True + types = None if chained else v.obj_types.get(obj.text) + if types: + owners: set[str | None] = set() + for t in types: + owners |= v.lineage(t) + return v.drawer(name, owners) + return v.drawer(name, None) + if q is not None and q.text == '::': + qual = toks[p - 2] if p >= 2 else None + if qual is None or qual.text == 'std': + return False + if qual.text in v.classes: + return v.drawer(name, v.lineage(qual.text)) + return v.drawer(name, {None}) # a namespace's free function + if q is not None and ((q.kind == 'id' and q.text not in KEYWORDS) + or (q.text in ('&', '*', '&&', '>') and not self.st.operand_end(p - 2) + and q.text != '>') + or (q.text == '>' and (p - 1) in self.st.template_close)): + return False # a declaration: Rng rng(seed), Type name(args) + if name in v.gen_objects: + return True # rng() + owners = v.lineage(here) | {None} + return v.drawer(name, owners) or self._gen_method(name, o) + + def owner_at(self, idx: int) -> str | None: + """The class whose member function (or body) holds token idx.""" + best = None + for lo, hi, owner in self.scopes: + if lo <= idx < hi and (best is None or lo >= best[0]): + best = (lo, hi, owner) + return best[2] if best else None + + def _gen_method(self, name: str, o: int) -> bool: + cls = self.in_gen_class(o) + return cls is not None and name in self.v.gen_methods.get(cls, set()) + + def region_draws(self, lo: int, hi: int) -> bool: + """Whether toks[lo:hi] evaluates a draw (lambda and block bodies aside).""" + k = lo + st = self.st + while k < hi: + t = self.toks[k] + if t.text in ('(', '[', '{') and k in st.close: + kind = st.kind.get(k) + c = st.close[k] + if kind == CALL and self.is_draw_call(k): + return True + if kind in (CAPTURE, PARAMS, BLOCK, NAME): + k = c + 1 + continue + if self.region_draws(k + 1, c): + return True + k = c + 1 + continue + k += 1 + return False + + # -- walking ------------------------------------------------------------- + def check_file(self) -> None: + self.walk(0, len(self.toks)) + + def walk(self, lo: int, hi: int) -> None: + """Check one region: its unsequenced operands, then every nested group.""" + self.check_operands(lo, hi) + k = lo + st = self.st + while k < hi: + t = self.toks[k] + if t.text in ('(', '[', '{') and k in st.close: + c = st.close[k] + kind = st.kind.get(k) + if kind == CALL: + self.check_call(k) + for a, b in self.split_args(k): + self.walk(a, b) + elif kind == PARAMS: + self.check_params(k) + elif kind in (CAPTURE, NAME): + pass + elif kind == CONTROL and k > 0 and self.toks[k - 1].text == 'for': + self.walk(k + 1, c) + else: + self.walk(k + 1, c) + k = c + 1 + continue + k += 1 + + def check_call(self, o: int) -> None: + args = self.split_args(o) + if len(args) < 2: + return + drawing = [n + 1 for n, (a, b) in enumerate(args) + if not self.bare_generator(a, b) and self.region_draws(a, b)] + if len(drawing) >= 2: + callee = self.callee_text(o) + self.findings.add(Finding( + self.ff.rel, self.toks[o].line, 'ARG', + f'{callee}(...): arguments {", ".join(map(str, drawing))} each draw; ' + f'the compiler picks their order')) + + def callee_text(self, o: int) -> str: + toks = self.toks + p = o - 1 + parts = [] + while p >= 0 and len(parts) < 5: + t = toks[p] + if (t.kind == 'id' and t.text not in KEYWORDS) or t.text in ('.', '->', '::') \ + or t.text == 'this': + parts.append(t.text) + p -= 1 + continue + if t.text == '>' and p in self.st.template_close: + parts.append('<>') + for op_, cl in self.st.template_open.items(): + if cl == p: + p = op_ - 1 + break + else: + p -= 1 + continue + break + return ''.join(reversed(parts)) or '(expression)' + + def check_params(self, o: int) -> None: + for a, b in self.split_args(o): + texts = [] + k = a + while k < b: + x = self.toks[k].text + if x == '<' and k in self.st.template_open: + k = self.st.template_open[k] + 1 + continue + if x == '=': + break + texts.append(x) + k += 1 + if any(x in self.v.gen_types for x in texts) and not any( + x in ('&', '&&', '*') for x in texts): + name = next((x for x in texts if x in self.v.gen_types), '?') + self.findings.add(Finding( + self.ff.rel, self.toks[a].line if a < len(self.toks) else 0, 'BYVALUE', + f'parameter of generator type {name} taken by value; pass it by ' + f'reference')) + + def pieces(self, lo: int, hi: int) -> Iterable[list[int]]: + """Top-level token indices of toks[lo:hi], cut at every sequenced + punctuator, statement keyword, block and condition.""" + toks = self.toks + st = self.st + piece: list[int] = [] + k = lo + while k < hi: + t = toks[k] + x = t.text + if x in ('(', '[', '{') and k in st.close: + kind = st.kind.get(k) + c = st.close[k] + if kind in (BLOCK, CONTROL, PARAMS, NAME): + if piece: + yield piece + piece = [] + k = c + 1 + continue + if kind == CAPTURE: + # a lambda expression is one atom that draws nothing now + j = st.after_params(c) + if j < len(toks) and toks[j].text == '(' and st.kind.get(j) == PARAMS: + j = st.after_params(st.close[j]) + if j < len(toks) and toks[j].text == '{' and j in st.close: + piece.append(-1) + k = st.close[j] + 1 + continue + k = c + 1 + continue + piece.append(k) + k = c + 1 + continue + if x == '<' and k in st.template_open: + k = st.template_open[k] + 1 + continue + if t.kind == 'op' and x == '>' and k not in st.template_close and \ + k + 1 < hi and toks[k + 1].joined and (k + 1) not in st.template_close: + if piece: + yield piece + piece = [] + k += 2 # '>>': sequenced in C++17 + continue + if (t.kind == 'op' and x in SEPARATOR_OPS) or (t.kind == 'id' and x in SEPARATOR_WORDS): + if piece: + yield piece + piece = [] + k += 1 + continue + piece.append(k) + k += 1 + if piece: + yield piece + + def check_operands(self, lo: int, hi: int) -> None: + toks = self.toks + st = self.st + for piece in self.pieces(lo, hi): + operands: list[list[int]] = [[]] + ops: list[int] = [] + for pos, k in enumerate(piece): + if k < 0: + operands[-1].append(k) + continue + t = toks[k] + if t.kind == 'op' and t.text in UNSEQUENCED_BINARY and k not in st.template_close: + prev = piece[pos - 1] if pos > 0 else None + binary = prev is not None and prev >= 0 and self._ends_operand(prev) + if prev is not None and prev < 0: + binary = True # after a lambda atom + if binary: + ops.append(k) + operands.append([]) + continue + operands[-1].append(k) + if len(operands) < 2: + continue + drawing = [n for n, ks in enumerate(operands) if self._operand_draws(ks)] + if len(drawing) >= 2: + op_idx = ops[drawing[1] - 1] + self.findings.add(Finding( + self.ff.rel, toks[op_idx].line, 'OPERAND', + f"both sides of '{toks[op_idx].text}' draw; the compiler picks their " + f"order")) + + def _ends_operand(self, k: int) -> bool: + t = self.toks[k] + if t.text in ('(', '[', '{') and k in self.st.close: + return True # a group atom stands for its closing bracket + return self.st.operand_end(k) + + def _operand_draws(self, ks: list[int]) -> bool: + st = self.st + for k in ks: + if k < 0: + continue + t = self.toks[k] + if t.text in ('(', '[', '{') and k in st.close: + kind = st.kind.get(k) + if kind == CALL and self.is_draw_call(k): + return True + if self.region_draws(k + 1, st.close[k]): + return True + return False + + +# --------------------------------------------------------------------------- +# Driver +# --------------------------------------------------------------------------- + +def iter_sources(root: Path, targets: list[str]) -> list[Path]: + out: list[Path] = [] + for target in targets: + base = (root / target) if not Path(target).is_absolute() else Path(target) + if base.is_file(): + out.append(base) + continue + if not base.is_dir(): + continue + for p in sorted(base.rglob('*')): + if p.suffix in SUFFIXES and p.is_file(): + out.append(p) + uniq = [] + seen = set() + for p in out: + rp = p.resolve() + if rp in seen: + continue + seen.add(rp) + try: + rel = rp.relative_to(root.resolve()).as_posix() + except ValueError: + rel = rp.as_posix() + if any(rel.startswith(prefix) for prefix in EXCLUDED_PREFIXES): + continue + uniq.append(rp) + return uniq + + +def scan(root: Path, targets: list[str], vocabulary_out=None) -> list[Finding]: + root = root.resolve() + files = iter_sources(root, targets) + raw_cache: dict[Path, str] = {} + facts: dict[Path, FileFacts] = {} + + def rel_of(p: Path) -> str: + try: + return p.relative_to(root).as_posix() + except ValueError: + return p.as_posix() + + def raw(p: Path) -> str | None: + if p not in raw_cache: + try: + raw_cache[p] = p.read_text(encoding='utf-8', errors='replace') + except OSError: + return None + return raw_cache[p] + + def closure(p: Path) -> list[Path]: + order: list[Path] = [] + pending = [p] + seen: set[Path] = set() + while pending: + cur = pending.pop() + cur = cur.resolve() + if cur in seen: + continue + seen.add(cur) + text = raw(cur) + if text is None: + continue + order.append(cur) + for m in INCLUDE_RE.finditer(text): + pending.append(cur.parent / m.group(1)) + return order + + def fact(p: Path) -> FileFacts: + if p not in facts: + facts[p] = collect_facts(p, rel_of(p), raw(p) or '') + return facts[p] + + # units: every scanned file with its include closure + units = {p: closure(p) for p in files} + containing: dict[Path, list[Path]] = {} + for p, members in units.items(): + for m in members: + containing.setdefault(m, []).append(p) + hinted_unit = {p: any(HINT.search(raw(m) or '') for m in members) + for p, members in units.items()} + vocab: dict[Path, Vocabulary] = {} + for p, members in units.items(): + if hinted_unit[p]: + vocab[p] = build_vocabulary([fact(m) for m in members]) + findings: set[Finding] = set() + scanned = set(files) + for f in files: + for unit in containing.get(f, [f]): + v = vocab.get(unit) + if v is None: + continue + if not v.gen_objects and not v.drawers: + continue + checker = Checker(fact(f), v) + checker.check_file() + findings |= checker.findings + if vocabulary_out is not None: + for p in sorted(vocab): + v = vocab[p] + if not v.gen_objects and not v.drawers: + continue + vocabulary_out.append( + f'{rel_of(p)}: generator types {sorted(v.gen_types - STD_ENGINES)}; ' + f'objects {sorted(v.gen_objects)}; drawers {v.names()}') + del scanned + return sorted(findings) + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description=__doc__.split('\n', 1)[0]) + ap.add_argument('paths', nargs='*', help='files or directories (default: %s)' % + ', '.join(DEFAULT_DIRS)) + ap.add_argument('--root', type=Path, default=ROOT, help='checkout to scan (default: this one)') + ap.add_argument('--vocabulary', action='store_true', + help='print what each translation unit counts as a draw') + args = ap.parse_args(argv) + root = args.root + if not root.is_dir(): + print(f'check_rng_draw_order: no such directory {root}', file=sys.stderr) + return 2 + targets = args.paths or list(DEFAULT_DIRS) + vocab_lines: list[str] | None = [] if args.vocabulary else None + findings = scan(root, targets, vocab_lines) + if vocab_lines is not None: + for line in vocab_lines: + print(line) + lines_cache: dict[str, list[str]] = {} + for f in findings: + if f.rel not in lines_cache: + try: + lines_cache[f.rel] = (root / f.rel).read_text( + encoding='utf-8', errors='replace').splitlines() + except OSError: + lines_cache[f.rel] = [] + src = lines_cache[f.rel] + text = src[f.line - 1].strip() if 0 < f.line <= len(src) else '' + print(f'{f.rel}:{f.line}: {f.kind} {f.detail}\n {text}') + if findings: + print(f'check_rng_draw_order: {len(findings)} finding(s). Take each draw in a ' + f'statement of its own, into a named local, in the order the arguments are ' + f'written (left to right).', file=sys.stderr) + return 1 + print('check_rng_draw_order: no draw left to the compiler\'s evaluation order') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index c272dc7d..7bd28c6c 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -188,6 +188,13 @@ def check_commands(source: Path) -> list[tuple]: [sys.executable, str(source / 'scripts/check_market_admission_schema.py')]), ('source-guard-cancellation', [sys.executable, str(source / 'scripts/check_cancellation_hash_coverage.py')]), + # R5 lane H-DOCGATES (AUDIT4-opus X17, perf N-2): two random draws whose + # order C++ leaves to the compiler build a different battery on x86-64 + # GCC than on AppleClang -- XPLAT1's bug class, back at ten sites. + ('rng-draw-order-tests', + [sys.executable, str(source / 'scripts/test_check_rng_draw_order.py')]), + ('rng-draw-order', + [sys.executable, str(source / 'scripts/check_rng_draw_order.py')]), ('verifier-tests', [sys.executable, str(source / 'scripts/test_ci_verify.py')]), ('preflight-tests', [sys.executable, str(source / 'scripts/test_ci_preflight.py')]), ('native-c-surface-tests', diff --git a/scripts/test_check_rng_draw_order.py b/scripts/test_check_rng_draw_order.py new file mode 100644 index 00000000..bd2b047d --- /dev/null +++ b/scripts/test_check_rng_draw_order.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +"""Self-test for check_rng_draw_order.py. + +The must-fail fixture holds XPLAT1's three shapes (e8bc3237's parent) and the +shapes the tenth-site audit found (a drawing helper in one argument, a nested +call's arguments, a distribution, an operator's two operands, a lambda over a +captured seed, a generator taken by value). The must-pass fixture holds every +sequenced form the checker must leave alone, and the one-draw-per-statement +rewrite of each must-fail line. Run: python3 scripts/test_check_rng_draw_order.py +""" +from __future__ import annotations + +import subprocess +import sys +import tempfile +import textwrap +import unittest +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) +import check_rng_draw_order as lint # noqa: E402 + +GENERATOR = """ +#include +#include +#include +#include +#include +struct Random { + std::uint64_t state; + explicit Random(std::uint64_t seed) : state(seed * 0x9E3779B97F4A7C15ull + 1) {} + std::uint64_t next() { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + return state; + } + int below(int n) { return static_cast(next() % static_cast(n)); } + bool chance(int p) { return below(100) < p; } + double unit() { return static_cast(next() >> 11) * (1.0 / 9007199254740992.0); } +}; +""" + +# Each line that must be reported ends with a marker comment naming the kind. +MUST_FAIL = GENERATOR + """ +void strategy_exit(const char*, const char*, double, double, double, double); +void strategy_entry(const char*, bool, double, const char*, const char*, int); +void strategy_order(const char*, bool, double, double); +static const char* const kExits[] = {"X", "Y"}; +static const double kNa = 0.0; + +void xplat1(Random& s, const char* id, bool is_long, double close) { + // e8bc3237^ case 7, case 4 and default + strategy_exit(kExits[s.below(2)], id, kNa, kNa, 4.0 + s.below(8), // @ARG + 1.0 + s.below(4)); + strategy_entry(id, is_long, 1.0 + s.below(3), "", s.chance(30) ? "g" : "", // @ARG + s.chance(30) ? 1 + s.below(2) : 0); + strategy_order(id, is_long, 1.0 + s.below(2), // @ARG + s.chance(50) ? (is_long ? close - 1 : close + 1) : kNa); +} + +struct Book { + Random rng_{7}; + const char* long_id() { return rng_.chance(70) ? "L" : "L2"; } + double units() { + static const double sizes[] = {1.0, 2.0, 0.5}; + return sizes[rng_.below(3)]; + } + int opening(bool buy, double size, long ref) { + const int base = buy ? int(size) : -int(size); + return rng_.chance(30) ? base + int(ref) : base; + } + void place(int request, bool market); + void close(const char* id, double qty, bool immediately); + void enter(long ref) { + place(opening(true, 1.0, ref), rng_.chance(30)); // @ARG + const int replaced = opening(rng_.chance(50), units(), ref); // @ARG + close(long_id(), 1.0, rng_.chance(30)); // @ARG + (void)replaced; + } + long level(long ref) { return ref + (rng_.chance(50) ? 1 : -1) * rng_.below(16); } // @OPERAND +}; + +struct Kc { double compute(double src, double high, double low, double close); }; +void noise(Kc& kc, Random& r) { + kc.compute(1500.0 + r.unit() * 10.0, 1510.0, 1490.0 + r.unit(), 1500.0); // @ARG +} + +void engines() { + std::mt19937_64 rng(7); + std::uniform_int_distribution dist(0, 9); + const auto pair = std::make_pair(dist(rng), dist(rng)); // @ARG + const bool same = rng() % 2 == rng() % 3; // @OPERAND + std::uint64_t state = 1; + auto uniform = [&state] { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + return state; + }; + const double sum = static_cast(uniform()) + static_cast(uniform()); // @OPERAND + auto later = [&rng] { return rng() - rng(); }; // @OPERAND + (void)pair; (void)same; (void)sum; (void)later; +} + +std::uint64_t draw_twice(Random copy) { // @BYVALUE + return copy.next(); +} +""" + +MUST_PASS = GENERATOR + """ +struct Request { int a; int b; }; +struct Fold { + std::uint64_t h = 1; + void u(std::uint64_t v) { + h = (h ^ v) * 0x9E3779B97F4A7C15ull; + h ^= h >> 29; + } +}; +std::uint64_t value(const Fold& f) { return f.h; } +std::vector walk(Random& r, int count, double wide); +void take(int a, int b); +void strategy_exit(const char*, const char*, double, double, double, double); +static const char* const kExits[] = {"X", "Y"}; + +struct Tracker { std::uint64_t step(int v) { return static_cast(v) * 2u; } }; +struct Twin { + Random rng_{3}; + Tracker staged_tracker_; + Tracker direct_tracker_; + void step(int n) { for (int k = 0; k < n; ++k) (void)rng_.next(); } + bool agree() { return staged_tracker_.step(1) == direct_tracker_.step(2); } +}; + +void sequenced(Random& r, Random& s, const char* id) { + const int a = r.below(3), b = r.below(4); + Request q{r.chance(50) ? 1 : 2, r.chance(30) ? 3 : 4}; + Request w = {r.below(3), r.below(4)}; + std::vector rows; + rows.push_back({r.below(3), r.below(4)}); + int x = r.chance(50) ? r.below(3) : r.below(4); + const bool y = r.chance(50) && r.chance(50); + const bool z = r.chance(50) || r.chance(50); + int c = (r.below(3), r.below(4)); + std::ostringstream os; + os << r.below(3) << r.below(4); + int slots[16] = {}; + slots[r.below(8)] = r.below(9); + if (r.chance(7)) x += r.chance(50) ? r.below(12) : -r.below(12); + const std::vector bars = walk(r, 40 + r.below(20), 10.0); + const int t1 = int(r.unit() * 6.0), t2 = static_cast(r.unit() * 6.0); + const int nested = r.below(r.below(3) + 1); + std::vector> grid; + // take(r.below(3), r.below(4)) in a comment is not code + const char* text = "take(r.below(3), r.below(4))"; + // the one-draw-per-statement rewrite of XPLAT1's case 7 + const char* const exit_id = kExits[s.below(2)]; + const double trail_points = 4.0 + s.below(8); + const double trail_offset = 1.0 + s.below(4); + strategy_exit(exit_id, id, 0.0, 0.0, trail_points, trail_offset); + const int first = r.below(3); + const int second = r.below(4); + take(first, second); + Fold f, g; + const bool folds_agree = value(f) == value(g); + (void)a; (void)b; (void)q; (void)w; (void)y; (void)z; (void)c; (void)bars; (void)t1; + (void)t2; (void)nested; (void)text; (void)folds_agree; (void)grid; +} +""" + + +def expected(text: str) -> set[tuple[int, str]]: + out = set() + for n, line in enumerate(text.splitlines(), 1): + if '// @' in line: + out.add((n, line.rsplit('// @', 1)[1].strip())) + return out + + +class DrawOrderTest(unittest.TestCase): + def scan_text(self, text: str, name: str = 'fixture.cpp') -> set[tuple[int, str]]: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / 'tests').mkdir() + (root / 'tests' / name).write_text(textwrap.dedent(text), encoding='utf-8') + return {(f.line, f.kind) for f in lint.scan(root, ['tests'])} + + def test_must_fail_reports_every_marked_line_and_nothing_else(self): + self.assertEqual(self.scan_text(MUST_FAIL), expected(MUST_FAIL)) + + def test_must_pass_is_clean(self): + self.assertEqual(self.scan_text(MUST_PASS), set()) + + def test_sequencing_a_site_clears_it(self): + broken = GENERATOR + """ + void take(int a, int b); + void f(Random& r) { take(r.below(3), r.below(4)); } + """ + fixed = GENERATOR + """ + void take(int a, int b); + void f(Random& r) { + const int a = r.below(3); + const int b = r.below(4); + take(a, b); + } + """ + self.assertEqual({k for _, k in self.scan_text(broken)}, {'ARG'}) + self.assertEqual(self.scan_text(fixed), set()) + + def test_a_helper_header_is_checked_under_the_unit_that_includes_it(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / 'tests').mkdir() + (root / 'tests' / 'gen.hpp').write_text(GENERATOR, encoding='utf-8') + (root / 'tests' / 'user.cpp').write_text( + '#include "gen.hpp"\nvoid take(int, int);\n' + 'void f(Random& r) { take(r.below(3), r.below(4)); }\n', encoding='utf-8') + found = {(f.rel, f.line, f.kind) for f in lint.scan(root, ['tests'])} + self.assertEqual(found, {('tests/user.cpp', 3, 'ARG')}) + + def test_cli_exit_codes(self): + for text, code in ((MUST_FAIL, 1), (MUST_PASS, 0)): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / 'tests').mkdir() + (root / 'tests' / 'fixture.cpp').write_text(text, encoding='utf-8') + proc = subprocess.run( + [sys.executable, str(HERE / 'check_rng_draw_order.py'), '--root', str(root), + 'tests'], capture_output=True, text=True) + self.assertEqual(proc.returncode, code, proc.stdout + proc.stderr) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/adapter_lookup_index_fixture.hpp b/tests/adapter_lookup_index_fixture.hpp index c79129eb..df574428 100644 --- a/tests/adapter_lookup_index_fixture.hpp +++ b/tests/adapter_lookup_index_fixture.hpp @@ -238,9 +238,13 @@ class StreamHost final : public source::PineStrategyHost { const double level = near(bar, 1, 10); const bool limit = rng_.percent(50); const bool oca = rng_.percent(40); - strategy_entry(buy ? long_id() : short_id(), buy, limit ? level : kNa, - limit ? kNa : level, kNa, {}, oca ? group() : "", - oca ? (rng_.percent(60) ? 1 : 2) : 0); + // One draw per statement, left to right: C++ leaves the order in + // which a call's arguments are evaluated unspecified. + const char* const entry_id = buy ? long_id() : short_id(); + const char* const oca_name = oca ? group() : ""; + const int oca_type = oca ? (rng_.percent(60) ? 1 : 2) : 0; + strategy_entry(entry_id, buy, limit ? level : kNa, limit ? kNa : level, kNa, {}, + oca_name, oca_type); } if (position != 0.0 && rng_.percent(45)) { ++commands; @@ -265,15 +269,22 @@ class StreamHost final : public source::PineStrategyHost { } if (position != 0.0 && rng_.percent(8)) { ++commands; - if (rng_.percent(50)) strategy_close(any_id(), {}, 1.0); - else strategy_close(any_id(), {}, kNa, 50.0, rng_.percent(30)); + if (rng_.percent(50)) { + strategy_close(any_id(), {}, 1.0); + } else { + const char* const close_id = any_id(); + const bool immediately = rng_.percent(30); + strategy_close(close_id, {}, kNa, 50.0, immediately); + } } if (rng_.percent(3)) { ++commands; strategy_close_all(); } if (rng_.percent(5)) { ++commands; strategy_cancel(any_id()); } if (rng_.percent(2)) { ++commands; strategy_cancel_all(); } if (rng_.percent(6)) { ++commands; - strategy_order("o", rng_.percent(50), 1.0, rng_.percent(50) ? near(bar, 1, 6) : kNa); + const bool is_long = rng_.percent(50); + const double limit = rng_.percent(50) ? near(bar, 1, 6) : kNa; + strategy_order("o", is_long, 1.0, limit); } if (rng_.percent(3)) { ++commands; strategy_exit_cancel_bracket("xl", "L"); } } diff --git a/tests/native_fused_settlement_fixture.hpp b/tests/native_fused_settlement_fixture.hpp index 3c1672ab..bdadd5a9 100644 --- a/tests/native_fused_settlement_fixture.hpp +++ b/tests/native_fused_settlement_fixture.hpp @@ -608,7 +608,11 @@ class FusedHost : public NativeStrategyHost { const double size = units(); const int kind = rng_.below(100); if (kind < 55) { - place(opening(buy, size, ref), rng_.percent(30)); + // One draw per statement, left to right: C++ leaves the order in + // which a call's arguments are evaluated unspecified. + const no::Request request = opening(buy, size, ref); + const bool market = rng_.percent(30); + place(request, market); } else if (kind < 85) { // A bracket: the entry and two legs its fill arms, one OCA group. const auto parent = submit(opening(buy, size, ref)); @@ -739,7 +743,9 @@ class FusedHost : public NativeStrategyHost { const std::size_t at = static_cast(rng_.below(static_cast(handles_.size()))); const no::RequestHandle target = handles_[at]; if (rng_.percent(60)) { - const auto result = replace(target, opening(rng_.percent(50), units(), ref)); + const bool buy = rng_.percent(50); + const double size = units(); + const auto result = replace(target, opening(buy, size, ref)); if (result.successor) { ++outcome.replaced; handles_[at] = *result.successor; diff --git a/tests/test_adapter_live_state_equivalence.cpp b/tests/test_adapter_live_state_equivalence.cpp index 46e7906a..cc420d98 100644 --- a/tests/test_adapter_live_state_equivalence.cpp +++ b/tests/test_adapter_live_state_equivalence.cpp @@ -208,7 +208,11 @@ std::vector revival_tape(const Config& config) { long cents = 10000; for (int index = 0; index < config.bars; ++index) { long open = cents; - if (rng.percent(10)) open += (rng.percent(60) ? 1 : -1) * rng.between(100, 300); + if (rng.percent(10)) { + // One draw per statement: the operands of '*' are unsequenced. + const int sign = rng.percent(60) ? 1 : -1; + open += sign * rng.between(100, 300); + } long drift = rng.between(-80, 80); if (rng.percent(6)) drift = rng.between(600, 1400); if (rng.percent(3)) drift = -rng.between(600, 1400); diff --git a/tests/test_native_direct_mutation.cpp b/tests/test_native_direct_mutation.cpp index 8cd2a967..f00c4e41 100644 --- a/tests/test_native_direct_mutation.cpp +++ b/tests/test_native_direct_mutation.cpp @@ -379,7 +379,9 @@ class Script { const bool waits = owner < 25 && !staged_.live().empty(); const int intent = rng_.below(100); if (intent < 30) { - r.intent = no::Transact{(rng_.percent(50) ? 1.0 : -1.0) * rng_.between(1, 3)}; + // One draw per statement: the operands of '*' are unsequenced. + const double sign = rng_.percent(50) ? 1.0 : -1.0; + r.intent = no::Transact{sign * rng_.between(1, 3)}; } else if (intent < 50) { const int size = rng_.below(100); if (size < 50) r.intent = no::Reduce{no::ExplicitUnits{static_cast(rng_.between(1, 3))}}; @@ -394,7 +396,8 @@ class Script { } else if (intent < 62) { r.intent = no::Flatten{}; } else if (intent < 70) { - r.intent = no::ReverseTo{(rng_.percent(50) ? 1.0 : -1.0) * rng_.between(1, 3)}; + const double sign = rng_.percent(50) ? 1.0 : -1.0; + r.intent = no::ReverseTo{sign * rng_.between(1, 3)}; } else if (intent < 85) { no::HostSized sized; sized.kind = rng_.percent(50) ? no::HostSizedKind::Open : no::HostSizedKind::Close; diff --git a/tests/test_native_handle_stable_replace.cpp b/tests/test_native_handle_stable_replace.cpp index bfd8e804..08a231a0 100644 --- a/tests/test_native_handle_stable_replace.cpp +++ b/tests/test_native_handle_stable_replace.cpp @@ -483,7 +483,9 @@ class ReissueHost final : public NativeStrategyHost { exit.kind = Kind::Exit; exit.trigger = rng_.below(3); exit.intent = rng_.below(3); - exit.level = ref_ + (rng_.percent(50) ? 1 : -1) * rng_.between(4, 16); + // One draw per statement: the operands of '*' are unsequenced. + const int side = rng_.percent(50) ? 1 : -1; + exit.level = ref_ + side * rng_.between(4, 16); const int tie = rng_.percent(30) ? pick_live(Kind::Exit) : -1; if (tie >= 0) { const Slot& other = slots_[static_cast(tie)]; @@ -499,7 +501,8 @@ class ReissueHost final : public NativeStrategyHost { place(exit); Slot sibling = exit; sibling.trigger = exit.trigger == 0 ? 1 : 0; - sibling.level = ref_ + (rng_.percent(50) ? 1 : -1) * rng_.between(4, 16); + const int sibling_side = rng_.percent(50) ? 1 : -1; + sibling.level = ref_ + sibling_side * rng_.between(4, 16); place(sibling); return; } diff --git a/tests/test_ta_kc_basis.cpp b/tests/test_ta_kc_basis.cpp index 27b088c7..48685e5a 100644 --- a/tests/test_ta_kc_basis.cpp +++ b/tests/test_ta_kc_basis.cpp @@ -191,9 +191,14 @@ static void test_recompute_sequences() { double prev_close = na(); int bad = 0; for (int t = 0; t < (int)b.src.size(); ++t) { - kc.compute(1500.0 + r.unit() * 10.0, 1510.0, 1490.0 + r.unit(), 1500.0); - kc.recompute(r.unit() < 0.1 ? na() : 1400.0 + r.unit() * 200.0, - 1600.0, 1400.0, r.unit() < 0.1 ? na() : 1450.0); + // One draw per statement, left to right: C++ leaves the order in which + // a call's arguments are evaluated unspecified. + const double first_src = 1500.0 + r.unit() * 10.0; + const double first_low = 1490.0 + r.unit(); + kc.compute(first_src, 1510.0, first_low, 1500.0); + const double second_src = r.unit() < 0.1 ? na() : 1400.0 + r.unit() * 200.0; + const double second_close = r.unit() < 0.1 ? na() : 1450.0; + kc.recompute(second_src, 1600.0, 1400.0, second_close); const ta::KCResult got = kc.recompute(b.src[t], b.high[t], b.low[t], b.close[t]); const double basis = basis_ref.compute(b.src[t]); const double span = true_range(b.high[t], b.low[t], prev_close); diff --git a/tests/test_ta_kc_range.cpp b/tests/test_ta_kc_range.cpp index fb502518..d57e349c 100644 --- a/tests/test_ta_kc_range.cpp +++ b/tests/test_ta_kc_range.cpp @@ -199,8 +199,11 @@ static void test_recompute_sequences() { for (int t = 0; t < (int)b.src.size(); ++t) { kc.compute(3100.0 + r.unit(), 3150.0, 3050.0, 3120.0); kcw.compute(3100.0 + r.unit(), 3150.0, 3050.0, 3120.0); - kc.recompute(r.unit() < 0.1 ? na() : 3000.0, 3400.0, - r.unit() < 0.1 ? na() : 2900.0, 3001.0); + // One draw per statement, left to right: C++ leaves the order in + // which a call's arguments are evaluated unspecified. + const double second_src = r.unit() < 0.1 ? na() : 3000.0; + const double second_low = r.unit() < 0.1 ? na() : 2900.0; + kc.recompute(second_src, 3400.0, second_low, 3001.0); kcw.recompute(3000.0, 3400.0, 2900.0, r.unit() < 0.1 ? na() : 3001.0); const ta::KCResult got = kc.recompute(b.src[t], b.high[t], b.low[t], b.close[t]); const double w = kcw.recompute(b.src[t], b.high[t], b.low[t], b.close[t]); diff --git a/tests/test_ta_pivot_point_levels.cpp b/tests/test_ta_pivot_point_levels.cpp index bfc1e3c0..7f11e95c 100644 --- a/tests/test_ta_pivot_point_levels.cpp +++ b/tests/test_ta_pivot_point_levels.cpp @@ -373,10 +373,15 @@ static void test_recompute_sequences() { int bad = 0; for (int t = 0; t < n; ++t) { const int t1 = int(r.unit() * 6.0), t2 = int(r.unit() * 6.0); - p.compute(kTypes[t1], r.unit() < 0.5, t1 != 2 && r.unit() < 0.5, 2400.0, 2600.0, 2300.0, - 2450.0); - p.recompute(kTypes[t2], r.unit() < 0.5, t2 != 2 && r.unit() < 0.5, - r.unit() < 0.2 ? na() : 2410.0, 2700.0, 2200.0, 2500.0); + // One draw per statement, left to right: C++ leaves the order in + // which a call's arguments are evaluated unspecified. + const bool anchor1 = r.unit() < 0.5; + const bool developing1 = t1 != 2 && r.unit() < 0.5; + p.compute(kTypes[t1], anchor1, developing1, 2400.0, 2600.0, 2300.0, 2450.0); + const bool anchor2 = r.unit() < 0.5; + const bool developing2 = t2 != 2 && r.unit() < 0.5; + const double open2 = r.unit() < 0.2 ? na() : 2410.0; + p.recompute(kTypes[t2], anchor2, developing2, open2, 2700.0, 2200.0, 2500.0); const std::vector got = p.recompute(kTypes[type[t]], a[t] != 0, dev[t] != 0, f.o[t], f.h[t], f.l[t], f.c[t]); if (!same_levels(got, reference(f, a, type, dev, t))) ++bad; From e7907d6e77f475cc890b07f7350326bbc224de5d Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 03:48:27 +0800 Subject: [PATCH 14/83] Tests: the runtime blocks' LIFO check gets a death row, and the chart-day memo a witness that reads it (R5 lane H-DOCGATES, X17) AUDIT4-opus perf N-7 and D-17: two INT23 witnesses proved nothing. 1. uninstall_runtime_ambient's debug-build LIFO check (09ced53e, INT23) had no row: a mutant deleting it survived every profile. tests/test_native_runtime_ambient_lifo.cpp holds the legal order in process (each block's writes copied back down to the thread's own) and, in a forked child, the outer block leaving first: a library built with its debug checks (PINEFORGE_LIBRARY_DEBUG_CHECKS=$, from tests/CMakeLists.txt; the TU strips NDEBUG like every test) must die of SIGABRT inside the call; any other library must return, leaving the thread's own block in force with the inner block's write lost -- the silent state loss the check exists to stop. Measured on a scratch copy of this tree: Debug "ok (16 checks)", Release "ok (19 checks)", Debug with UBSan ok; with the check deleted, Debug fails 2 of 16 (the child returned). This tree's Release kernel build: "ok (19 checks)". Source-free, so it runs in the kernel profile too: KERNEL_MIN_TESTS 273 -> 274, RELEASE_MIN_TESTS 674 -> 675 (recorded below both floor lines so the pages' citations of them do not move). 2. test_chart_day_memo's `CHECK(pair.kept.holds_cache())` became vacuous with D2-C: the consumer holds the adapter's lookup index on every chart, so INT23's mutant cd5 (a memo never kept or read) passed. The new memo_witness reads the memo in place and plants keys in it (test-only: the index's day_lo / day_hi / day_key offsets come from a layout mirror of AdapterLookupIndex, held to the whole days UTC reads keep before anything is planted, so a drifting layout fails instead of writing over another member): on each UTC spelling a read keeps its whole floor day and key and a planted key answers exactly that day; on each chart timezone nothing is kept and a planted key is never answered. Mutants on a scratch copy: cd1-cd4 still fail; cd5 (never read), cd6 (kept, never read) and cd7 (a zoned chart consults the memo) now fail where the old check passed all three. No source change: tests only. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit d5ead4d5893e175f431bbd9b8d25f138fe6d5706) INT26 pick note: the floors conflicted with the merged ones; each adds this commit's LIFO death row to the merged value (KERNEL 279 -> 280, RELEASE 691 -> 692), with this commit's comment kept below RELEASE_MIN_TESTS so the cited floor lines stay put. Every other hunk of d5ead4d5 carried. --- scripts/ci_verify.py | 10 +- tests/CMakeLists.txt | 16 ++ tests/test_chart_day_memo.cpp | 219 ++++++++++++++++++++- tests/test_native_runtime_ambient_lifo.cpp | 212 ++++++++++++++++++++ 4 files changed, 451 insertions(+), 6 deletions(-) create mode 100644 tests/test_native_runtime_ambient_lifo.cpp diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index fc019171..c2d38a7d 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -246,7 +246,7 @@ # registered, 278 run: the WebSocket row still skips on a system libcurl. # 279 = those 278 plus R5 lane H-DOCGATES' test_doc_reverts (the doc-revert # gate's self-test, a Python row every profile registers; INT26 pick, summed). -KERNEL_MIN_TESTS = 279 +KERNEL_MIN_TESTS = 280 # Release-row floor, the same gate for the default profile. Before lane P7 # only the kernel profile had one, so a row that left release alone (a # source-bound TU dropped from TEST_SOURCES, a deleted twin or ABI row) left a @@ -372,7 +372,13 @@ # No release row skips, so 690 registered is 690 run. # 691 = those 690 plus R5 lane H-DOCGATES' test_doc_reverts (see # KERNEL_MIN_TESTS; INT26 pick, summed). -RELEASE_MIN_TESTS = 691 +RELEASE_MIN_TESTS = 692 +# R5 lane H-DOCGATES (AUDIT4-opus perf N-7) raises both floors by one more: +# test_native_runtime_ambient_lifo, the runtime blocks' LIFO death row, is +# source-free, so it registers and runs in the kernel profile (279 -> 280) and +# in release (691 -> 692; INT26 pick, summed). Recorded below both floors so +# neither floor's line moves: CONTRIBUTING.md and contributing-llm.md cite +# them by line. # ADR-0001 ruled-count floors, beside the ctest floors (R5 lane H-DOCGATES, # AUDIT4-opus X13 / docs-a N7). check_kernel_residuals.py counts the rulings # its vocabulary reads -- 174 identifiers and 45 texts on the lane's tree: the diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7c0a8268..e4d79113 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1230,6 +1230,12 @@ list(APPEND TEST_SOURCES test_native_precommit_hook) # nested run and two threads (differential). Source-free, so it also runs in # the kernel-only profile. list(APPEND TEST_SOURCES test_native_runtime_ambient) +# R5 lane H-DOCGATES (AUDIT4 N-7): the runtime blocks' LIFO order -- the legal +# order in process, and in a forked child the outer block leaving first, which +# a library with its debug checks compiled in must answer with SIGABRT and any +# other library by returning (PINEFORGE_LIBRARY_DEBUG_CHECKS, below). +# Source-free, so it also runs in the kernel-only profile. +list(APPEND TEST_SOURCES test_native_runtime_ambient_lifo) # R5 lane D2-C: the Pine host's publications, its suppressed probe tail and # its request.security evaluations (host-driven, kernel-routed, the # auxiliary lower-timeframe array) set the chart day partition, the EMA @@ -2150,6 +2156,16 @@ target_link_libraries(test_timezone_concurrency PRIVATE Threads::Threads) target_link_libraries(test_local_time_lock_free_hit PRIVATE Threads::Threads) target_link_libraries(test_native_runtime_ambient PRIVATE Threads::Threads) +# The LIFO row's expectation is the library's build type. The library's +# NDEBUG, which compiles uninstall_runtime_ambient's order check out, comes +# from CMake's build-type flags -- Release, RelWithDebInfo and MinSizeRel +# define it; Debug, the debug and sanitizers profiles' type, does not -- and +# the row's TU strips NDEBUG like every test's (-UNDEBUG above), so it is +# told. Flags that disagree with the build type fail the row; none passes it +# without the check. +target_compile_definitions(test_native_runtime_ambient_lifo PRIVATE + PINEFORGE_LIBRARY_DEBUG_CHECKS=$) + # Pure-C sanity test for . Compiled with the C # compiler so any C++-only syntax leaking into the public header # triggers a hard build failure. Linked with CXX so it can pull in diff --git a/tests/test_chart_day_memo.cpp b/tests/test_chart_day_memo.cpp index 2884ac26..ee7d1f5f 100644 --- a/tests/test_chart_day_memo.cpp +++ b/tests/test_chart_day_memo.cpp @@ -29,17 +29,26 @@ // the trades and the report's statistics -- each kept-key run equal to its // computed-key run, and every report's statistics equal to the // 6c081f5d walk over the run's own equity curve -// (tests/equity_stats_reference.hpp). +// (tests/equity_stats_reference.hpp); +// * the memo itself, which no key above can show -- it answers the key the +// arithmetic answers -- read and planted in place (memo_witness): on each +// UTC spelling a read keeps its whole floor day and key, and a key +// planted over a day answers that day's first to last millisecond and no +// second outside it; on each chart timezone nothing is kept and a +// planted key is never answered; the computed host holds no cache. #include #include #include #include "../src/native_execution_consumer.hpp" +#include "../src/source/pine_host_reads.hpp" #include "../src/timezone.hpp" #include "equity_stats_reference.hpp" #include +#include #include +#include #include #include #include @@ -49,6 +58,8 @@ #include #include #include +#include +#include #include namespace { @@ -145,9 +156,11 @@ class KeyHost final : public source::PineStrategyHost { std::int64_t key(std::int64_t timestamp_ms) const noexcept { return fixture_chart_day_key(timestamp_ms); } - bool holds_cache() const { - return as_native_consumer(execution_consumer()).host_cache() != nullptr; + bool holds_cache() const { return cache() != nullptr; } + NativeHostCache* cache() const { + return as_native_consumer(execution_consumer()).host_cache(); } + const void* adapter_address() const noexcept { return &adapter_; } }; long long battery_keys = 0; @@ -323,8 +336,9 @@ void zone_battery() { pair.walk(mondays, seed += 0x9e3779b9); pair.walk(months, seed += 0x9e3779b9); } + // The kept host's memo is memo_witness's: holding a cache shows + // nothing since D2-C, which adopts the index on every chart. CHECK(pair.computed.holds_cache() == false); - if (utc_spelling(zone.name)) CHECK(pair.kept.holds_cache()); } } @@ -575,20 +589,217 @@ void day_ledger_runs() { } } +// ── The memo itself ────────────────────────────────────────────────────── +// Every check above holds a key to the value it must have, and the memo +// answers the value the arithmetic answers, so none of them can tell a key +// read from the memo from one computed again: INT23's mutant cd5, a memo +// never kept or read, passes them all. Nor can holding a cache: since R5 +// lane D2-C the consumer holds the adapter's lookup index from every run's +// begin (take_run_facts), on a chart timezone too, where chart_day_key never +// reaches it. So the witness reads the memo in place and plants keys in it. +// AdapterLookupIndex is pine_adapter.cpp's own type. LookupIndexLayout +// repeats its members in order over the same base only to find where the +// last three -- day_lo, day_hi, day_key -- live; they are read and written +// as bytes at those offsets from the index's NativeHostCache base, never +// through the layout type. The offsets are held to the whole days UTC reads +// keep before anything is planted, so a layout that drifts from the index's +// fails there instead of writing over another member. +struct LookupIndexLayout final : source::detail::PineRunCache { + struct CohortSides { + std::size_t folded = 0; + bool opened_long = false; + bool opened_short = false; + }; + std::uint64_t answers = 0; + std::unordered_map cohort_sides; + std::uint64_t erased_seen = 0; + std::uint64_t folded_high = 0; + std::size_t rows_folded = 0; + std::vector unbound; + std::array>, 3> legs_by_origin; + std::unordered_map> immediate_closes_by_bar; + std::int64_t day_lo = 1; + std::int64_t day_hi = 0; + std::int64_t day_key = 0; + std::uint64_t answered() const noexcept override { return answers; } +}; + +// The memo: the whole seconds [lo, hi) it keeps and the key they answer. The +// default is an index's own before any read keeps a day. +struct DayMemo { + std::int64_t lo = 1; + std::int64_t hi = 0; + std::int64_t key = 0; + bool operator==(const DayMemo& other) const { + return lo == other.lo && hi == other.hi && key == other.key; + } +}; + +// The memo of the lookup index the host's consumer holds for the host's own +// adapter, in place; not held when the consumer holds no such index. +class MemoInPlace { +public: + explicit MemoInPlace(const KeyHost& host) { + NativeHostCache* const cache = host.cache(); + if (cache == nullptr || cache->kind() != &source::detail::kPineRunCacheKind) return; + if (static_cast(cache)->owner + != host.adapter_address()) + return; + if (std::strstr(typeid(*cache).name(), "AdapterLookupIndex") == nullptr) return; + index_ = reinterpret_cast(cache); + } + bool held() const { return index_ != nullptr; } + DayMemo read() const { + DayMemo memo; + std::memcpy(&memo.lo, index_ + offsets().lo, sizeof memo.lo); + std::memcpy(&memo.hi, index_ + offsets().hi, sizeof memo.hi); + std::memcpy(&memo.key, index_ + offsets().key, sizeof memo.key); + return memo; + } + void plant(const DayMemo& memo) const { + std::memcpy(index_ + offsets().lo, &memo.lo, sizeof memo.lo); + std::memcpy(index_ + offsets().hi, &memo.hi, sizeof memo.hi); + std::memcpy(index_ + offsets().key, &memo.key, sizeof memo.key); + } + +private: + struct Offsets { + std::ptrdiff_t lo; + std::ptrdiff_t hi; + std::ptrdiff_t key; + }; + static const Offsets& offsets() { + static const Offsets at = [] { + const LookupIndexLayout layout; + const auto* base = reinterpret_cast( + static_cast(&layout)); + const auto of = [&](const std::int64_t& field) { + return reinterpret_cast(&field) - base; + }; + return Offsets{of(layout.day_lo), of(layout.day_hi), of(layout.day_key)}; + }(); + return at; + } + unsigned char* index_ = nullptr; +}; + +// A key no day has: day 42 of month 42. +constexpr std::int64_t kPlantedKey = 4242; + +long long memo_days_kept = 0; +long long memo_planted_answers = 0; +long long memo_computed_reads = 0; + +// The whole floor day a UTC read at `ts` keeps, as chart_day_key keeps it: +// the stamp truncated to its second, then floored to its day. +DayMemo kept_day(std::int64_t ts) { + const std::int64_t secs = ts / 1000; + const std::int64_t day = secs / 86400 - (secs % 86400 < 0 ? 1 : 0); + return {day * 86400, day * 86400 + 86400, reference_day_key("UTC", ts)}; +} + +bool memo_is(const MemoInPlace& memo, const DayMemo& want, const char* zone, std::int64_t ts) { + const DayMemo got = memo.read(); + if (got == want) return true; + if (failures < 25) { + std::fprintf(stderr, " zone '%s' after ts_ms=%" PRId64 ": memo [%" PRId64 ",%" PRId64 + ") key %" PRId64 ", want [%" PRId64 ",%" PRId64 ") key %" PRId64 "\n", + zone, ts, got.lo, got.hi, got.key, want.lo, want.hi, want.key); + } + return false; +} + +void memo_witness() { + const std::int64_t mar_15 = 1710460800000LL; // 2024-03-15 00:00 UTC + const std::int64_t mar_20 = 1710892800000LL; // 2024-03-20 00:00 UTC + const std::int64_t dec_31_1969 = -kDay; // 1969-12-31 00:00 UTC + const std::int64_t read_at = mar_15 + kHour; + const DayMemo planted{mar_20 / 1000, mar_20 / 1000 + 86400, kPlantedKey}; + bool offsets_held = false; + // The UTC spellings first: their kept days hold the offsets the chart + // timezones' plants use. + for (const char* zone : {"", "UTC", "Etc/UTC", "America/New_York", "Europe/London", + "Asia/Tokyo", "EST5EDT,M3.2.0,M11.1.0", "UTC+05:30"}) { + KeyHost kept; + kept.stage(zone, true); + KeyHost computed; + computed.stage(zone, false); + CHECK(kept.key(read_at) == reference_day_key(zone, read_at)); + CHECK(computed.key(read_at) == reference_day_key(zone, read_at)); + CHECK(computed.holds_cache() == false); + const MemoInPlace memo(kept); + CHECK(memo.held()); // the index D2-C adopts at the run's begin + if (!memo.held()) continue; + if (utc_spelling(zone)) { + // Kept: each read keeps its whole floor day and key -- a day + // before the epoch too, where the stamp truncates towards zero + // and the day floors below it. + bool kept_whole = memo_is(memo, kept_day(read_at), zone, read_at); + CHECK(kept_whole); + for (std::int64_t ts : {dec_31_1969 + 12 * kHour, read_at}) { + if (!kept_whole) break; + CHECK(kept.key(ts) == reference_day_key(zone, ts)); + kept_whole = memo_is(memo, kept_day(ts), zone, ts); + CHECK(kept_whole); + } + if (!kept_whole) continue; // never plant through offsets that do not hold + offsets_held = true; + memo_days_kept += 3; + // Read: a key planted over a day answers its first to its last + // millisecond, and those reads write nothing. + memo.plant(planted); + for (std::int64_t ts : {mar_20, mar_20 + 12 * kHour + 34567, mar_20 + kDay - 1}) { + const bool answered = kept.key(ts) == kPlantedKey; + CHECK(answered); + if (answered) ++memo_planted_answers; + } + CHECK(memo_is(memo, planted, zone, mar_20)); + // ... and no second outside it: the first millisecond after and + // the last before compute their own key and keep their own day. + for (std::int64_t ts : {mar_20 + kDay, mar_20 - 1}) { + memo.plant(planted); + const bool computes = kept.key(ts) == reference_day_key(zone, ts); + CHECK(computes); + CHECK(memo_is(memo, kept_day(ts), zone, ts)); + if (computes) ++memo_computed_reads; + } + } else { + // A chart timezone keeps nothing, and never answers a key planted + // over the day it reads. + CHECK(memo_is(memo, DayMemo{}, zone, read_at)); + if (!offsets_held) continue; + const DayMemo over{mar_15 / 1000, mar_15 / 1000 + 86400, kPlantedKey}; + memo.plant(over); + const bool computes = kept.key(read_at) == reference_day_key(zone, read_at); + CHECK(computes); + CHECK(memo_is(memo, over, zone, read_at)); + if (computes) ++memo_computed_reads; + } + } +} + } // namespace int main() { zone_battery(); utc_edges(); day_ledger_runs(); + memo_witness(); std::printf("test_chart_day_memo: %lld keys against the 6c081f5d body (%lld on chart " "timezones), %lld day-ledger runs (%lld source folds, %lld intraday-loss closes) " "kept == computed\n", battery_keys, zoned_keys, ledger_runs, ledger_folds, loss_closes); + std::printf("test_chart_day_memo: the memo in place: %lld UTC days kept whole, %lld reads " + "answered by a key planted over their day, %lld reads outside it or on a chart " + "timezone computed\n", + memo_days_kept, memo_planted_answers, memo_computed_reads); CHECK(battery_keys > 3000000 / kStride); CHECK(zoned_keys > 100000 / kStride); CHECK(ledger_runs == 70); CHECK(loss_closes > 0); + CHECK(memo_days_kept == 9); + CHECK(memo_planted_answers == 9); + CHECK(memo_computed_reads == 11); if (failures == 0) { std::printf("test_chart_day_memo: ok (%lld checks)\n", checks); return 0; diff --git a/tests/test_native_runtime_ambient_lifo.cpp b/tests/test_native_runtime_ambient_lifo.cpp new file mode 100644 index 00000000..d1b112b0 --- /dev/null +++ b/tests/test_native_runtime_ambient_lifo.cpp @@ -0,0 +1,212 @@ +// R5 lane H-DOCGATES (AUDIT4 N-7): the runtime blocks leave in the reverse +// order they came, and a library built with its debug checks holds that +// order. +// +// install_runtime_ambient makes a pump's block the calling thread's block in +// force and answers the block it covers; uninstall_runtime_ambient copies the +// block into the one it covered and makes that one current again +// (src/runtime_ambient.hpp). Pumps nest, so blocks must leave last in, first +// out. Where the library is compiled without NDEBUG -- a Debug build, as the +// debug and sanitizers profiles are -- uninstall_runtime_ambient aborts when +// the block leaving is not the one in force (src/ta_extremes_volume.cpp, R5 +// INT23); a build with NDEBUG pays nothing for the order and does not check +// it. This row holds both: +// +// 1. In process, the legal order: an outer block, an inner block over it, +// the inner leaving, then the outer. Nothing aborts; each block starts +// from the values in force, and each block's writes reach the block it +// covered, down to the thread's own. +// 2. In a forked child, the outer block leaving while the inner is in +// force. Where the library checks the order +// (PINEFORGE_LIBRARY_DEBUG_CHECKS=1, which tests/CMakeLists.txt sets +// from the build type: this TU strips NDEBUG, as every test does, so it +// cannot tell by itself) the child must die of SIGABRT inside that call. +// Otherwise the call must return, leaving the thread's own block in +// force with the outer block's values and the inner block's write lost: +// the silent state loss the debug check exists to stop. +// +// Fail-before: with the check deleted from uninstall_runtime_ambient (the +// tree before INT23's 09ced53e), a Debug build fails 2 -- the child returns; +// the lane report records it. +// +// Source-free: this TU runs in the kernel-only profile. POSIX (fork, pipe, +// waitpid), as test_native_calendar.cpp is. +#include "../src/runtime_ambient.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#ifndef PINEFORGE_LIBRARY_DEBUG_CHECKS +#error "tests/CMakeLists.txt defines PINEFORGE_LIBRARY_DEBUG_CHECKS from the build type" +#endif + +using namespace pineforge::internal; + +namespace { + +int failures = 0; +long checks = 0; + +#define CHECK(cond) \ + do { \ + ++checks; \ + if (!(cond)) { \ + ++failures; \ + std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + } \ + } while (0) + +// 1. The legal order. +void blocks_leave_in_reverse_order() { + RuntimeAmbient* const own = &runtime_ambient(); // nothing installed yet + const RuntimeAmbient saved = *own; + own->bar_context.bar_index = 3; + RuntimeAmbient outer{}; + RuntimeAmbient inner{}; + + RuntimeAmbient* const covered_by_outer = install_runtime_ambient(outer); + CHECK(covered_by_outer == nullptr); // it covers the thread's own + CHECK(&runtime_ambient() == &outer); + CHECK(outer.bar_context.bar_index == 3); + runtime_ambient().bar_context.bar_index = 7; + + RuntimeAmbient* const covered_by_inner = install_runtime_ambient(inner); + CHECK(covered_by_inner == &outer); + CHECK(&runtime_ambient() == &inner); + CHECK(inner.bar_context.bar_index == 7); + runtime_ambient().bar_context.bar_index = 11; + + uninstall_runtime_ambient(inner, covered_by_inner); + CHECK(&runtime_ambient() == &outer); + CHECK(outer.bar_context.bar_index == 11); + + uninstall_runtime_ambient(outer, covered_by_outer); + CHECK(&runtime_ambient() == own); + CHECK(own->bar_context.bar_index == 11); + + *own = saved; + std::printf("legal order: outer, inner, inner out, outer out -- no abort, each write " + "copied back down to the thread's own block\n"); +} + +void write_all(int fd, const void* data, std::size_t size) { + const char* at = static_cast(data); + while (size > 0) { + const ssize_t n = ::write(fd, at, size); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) ::_exit(3); + at += n; + size -= static_cast(n); + } +} + +// The child: 'B' down the pipe before the out-of-order call; if the call +// returns, 'R', the block then in force ('o' the thread's own, 'u' outer, +// 'i' inner) and the bar index it holds. +[[noreturn]] void leave_out_of_order(int fd) { + const struct rlimit no_core = {0, 0}; + (void)::setrlimit(RLIMIT_CORE, &no_core); // the abort asked for leaves no core file + RuntimeAmbient* const own = &runtime_ambient(); + RuntimeAmbient outer{}; + RuntimeAmbient inner{}; + RuntimeAmbient* const covered_by_outer = install_runtime_ambient(outer); + runtime_ambient().bar_context.bar_index = 7; + (void)install_runtime_ambient(inner); + runtime_ambient().bar_context.bar_index = 11; + write_all(fd, "B", 1); + uninstall_runtime_ambient(outer, covered_by_outer); // the outer leaves first + write_all(fd, "R", 1); + const RuntimeAmbient* const now = &runtime_ambient(); + const char in_force = now == own ? 'o' : now == &outer ? 'u' : now == &inner ? 'i' : '?'; + write_all(fd, &in_force, 1); + const long long bar_index = now->bar_context.bar_index; + write_all(fd, &bar_index, sizeof bar_index); + ::_exit(0); +} + +// 2. The outer block leaving first. +void out_of_order_leave() { + int fds[2]; + CHECK(::pipe(fds) == 0); + std::fflush(nullptr); + const pid_t pid = ::fork(); + CHECK(pid >= 0); + if (pid < 0) return; + if (pid == 0) { + ::close(fds[0]); + leave_out_of_order(fds[1]); + } + ::close(fds[1]); + std::string told; + for (;;) { + char chunk[64]; + const ssize_t n = ::read(fds[0], chunk, sizeof chunk); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) break; + told.append(chunk, static_cast(n)); + } + ::close(fds[0]); + int status = 0; + pid_t waited; + do { + waited = ::waitpid(pid, &status, 0); + } while (waited < 0 && errno == EINTR); + CHECK(waited == pid); + CHECK(!told.empty() && told[0] == 'B'); // the child reached the call + const bool aborted = WIFSIGNALED(status) && WTERMSIG(status) == SIGABRT; + const bool returned = WIFEXITED(status) && WEXITSTATUS(status) == 0; + if (PINEFORGE_LIBRARY_DEBUG_CHECKS) { + CHECK(aborted); + CHECK(told == "B"); // nothing after the call: it died inside it + if (aborted && told == "B") { + std::printf("out of order, debug checks compiled in: the child died of SIGABRT " + "inside uninstall_runtime_ambient\n"); + } else { + std::fprintf(stderr, " out of order, debug checks compiled in: the child %s " + "(%zu byte(s) told) instead of dying of SIGABRT inside the call\n", + returned ? "returned" : "ended otherwise", told.size()); + } + } else { + CHECK(returned); + CHECK(told.size() == 3 + sizeof(long long)); + long long bar_index = -1; + const char in_force = told.size() > 2 ? told[2] : '?'; + if (told.size() == 3 + sizeof(long long)) + std::memcpy(&bar_index, told.data() + 3, sizeof bar_index); + CHECK(told.size() > 1 && told[1] == 'R'); + CHECK(in_force == 'o'); // the thread's own block is in force again, + CHECK(bar_index == 7); // holding the outer block's value: 11 is lost + std::printf("out of order, no debug checks: the call returned (release builds pay " + "nothing); block in force '%c' holds bar_index %lld, the inner block's " + "11 lost\n", in_force, bar_index); + } + if (!aborted && !returned) { + std::fprintf(stderr, " child status 0x%x: %s %d\n", static_cast(status), + WIFSIGNALED(status) ? "signal" : "exit", + WIFSIGNALED(status) ? WTERMSIG(status) : WEXITSTATUS(status)); + } +} + +} // namespace + +int main() { + std::printf("test_native_runtime_ambient_lifo: library debug checks %s\n", + PINEFORGE_LIBRARY_DEBUG_CHECKS ? "compiled in (Debug)" : "compiled out"); + blocks_leave_in_reverse_order(); + out_of_order_leave(); + if (failures != 0) { + std::fprintf(stderr, "test_native_runtime_ambient_lifo: %d of %ld checks failed\n", + failures, checks); + return 1; + } + std::printf("test_native_runtime_ambient_lifo: ok (%ld checks)\n", checks); + return 0; +} From daae552adc9979df2ddf93214590eaf9ed3fc3d5 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 03:52:58 +0800 Subject: [PATCH 15/83] Docs: the deferred-performance inventory, the witness drift and the next script-ABI epoch's schedule are written down; three comments say what the code does (R5 lane H-DOCGATES, X17) AUDIT4-opus X17 (perf N-1, N-4, N-5, N-6, N-9, N-10; K1-9; L4-10), the documentation half. Comment-only for the three C++ files: their code and literals are token-identical to HEAD's (scripts/cxxlex.py), and the Release objects of src/native_calendar.cpp (which includes native_calendar.hpp) and src/source/pine_strategy_host.cpp (which includes pine_adapter.hpp) are byte-identical before and after (sha256 ccd2c0b06f48..., c9ce59093717...). - TLS wording: pine_strategy_host.cpp said D2-C left "no thread-local access per bar"; it left no thread-local WRITE. The comment now names the readers that still reach the block through the thread's pointer once per call (the extremum rings behind ta.highest / lowest / highestbars / lowestbars / stoch / wpr / range; the day partition behind the default ta.vwap, crosses_boundary(DAY) and the session-period helpers), measured 1.000 access a bar per call by AUDIT4. - K1: native_calendar.hpp claimed every instant a held session day holds() keys to its ordinal; America/Sitka and America/Juneau in October 1867 under "2330-2300" hold 30 and 45 instants (all out of session) whose own session_day_at carries another ordinal. The comment states the guarantee where the cycles tile and names the exception. - L4: the two retired receipt-reader slots and their waivers are scheduled for the next engine_script_run epoch (slot comment, both waiver rows, and the progress page). - docs/native-refactor-progress.md "Deferred performance work": D2-B, the PERF-D3 S1a / S1b / S1c / S2 / S2e rows and the quiet-run loop, PGO, L3's compact records, checked-values handoff and chunked trades_ store, the zoned chart-day memo, and the thread-local readers that remain -- each with its measured size (lane, host, tool), the ledger ruling that deferred it, where its prototype lives (perf-d2/proto-throwaway 92e85571, perf-d3/proto-throwaway a820b7df, r5/l3 b33ea659) and what reopens it; "audit-off mode", "LTO" and "data-oriented layout" are struck until a design note defines them. - "Recorded witness drift": K24's pre-size is dormant under Window retention; PERF-L4 reads the receipts 3.09 times a bar, not once (test_adapter_quiet_bar_probe on this tree: ReceiptRead asked 40996, 81.8 % stopped at the gate, against 13248 for a once-per-bar gate); D2-C's carry counts moved 479/397 -> 489/412 at dbcca850 (bisected on macOS), and x86-64 now prints the Mac's numbers after this lane's one-draw-per-statement fix. - The page joins check_doc_anchors.py's published set, so its 28 new citations are held; the pages' anchors that the comment lines shifted are re-anchored (native-engine.md, the ADR, the design record). No sentence is deleted: the pages gain sections, and the re-anchors change digits only. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 2201c1ac15ef463a8f75867651ee96c45e993f04) INT26 pick note: the ADR, design and native-engine pages conflicted on citation digits only (int16_digit_merge per line, digits carried from the pre-pick tree; one ADR citation of pine_strategy_host.cpp both sides moved taken additively, 1925 -> 1931, re-judged at INT26 item vii). hunkcheck: every hunk of 2201c1ac carried; two pages differ in anchor digits only. --- docs/adr/0001-kernel-adapter-boundary.md | 4 +- docs/design/native-feature-parity.md | 8 +- docs/native-refactor-progress.md | 89 +++++++++++++++++++++++ docs/pages/native-engine.md | 2 +- include/pineforge/native_calendar.hpp | 15 ++-- include/pineforge/source/pine_adapter.hpp | 4 +- scripts/broker_state_hash_waivers.txt | 4 +- scripts/check_doc_anchors.py | 2 + src/source/pine_strategy_host.cpp | 8 +- 9 files changed, 120 insertions(+), 16 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 16cb0cbf..17808098 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -245,7 +245,7 @@ kernel and own these quirks, each at its site: quantities leave no remnant row on a `lab tv` tape, which the Pine host books row for row while a bare host books four 2.8e-17-unit dust rows beside them. - **Close reservations.** `strategy.close` callsite batching, two-call provenance and the entry-id - ledger (`close_logical_units_` `pine_adapter.hpp:2023-2034`); the POOC reservation-growth population predicate + ledger (`close_logical_units_` `pine_adapter.hpp:2013-2024`); 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 @@ -337,7 +337,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:454-460`). `strategy.close` batching and the entry-id ledger - are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2023-2034`) and + are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2013-2024`) 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:424`); what enables the generic model is the presence of `NativeRunSpec::margin`. So is the string-sentinel decoding of adapter-written comments: diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 215bf30b..b1f78952 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -50,7 +50,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2737-2748; `entry()` pine_adapter.cpp:5289, `order()` pine_adapter.cpp:10480) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5685-5732; `submit` native_host.hpp:1201 | 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:8343 → `Flatten{}` pine_adapter.cpp:8372-8373; `Reduce{ExplicitUnits}` pine_adapter.cpp:7490-7499; `ReverseTo` pine_adapter.cpp:6324-6325) | **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:2707-3914 → `host.replace` pine_adapter.cpp:2723-2803) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4722-4768, `cancel_all` pine_adapter.cpp:18032-18047; `live_by_source_key_` pine_adapter.hpp:1995; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4722-4768, `cancel_all` pine_adapter.cpp:18032-18047; `live_by_source_key_` pine_adapter.hpp:1985; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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:4189; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7175-7526) | **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:5513-5548, `cohort_for` pine_adapter.cpp:2501; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7473-7490) | **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:1270, `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:2746-2752) | **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 | — | @@ -163,7 +163,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | | RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9280, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | | RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1358, native_execution_consumer.cpp:10682; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | -| RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:538; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1925 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.8 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | +| RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:538; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1931 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.8 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:411 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:405 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:411 is its deprecated spelling, which the generic default forwards to (`tests/test_native_host_hash_extension.cpp`). | K | L2 (the virtual rides the shared bump) | F:I3 O:J1 S:X3 | — | ### 1.9 Other runtime surface @@ -319,7 +319,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:1730, `guard_native_mutation` engine_aux_security.cpp:78 | 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). No kernel TU includes a source header | `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13 | 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:739 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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:548; the ledger `close_logical_units_` pine_adapter.hpp:2023-2034; `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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:548; the ledger `close_logical_units_` pine_adapter.hpp:2013-2024; `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) | 58 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 58 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:13933f70987f6bfe37d3015ae065a8d5ee5937c37431d48e2ecd1f17a4c29958` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:862, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:590-709, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:515-567, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:494-511, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:802-833, c_abi.cpp:929 | 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 | @@ -1254,7 +1254,7 @@ unchanged after it. | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5757) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290); 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:16118-16147) | 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:1951) | 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 | +| E6 | **Double equity recording.** The source host already records per bar (history: only its cadence remains, `scheduler_mark_report_point` pine_strategy_host.cpp:1957) | 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:333) 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:46-78, *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:1620; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | | E9 | **TA numerics are TV-calibrated** (1e-10 band in DMI / MFI / percentrank; conditional-call extremum ring `ExtremeRing` ta.hpp:134) | Non-Pine users may expect IEEE semantics; changing them breaks parity | Rename only (§2.ii b). For an every-bar caller the ring equals a positional window (the ring comment `sha256:fd1f92206558863ad8df33fd3ef3b8cc8b9bd6678a25fc98ca866168c1a81aca` ta.hpp:87-88). Document. → U6 | F O S | diff --git a/docs/native-refactor-progress.md b/docs/native-refactor-progress.md index 54a931a0..bf42c117 100644 --- a/docs/native-refactor-progress.md +++ b/docs/native-refactor-progress.md @@ -159,6 +159,95 @@ coordinators retain their preflight order and observe only committed rows. These completed R3 milestones do not make an R4 acceptance claim. R4-B remains in implementation as recorded above. +## Deferred performance work + +The R5 performance wave stopped at the supervisor's cap ("R5 SUPERVISOR RULING +2026-09-25 00:15 Taipei") and the owner's decision ("OWNER DECISION 2026-09-25 +00:20 Taipei"), both in the campaign ledger. The items below were measured or +designed and then deferred, and none of them is implemented. Each figure is the +named lane's own, on the host and with the tool given. A ratio is candidate +over base, so below 1 is faster, and "off / on" is the magnifier. Rulings are +quoted by their ledger title. Lane reports and scratch trees sit in the +campaign's evidence tree (`$EV/exec/`). Prototype branches are local to this +repository and were never pushed. + +| 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:263`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:5175`), which still runs about three times a bar | PERF-D2, spark 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 | Branch `perf-d2/proto-throwaway` (`92e85571`); `$EV/exec/PERF-D2-scratch/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, spark 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)" | Branch `perf-d3/proto-throwaway` (`a820b7df`, five commits on `b33ea659`); `$EV/exec/PERF-D3-scratch/patches/`; spark `~/perf-d3/` | 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:1029`) 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" | `$EV/exec/PERF-D3-scratch/patches/hold-ub-measurement-only.py` | As S1a. Size L, parity risk high: TradingView's margin money rules | +| S2 and S2e, the codegen feature manifest | Codegen states which per-bar hooks and publications a strategy can reach, and the engine skips the others; no manifest means every feature. A median 39 of 52 are statically unreachable, over the 312 corpus strategies and the 100 slots alike. S2e adds two gates that stop the Pine risk ledgers of a strategy with no risk rule | PERF-D3, on top of S1: floor −14 % / −15 % in time (−6.4 % in instructions), 14 probes 0.943 / 0.934, 100 slots 0.942 / 0.945. The nine S2 gates are byte-identical. S2e adds about 2–3 % and moves the adapter hash domain `pineforge-source-adapter/v4` to its next version | As S1a; the supervisor's cap: "any structural S1/S2 redesign is a post-release epoch" | In `a820b7df` (a regex-based manifest scanner); `$EV/exec/PERF-D3-scratch/patches/` | A codegen and engine co-change with a corpus re-emission; S2e also needs an owner decision on the hash-domain step | +| The kernel quiet-run loop and the Pine quiet calculation | A loop for consecutive quiet bars of a pumped batch at the chart's own timeframe (label and gap checks in the batch preflight, precomputed intervals and session facts, one calculation frame); and skipping a flat bar's post-script hooks when its script issued nothing | Estimated, not prototyped (PERF-D3's report, section 3.7): about 1,000 instructions a bar for the loop and 800 for the quiet calculation, taking the floor from 3,726 instructions (S1, S2 and D2-A) to about 1,900, or 180–200 ns a bar | As S1a (PERF-D3's follow-up row) | The PERF-D3 report only | After S1a and S1b | +| PGO | Profile-guided optimisation of the library objects: an instrumented build, a training run over the bench strategies, a `-fprofile-use` rebuild and the identity gates, per compiler and target, with a staleness gate on the profile | LAYOUT1, spark A725, GCC 13.3, `perf stat`, trained on 86 slots and timed on the 14 held-out probes: 14.5 % fewer cycles, CPU time 0.865, 7 % fewer instructions, 26 % fewer L1i refills, 36 % fewer front-end stalls, layout spread 9.0 % → 5.1 %, fingerprints identical | "R5 LAYOUT1 DONE + ACCEPTED 2026-09-24 21:45 Taipei": "PGO APPROVED as a lane (PGO1) scheduled LAST in the perf wave after the code lanes (profile staleness)"; the supervisor's cap: "PGO folds into the release lane"; the owner: "release with PGO in the release build"; TRIAGE1 §D | `$EV/exec/LAYOUT1-scratch/`; spark `~/layout1/results/`. The tree carries only LAYOUT1's `-falign-functions=64` | The release lane. BENCH4 timed the standard Release build without PGO, and neither BENCH4's nor REL10-ENGINE's brief names it | +| 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:1651`) hands out the whole vector and the core holds `history_` (`include/pineforge/native_order.hpp:2126`) inside `native_order_v7`, so both are a layout and API change | L3, spark 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" | `r5/l3` (`b33ea659`, merged by INT22); `$EV/exec/L3-scratch/`; spark `~/l3/prof` | A `native_order` layout epoch | +| A checked-values handoff for executions (L3) | `execution_values` runs twice, in `check_execution` (`src/native_order.cpp:4764`) and again in `apply_execution` (`src/native_order.cpp:4778`); 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:13512`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:15893`) reads `chart_day_key` (`src/source/pine_adapter.cpp:16048`) unconditionally. The non-UTC monthly Sharpe/Sortino walk, `month_key_local` (`src/engine_metrics.cpp:32`), pays one `localtime_r` per equity point. UTC charts use D2-D's memo, `civil_chart_day_key` (`src/source/pine_adapter.cpp:13489`) | Not measured on a zoned chart. The UTC path cost 134 instructions a bar before D2-D's memo and 68 after (D2-D, spark, 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:438`). 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:563`), 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:258`) reads two thread-locals per call on a zoned clock, and the prepared order storage's `free_blocks` (`src/native_order.cpp:588`) 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 (spark 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 | `$EV/exec/AUDIT4-opus-scratch/perf/tls/` | A byte-identical lane that hands these readers the pump's block without a thread-local read. `ta::bar_context()` is script ABI, so its public declaration stays as it is | +| "audit-off mode", "LTO", "data-oriented layout" | Named in "OWNER DECISION 2026-09-25 00:20 Taipei" among the post-release structural items: "S1 bulk quiet bars, S2 per-strategy specialization, audit-off mode, LTO+PGO, data-oriented layout". No engine or campaign document defines, designs or measures the three | None | The owner decision | None | Struck from this inventory until a design note defines one of them | + +### Recorded witness drift + +**PERF-K24's pre-size is dormant under Window retention.** PERF-K24's report +(finding 1) says of the Pine replay that "most of the replay gain comes from" +K4, the batch log pre-size. Since V19-B's step 6 (`08e71bae`) a run under the +default `NativeEventRetention::Window` keeps no driver point and retires its +journal at every script-bar boundary, so K4 has nothing to size. +`reserve_driver_log` (`src/native_execution_consumer.cpp:2538-2543`) returns +unless the retention is Full, and `presize_logs` +(`src/native_execution_consumer.cpp:8837-8848`) sizes no journal under Window. +`NativeRunSpec::event_retention` (`include/pineforge/native_run_spec.hpp:639`) +defaults to Window, and the Pine adapter declares +`NativeEventRetention::Window` (`src/source/pine_adapter.cpp:2429`). 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 spark) describes the tree before V19-B. In +AUDIT4's probe an idle Window run holds 0 driver points and no block of 1 MiB +or more, and the same run under Full holds 66,000 points in one block of +7,509,264 bytes. + +**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:5175`) returns at its receipt gate when no +command lies above its cursor. `every_gate_is_taken_both_ways` +(`tests/test_adapter_quiet_bar.cpp:1036`) prints +`gate ReceiptRead asked 40996 quiet 33552 live 7444`, against `asked 13248` +for every gate asked once per bar open, so the adapter reads 3.09 times a bar +and 82 % of those reads stop at the gate (macOS arm64, this tree). PERF-L4's +report does not list the narrowing as a deviation. The once-per-bar read +belongs to the deferred D2-B above. + +**D2-C's carry witness prints counts that moved at `dbcca850`.** +`rewritten_quotes_carry_alike` (`tests/test_native_settlement_carry.cpp:281`) +prints `rewriting hosts: 12 configurations, fee inputs or rates rewritten, + settlements carried across them`. The row checks only that a carried run +and a re-staged run agree, so both counts are printed, not pinned. D2-C and +INT23 recorded 479 and 397. B-ADAPTER's item 5, `dbcca850` ("a current +execution converts at its cursor's rate, threaded; it neither writes nor +restores the host's clock"), moved them to 489 and 412, and K-ULP1's +`deb344b1` did not. Bisected on macOS arm64 with AppleClang 17, Release: 479 +and 397 at `6f4ca1a9`, `deb344b1` and `4b00da92`; 489 and 412 at `dbcca850`, +`91d65ad6` and later. On x86-64 GCC 13 the row printed 491 and 407, and its +`l2 hosts` line 1066 of 1308 where the Mac reads 1003 of 1249, because its +fixture drew twice inside one argument list at two call sites and GCC on +x86-64 evaluates such arguments in the other order. R5 lane H-DOCGATES took +those draws one per statement, left to right, and `scripts/check_rng_draw_order.py` +keeps the class out: x86-64 GCC 13 (Docker `xplat1-gcc13:amd64`) now prints the +Mac's 489 and 412 and 1003 of 1249, and no Mac output moved. + +### Scheduled for the next script-ABI epoch + +- **PERF-L4's retired reader slots.** `event_high_water_reader_` and + `terminal_receipt_high_water_reader_` + (`include/pineforge/source/pine_adapter.hpp:1917-1918`), 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 + `engine_script_run` epoch, with their two rows in + `scripts/broker_state_hash_waivers.txt`, which say so. + ## Publication and acceptance Publish reviewed increments in an evolving draft PR so implementation and diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 736a733b..7a335fd7 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1905,7 +1905,7 @@ whole days, so all four are true. Every callback of one script bar carries the same four, fills and fill recalculations included, and a C host reads the first three from `pf_native_decision_v1`'s session bytes (`native_c_api.h:1454`). The kernel resolves each session day once through -`native_calendar::session_day_at` (`native_calendar.hpp:407`), which a host +`native_calendar::session_day_at` (`native_calendar.hpp:412`), which a host may call too. `tests/test_native_session_day_facts.cpp` replays the TradingView session tapes through a bare host. It is a presentation snapshot copied onto the callback stack: writing to it cannot move the floor, the diff --git a/include/pineforge/native_calendar.hpp b/include/pineforge/native_calendar.hpp index af59391d..8cca5470 100644 --- a/include/pineforge/native_calendar.hpp +++ b/include/pineforge/native_calendar.hpp @@ -383,11 +383,16 @@ std::optional period_key(const SessionCalendar& calendar, const Timefra // [origin_ms, next_origin_ms) that holds it, the session-day ordinal every // instant of that cycle keys to, and the cycle's in-session spans in epoch ms // ([first, second), ascending and disjoint; none on a masked or empty day). -// For every instant t the day holds(), in_session(calendar, t) is -// in_session_at(t) and session_day_ordinal(calendar, t) is `ordinal`, so a -// caller asking about many instants of one day resolves the day once rather -// than once per instant. For the queried instant itself both answers hold -// even when holds() is false (a clock-change fold the cycles do not tile). +// Where the session cycles tile the timeline, for every instant t the day +// holds(), in_session(calendar, t) is in_session_at(t) and +// session_day_ordinal(calendar, t) is `ordinal`, so a caller asking about many +// instants of one day resolves the day once rather than once per instant. +// A clock change that moves the local date breaks the tiling: America/Sitka +// and America/Juneau in October 1867 under "2330-2300" hold instants (30 and +// 45, all out of session) whose own session_day_at(t) carries another +// ordinal, so a caller that needs exact ordinals there re-resolves each +// instant. For the queried instant itself both answers hold even when +// holds() is false (a clock-change fold the cycles do not tile). struct NativeSessionDay { int64_t origin_ms = 0; diff --git a/include/pineforge/source/pine_adapter.hpp b/include/pineforge/source/pine_adapter.hpp index 57364c2a..3bc46231 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -1919,7 +1919,9 @@ class PineExecutionAdapter { // host is a PineStrategyHost". The readers ask the host's // PineStrategyHost view instead, so nothing reads or writes these two // slots; they keep this class's layout, which is part of - // PineStrategyHost's and so of the generated script's ABI. + // PineStrategyHost's and so of the generated script's ABI. Scheduled: + // both slots and ReceiptHighWaterReader go at the next engine_script_run + // epoch, the layout change that epoch licenses (R5 lane H-DOCGATES). ReceiptHighWaterReader event_high_water_reader_ = nullptr; ReceiptHighWaterReader terminal_receipt_high_water_reader_ = nullptr; bool is_declined_market_reversal( diff --git a/scripts/broker_state_hash_waivers.txt b/scripts/broker_state_hash_waivers.txt index aa41673e..c8be3efc 100644 --- a/scripts/broker_state_hash_waivers.txt +++ b/scripts/broker_state_hash_waivers.txt @@ -46,8 +46,8 @@ syminfo_mintick_ # Metadata convenience scalar; executable levels/terms retain t trade_start_time_ # Configured execution-window boundary; native admission receives the projected boundary before requests exist. run_counter_ # run-scoped; excluded for reuse invariance (ab9714be test_script_run_prepare:111) source_last_bar_index_ # derived script-input horizon, not broker state: a prefix run's per-bar hashes must equal the full run's (test_live_state_hash_recording_l4d:69; ab9714be behaviour); folding it would differ before any behaviour does -event_high_water_reader_ # Immutable callback binding to the host's native event watermark, not mutable broker state. -terminal_receipt_high_water_reader_ # Immutable callback binding to the host's terminal-receipt watermark, not mutable broker state. +event_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. +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. diff --git a/scripts/check_doc_anchors.py b/scripts/check_doc_anchors.py index 7091be6b..7bb76433 100644 --- a/scripts/check_doc_anchors.py +++ b/scripts/check_doc_anchors.py @@ -200,6 +200,8 @@ 'docs/pages/*.md', 'docs/adr/*.md', 'docs/design/*.md', + # R5 lane H-DOCGATES: the deferred-performance inventory cites the tree too. + 'docs/native-refactor-progress.md', ) SEARCH_DIRS = ( diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index 11fd37df..182acfc9 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -1813,7 +1813,13 @@ void source::PineStrategyHost::scheduler_publish_source_bar( } // The chart day partition, the EMA seeding default and the TA bar context // this publication runs under, written on the pump's runtime block (R5 - // lane D2-C: no thread-local access per bar; runtime_ambient.hpp). + // lane D2-C: no thread-local WRITE per bar; runtime_ambient.hpp). The + // readers still reach the block through the thread's pointer, one + // thread-local access per call: every extremum ring behind ta.highest / + // lowest / highestbars / lowestbars / stoch / wpr / range, and the day + // partition behind the default-anchored ta.vwap, crosses_boundary(DAY) and + // the session-period helpers (docs/native-refactor-progress.md, "Deferred + // performance work"). internal::RuntimeAmbient* const ambient = NativeExecutionConsumer::bound(*this).pump_ambient(); internal::AmbientDayPartitionScope chart_day_partition( ambient, chart_day_partition_.empty() ? nullptr : &chart_day_partition_); From c5c611a3a0dafd8b8d0ac6975e4d22787a05c141 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:46:39 +0800 Subject: [PATCH 16/83] Docs+Gates: every kernel seam and every kernel member the source layer writes gets an ADR-0001 row, the account of TradingView-justified comments is complete, and a gate holds the inventory (R5 lane H-DOCGATES, X10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUDIT4-opus X10 found the ADR contradicting itself and the tree. Each sentence was re-derived on this tree: - The examples sentence claimed every C++ host includes only native_host.hpp. Three include native_toolkit.hpp or native_module.hpp (both include native_host.hpp) and the two C hosts include pineforge.h; "each including only `` (or `native_c_api.h`)" is rewritten to say so. - The build_close_trade_with_costs row still said "One question stays open, recorded at the site: the net basis is applied to a host-owned excursion's magnitudes too (RULING A48's hook)". R5 lane F3 closed it in 684800bf; the row now says so and scopes the net basis to the kernel's own sampled excursions. - The neutral-spellings row said map.hpp and drawing.hpp are neutral names with deprecated pine_* aliases, "already ruled: "Neutral spellings" above" -- a section that does not exist, contradicting the residual table's PineMap row (primaries with no neutral alias). The row now names both cases and cites the two residual rows. - "They do not yet account for the whole population" (the paragraph ending "until each gets a row or moves") and the Consequences bullet "Not yet fully accounted for" contradicted rule 2's "no site left unruled". Four Section B rows close the eighteen unruled lines the audit's closure grep found (the chart-day partition, the bar-time accessors, the offset sign, the grid and lot helpers); the paragraph now records the re-taken census (369 comment lines in 44 of 92 files, none unruled) and the bullet says the count is held by reading, not by a gate. Rule 2 pointed at "the section below"; the section is above. - Rule 1 now names the generated-code language runtime and its admission test, which the three category rows apply. - The subscriptions row listed four exclusions ("the sites the predicate leaves out (lower timeframe, lookahead, auxiliary, streams) keep the source evaluator"); declare_security_sites_to_kernel has fifteen and declares every site or none. The row lists them. - Fourteen new rows in "Kernel state the adapter sets" cover every seam and member the new gate finds, including the two whole-struct current_bar_ writes (E21 check e), broker_fill_event_seq_, the dead source_stream_entry_comment (scheduled for the next script-ABI epoch in docs/native-refactor-progress.md), fold_exit_trail_peak_, NativeHostCache and the rows AG1/AG2 re-date. The dust sweep is recorded OPEN for lane H-THIN (A4-DUST); the position_entry_count_ overwrite is recorded OPEN. - Design row RP5 said the short-seed swaps are "measured in §3.7 part 2"; no row there measures them, and it now says so. The second "### 3.8" (lane F9) is §3.9, and RP9's "§3.8 records the old quadratic adapter fold" now points at it. - Two engine.hpp comments were false: "Generated code reads them for Pine's bare ``hour``" (the transpiler emits pine_hour(..., syminfo_.timezone); no corpus strategy names a _bar_* accessor) and script_tf_'s writers "(both sites live in engine_run.cpp)" (the one writer is the Pine host). Comment-only, line-count preserving: all 86 library objects are byte-identical with and without the edit. scripts/check_kernel_seam_rows.py (a binding ci_preflight stage, with its must-fail suite as ci_preflight stage and CTest row test_kernel_seam_rows) fails when a kernel `virtual source_*` seam, a BacktestEngine member the source layer writes, or a Trade / PyramidEntry field written through the kernel's rows has no ADR-0001 row naming it in its first cell. Over this commit's parent it reports 43 without a row; here 0. It reads class bodies itself: check_broker_state_hash_coverage.py loses a member declared right after an inline function body (current_bar_, bar_magnifier_enabled_, trace_buffer_). KERNEL_MIN_TESTS 274 -> 275, RELEASE_MIN_TESTS 675 -> 676. Sentences deleted or rewritten, by the commit that wrote them: ca0aac5 (INT16's examples sentence), 2757095 (P2's lead-in of the mechanism table), 5691220 (L14-C's neutral-spellings row), 2dd9834 (F2's "The rows above account" paragraph and its "Not yet fully accounted for" bullet) and d3cca9e (the gates paragraph's closing sentence, now naming the seam-row gate beside the review record). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 4cbc6ab6c1d8925d2d9353fc030ab511665252f9) INT26 pick note: the merged pages keep both sides. - ADR-0001 inventory: INT25's K-ULP4 and K-ULP5 rows stay after the journal window row (its V19-FIX wording kept), then this commit's fourteen rows. - ADR `subscriptions` row: this commit's all-or-nothing predicate list, plus the one clause it left out (`declare_security_sites_to_kernel` also refuses a registry whose `sec_id`s are not dense from 0), then H-MEASURE's measured sentence on what each reachable clause changes (c77ee9f9). - ADR close-trade row: this commit's closed A48 question and H-MEASURE's report-only excursion columns (word merge). - ADR rule-2 paragraph: this commit's gate list, then INT25's C-SURFACE-1.0 transport ruling section unchanged. - design RP5: H-MEASURE's measured short-seed sentence stays; this commit's "unmeasured: §3.7 has no row for them" was true only before H-MEASURE's `tests/test_short_seed_report_swap.cpp` (37090e19). - floors: this commit's seam-row self-test adds one row to the merged values (KERNEL 280 -> 281, RELEASE 692 -> 693). check_kernel_seam_rows on the merged tree: 3 seams, 44 written members, 0 without a row. --- CONTRIBUTING.md | 1 + README.md | 1 + docs/adr/0001-kernel-adapter-boundary.md | 100 +++++-- docs/ci.md | 17 ++ docs/design/native-feature-parity.md | 4 +- docs/native-refactor-progress.md | 6 + docs/pages/contributing-llm.md | 7 +- include/pineforge/engine.hpp | 18 +- scripts/check_kernel_seam_rows.py | 332 +++++++++++++++++++++++ scripts/ci_preflight.py | 7 + scripts/ci_verify.py | 7 +- scripts/test_check_kernel_seam_rows.py | 193 +++++++++++++ scripts/test_ci_preflight.py | 3 +- tests/CMakeLists.txt | 4 + 14 files changed, 657 insertions(+), 43 deletions(-) create mode 100644 scripts/check_kernel_seam_rows.py create mode 100644 scripts/test_check_kernel_seam_rows.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 181f024d..6e75e196 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,6 +138,7 @@ What each gate refuses: | `check_native_c_api_surface.py` | a public `NativeStrategyHost` member with no C spelling and no recorded reason | | `check_native_feature_rulings.py` | a `NativeRunSpec` field the adapter does not declare and the ADR does not rule | | `check_kernel_residuals.py` | a TradingView-shaped name reaching the kernel archive or its installed headers without an ADR 0001 row | +| `check_kernel_seam_rows.py` | a kernel `virtual source_*` seam, or a kernel member or row field the source layer writes, that no ADR 0001 row names in its first cell | | `check_native_cpp_versions.py`, `check_aggregate_cpp_versions.py` | an internal C++ epoch moved without its consumers | | `check_adapter_spec_shadowing.py` | the adapter setting a kernel field it is ruled not to set | | `check_twin_parity.py` | a frozen test assertion rewritten instead of a behaviour change being argued | diff --git a/README.md b/README.md index 83e4c412..542b7452 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,7 @@ The gates a pull request passes, one line each: | TradingView parity | `./scripts/check_corpus_parity.sh --subset` | A trade that moved: 54 probes re-run and hashed against `scripts/corpus_parity_baseline.txt`. The full 312-probe sweep (`--subset` dropped) runs nightly. | | CTest row floors | `ci_verify.py release` / `kernel` | A test row that vanished: each profile counts the rows that actually ran against a floor. | | Kernel residuals | `scripts/check_kernel_residuals.py` | A TradingView-shaped name reaching the kernel archive or its installed headers without an ADR 0001 row. | +| Kernel seams | `scripts/check_kernel_seam_rows.py` | A kernel `source_*` seam, or a kernel member or row field the source layer writes, with no ADR 0001 row. | | Feature rulings | `scripts/check_native_feature_rulings.py` | A `NativeRunSpec` field the adapter does not declare and the ADR does not rule. | | C surface | `scripts/check_c_abi_runtime.py`, `scripts/check_native_c_api_surface.py` | A `PF_API` export added without its inventory row; a public host member with no C spelling and no recorded reason. | | Twin parity | `scripts/check_twin_parity.py` | A frozen assertion quietly rewritten instead of a behaviour change being argued. | diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 17808098..fa780bb1 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -380,9 +380,12 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files codegen-emitted (`include/pineforge/pineforge.h:631`), which is why a C host frees its report with `strategy_native_report_free_v1`. - **The examples are a gated target with executed assertions.** Eighteen Pine-free hosts ship - under `examples/native/` — sixteen C++ and two C — each including only - `` (or `native_c_api.h`), linking `PineForge::kernel`, and checking - its own numbers before it prints its summary line. `PINEFORGE_BUILD_EXAMPLES` + under `examples/native/` — sixteen C++ and two C. Each C++ host includes one public kernel + header: ``, or `native_toolkit.hpp` + (`native_bracket_strategy.cpp`) or `native_module.hpp` (`native_market_strategy.cpp`, + `native_selected_strategy.cpp`), both of which include `native_host.hpp`. Each C host includes + ``, which includes `native_c_api.h`. Every one links `PineForge::kernel` + and checks its own numbers before it prints its summary line. `PINEFORGE_BUILD_EXAMPLES` (`CMakeLists.txt:37`, default OFF) builds them (`add_subdirectory` `CMakeLists.txt:332`) and registers each as a CTest row that runs through `examples/native/run_example.cmake`, which fails on a nonzero exit, a signal, a timeout **or** a missing summary line — so a row asserts @@ -501,8 +504,13 @@ Names the vocabulary does not match but the audits named, with their rulings: | `[pineforge] WARNING: …`, `on_margin_call` | `src/session_time.cpp`, `native_c_api.h` | not residue: the project's own name and a broker term. | | `legacy_tolerance`, `NativeLegacyTolerance`, `LegacyTolerant`, `native_legacy_tolerance_enabled` | `NativeRunSpec::legacy_tolerance` and the deprecated spellings beside it (`include/pineforge/native_run_spec.hpp`); read by `src/market_driver.cpp` and folded into the run spec's digest by `src/native_execution_consumer.cpp` | **retained, the member name frozen by the standalone native C++ ABI** (design §2.ii row c; the round-2 and third audits' last "legacy" spelling, R5 lane F6). Lane L12 renamed the policy -- the type is `NativeFeedTolerance`, the predicate `native_feed_tolerance_enabled`, the enumerator `FeedTolerant`, and a C host sets it as `pf_native_run_spec_ext_v1::feed_tolerance` -- but not the member, because `NativeRunSpec` belongs to `native_run_spec_v3` and `scripts/check_native_cpp_versions.py` pins it verbatim: `NativeFeedTolerance legacy_tolerance = NativeFeedTolerance::None;` is a required policy member, and its digest fold `f.u(static_cast(spec.legacy_tolerance));` is pinned text. A rename, or a same-offset alias through an anonymous union (which rewrites that pinned struct text), is therefore a `native_run_spec_v4` decision, not a lane's; the member is renamed `feed_tolerance` there. The word names no TradingView rule: it is a feed-shape policy (structural-only batch bars, non-negative warm-up OHLC) that the Pine adapter and any C host may set, and the three deprecated spellings beside it stay aliases the same checker pins. | -Kernel state the adapter sets — the mechanism rulings (the vocabulary gate cannot -see a mechanism, so these rows are held by reading, not by the checker): +Kernel state the adapter sets — the mechanism rulings. The vocabulary gate cannot see a +mechanism, so a ruling below is held by reading. Since R5 lane H-DOCGATES, +`scripts/check_kernel_seam_rows.py` holds the inventory: it fails when a kernel `virtual source_*` +seam, a `BacktestEngine` member the source layer writes, or a field of the kernel's `Trade` / +`PyramidEntry` rows the source layer writes is not named in the first cell of a table row of +this document. A row-struct field counts only spelled qualified (`Trade::time`), because its bare +name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10): | kernel state | who writes it | ruling | |---|---|---| @@ -517,6 +525,20 @@ see a mechanism, so these rows are held by reading, not by the checker): | what the journal window made state — `RequestDefinition::root`, the order core's chain index (the issued incarnations and each replace successor's root) and `TrailTrack` / `TrailActive::activation_ordinal` (`include/pineforge/native_order.hpp`), and the FX-roll check's two driver-point instants (`NativeExecutionConsumer::driver_marks_`, `src/native_execution_consumer.hpp`) | the core, at the replace that creates a successor and at a trail's arm; the consumer at every walked driver point (synchronous current executions keep their driver log/readback and high water, but do not replace the FX predecessor) | **kernel state, folded into the continuation** (R5 lane V19-B). Rule 2 (a), mechanism: the facts a cohort command, `trail_state()` and the FX-roll check read about requests and points the journal no longer holds, kept as the kernel's own tables for every host. Rule 2 (b), knob: none. They fold where they exist -- a successor's root once at its replace, an arm ordinal where a trail armed, the two instants under a staged curve and a margin model -- which moved the pinned values of runs that replace, arm a trail or roll FX (re-pinned once, each marked); every answer is the one the journal scan gave. Pinned by `tests/test_native_journal_window.cpp` (kernel-only: the chain index against a journal oracle over randomized command streams, a trail's arm ordinal after its event retired) and `tests/test_native_event_retention.cpp` (a trail and a margin receipt read back after the window retired their events). | | the settlement's quantity refusals and their outcome -- `execution::Status::UnrepresentableQuantity` from `next_close_split`, `order_action::plan` and an opening beside a surviving book (`src/engine_execution.cpp`), which `NativeExecutionConsumer::consume_matched_request` answers as `MatchRejectReason::UnrepresentableQuantity` (`include/pineforge/native_order.hpp`; C `PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY`), as it answers the request core's `CoreFailure::NonrepresentableQuantity` from `check_execution` / `prepare_execution` (a fill the request's own units cannot absorb); the quantity grid's own quantities (`CommandContext::units_are_scope_boundary` for a `Reduce` of a FIFO boundary of its scope, measured at submit, what it settles held at its candidate to the grid, a boundary of the scope as it stands or at least its held total, and a `ScopeFraction` whose product is its gross scope's held total, unfloored -- each for a request that settles in one fill); and the opt-in `NativeRunSpec::quantity_tolerance` walk (`tolerant_close_split`) | any host, by the quantities it requests and, for the tolerance, in its spec (C: the tail of `pf_native_run_spec_ext_v1`); the Pine adapter requests its quantities through its own `1e-10` rules, emits no `ScopeFraction` and declares no tolerance (next section) | **a typed refusal of the request, the book's own quantities on the grid, and an opt-in tolerance** (R5 lane K-ULP4, the owner-level ruling of audit X1). Rule 2 (a), mechanism: binary64 bookkeeping is venue-neutral -- a host of any venue meets the same absorbed quantities, gets the same terminal typed outcome for the one request, and keeps its run; the grid admits the quantities a host chooses and the book's own sizes the same way for every venue; the tolerance is data, a spec value with no platform branch. Rule 2 (b), knob: the refusal is none -- it replaces a whole-run `SettlementFailure` (code 6, discriminator 5) no host could want, and a request that settles exactly settles as before, bit for bit; the tolerance is a knob, but not for a decision the run has already made another way: absent, the walk is exact and the spec digest is the one it had before the field (pinned from 91d65ad6), so no existing run faces a hash-visible choice. What moved: runs that stopped on such a request now complete with its refusal (the two point-budget rows of `tests/test_native_resting_acceptance.cpp` and `tests/test_native_resting_replay_contract.cpp` pinned the old run failure and now pin the refusal); on a quantity grid a whole-scope fraction and a boundary `Reduce` that were floored or `OffGrid` now settle -- 48 of AUDIT4's 3,091 previously clean battery scenarios, each one where one of the two rules fired, and no other. Pinned by `tests/test_native_unrepresentable_refusal.cpp` and `tests/test_native_quantity_tolerance.cpp` (kernel-only), `check_unrepresentable_quantity` of `tests/test_native_c_api.c`, and the genuine-* cases of `tests/test_native_partial_close_split.cpp` and `tests/test_native_exact_sum_close.cpp`. | | an OCA-Reduce sibling's deduction binary64 cannot take -- at most half an ulp of what it is taken from, so that fl(units - d) is the units again -- absorbed where the request core deducts a member's fill (`absorbed_deduction`, `pending_sum` and `absorbed_pending_delta` in `src/native_order.cpp`, read by `WorkingRequestCore::prepare_group_effect` / `apply_group_effect`, by `prepare_terms` / `apply_terms` through `effective_host_units`, and by `prepare_owner_applied` / `apply_owner_applied`), and recorded as a `ReservationReducedEvent` with `actual_deduction` 0, a `DeferredGroupAdjustmentEvent` with `deferred_delta` 0 that joins no chain, or a `TermsResolvedEvent` / `QuantityBoundEvent` with `effective_deduction` 0 beside a positive `pending_total` (`include/pineforge/native_order.hpp`; C: `closed_units` 0 on a `PF_NATIVE_EVENT_RESERVATION_REDUCED` or `PF_NATIVE_EVENT_DEFERRED_GROUP` row) | any host, by the OCA-Reduce groups it submits (C: `PF_NATIVE_GROUP_REDUCE`), and the Pine adapter by the `strategy.oca.reduce` groups it maps (`PineExecutionAdapter::group_for`, `src/source/pine_adapter.cpp`) | **absorbed as a no-op** (R5 lane K-ULP5, the owner-level ruling on K-ULP4's finding 2). Rule 2 (a), mechanism: binary64 bookkeeping is venue-neutral -- a deduction too small to move the sibling's remaining units (or its pending total) leaves them where binary64 subtraction puts them, unchanged, for a host of any venue, and the member's fill stands; a pending total below half an ulp of a later fill rounds into it, as the addition does. Rule 2 (b), knob: none -- it replaces a whole-run `SettlementFailure` (code 6, discriminator 7, after the fill was booked) that no host could want, a deduction binary64 can take is taken as before, bit for bit, and only a pending total that overflows binary64 still fails the run with that discriminator. What moved: runs that stopped on such a deduction now complete -- K-ULP4's p5b probe on the C++ and the C host, and the 914 of the 3,000 runs of the seeded OCA-Reduce battery that stopped on f1af50dc -- and the rows that pinned the old contract now pin the absorption (the G5 rows of `tests/test_native_resting_acceptance.cpp` and `tests/test_native_resting_replay_contract.cpp`, `checked_reservation_arithmetic` of `tests/test_native_order_resting_core.cpp`, A-T4d of `tests/test_native_execution_terms.cpp`, `numeric_absorption_and_chain_authentication` of `tests/test_native_order_terms_core.cpp`); the overflow rows still pin the failure. No completed run's deductions move -- each was one binary64 could take -- and `inspect_current_execution`, which threw `std::overflow_error` for a host-sized request whose pending total its units absorb, now answers it. Pinned by `tests/test_native_group_absorption.cpp` (kernel-only) and `check_reservation_absorbed` of `tests/test_native_c_api.c`. | +| the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:1825` and `:1934`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:295`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | +| the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1478`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | +| the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1616`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | +| the auxiliary-feed seams -- `source_aux_security_feed_enabled`, `source_aux_security_input_view` (`include/pineforge/engine.hpp`, behind `PINEFORGE_HAS_AUX_SECURITY_FEED_V1`) -- and `last_error_` | the kernel declares both seams with an inert default (disabled, no bars: `src/engine_consumer.cpp`). It asks them once, in `prepare_native_security_feeds` (`src/engine_aux_security.cpp`), for the auxiliary slice it feeds the `request.security` evaluators. The Pine host overrides both from `strategy_set_aux_security_feed`'s bars (`src/source/pine_aux_security.cpp`). It writes `last_error_` when that setter refuses its input and when its run's preparation throws | **retained with the `auxiliary_feed` adapter-policy ruling above**. The Pine host drives its own auxiliary slice (lane N7's three measurements), and these seams are how that slice reaches the kernel's feed preparation. A bare host declares `NativeRunSpec::auxiliary_feed` and never meets them. `last_error_` is the handle's one error slot, which any layer that refuses a call writes | +| `source_stream_entry_comment` (`include/pineforge/engine.hpp`) | nobody. The kernel's default (`src/engine_consumer.cpp`) and the Pine host's override (`src/source/pine_strategy_host.cpp`) both do nothing, so `stream_observe_entry` keeps the lot's own comment (`src/engine_stream.cpp`) | **dead, kept for the script ABI**. It is a virtual of `BacktestEngine`, which every generated strategy derives from, so deleting it moves every generated script's vtable: an `engine_script_run` epoch. It goes at the next one (`docs/native-refactor-progress.md`, "Scheduled for the next script-ABI epoch") | +| the magnifier report facts -- `bar_magnifier_enabled_`, `diag_magnifier_sub_bars_processed_`, `diag_magnifier_sample_ticks_processed_` (`include/pineforge/engine.hpp`) | the kernel, from the run spec's intrabar path and its driver statistics (`src/native_execution_consumer.cpp`). The Pine host writes them again at every bar callback, from its scheduler's magnifier switch and the context's driver statistics (`on_native_bar_open`, `on_native_bar`) | **retained, report-only**: the report's `bar_magnifier_enabled` and magnifier totals (`src/engine_report.cpp`), outside the hashed region. The kernel publishes them for a bare host itself (the C-surface row "kernel-owned report facts" above), so the Pine host's second write is an adapter-thinness question, not a boundary one | +| `broker_fill_event_seq_` (`include/pineforge/engine.hpp`, in the hashed region) | the Pine host alone, one step per applied broker instruction (`broker_fill_event_seq_` `pine_strategy_host.cpp:595`, in `on_native_applied`); the kernel only resets and folds it | **retained, hashed: the adapter's counter on the kernel's class**. Its placement facts (`signal_close_mc_fill_seq`) compare against it in fill-time gates that cross a bar; the execution authority is the kernel's own ordinal. It is folded into the broker-state hash (`src/engine_state_hash.cpp`), so moving it to the source layer's hash extension moves every run's hash: an epoch decision, not a docs change | +| `broker_state_hashes_` (`include/pineforge/engine.hpp`) | the kernel under `KernelRecorded` (`src/native_execution_consumer.cpp`), and the Pine host at each script point it marks while recording is on (`scheduler_record_broker_hash` `pine_strategy_host.cpp:1961`) | **retained, per driving mode** (design Q4): the report's per-bar hash series, waived as append-only report output that execution never consults | +| the position book's mirror -- `position_qty_`, `position_entry_price_`, `position_entry_count_`, `pyramid_entries_` (`include/pineforge/engine.hpp`, all hashed) | the kernel books and settles them. The Pine host writes them in two places. After every applied event it erases each lot at or below `internal::kQtyEpsilon` and recomputes the position's size, average price and count (`pyramid_entries_` `pine_strategy_host.cpp:600-626`, in `on_native_applied`, restating `ab9714be`'s partial-exit settlement). Before every script body it overwrites `position_entry_count_` with its own entry-slot count (`source_entry_slot_count` `pine_strategy_host.cpp:1886`) | **OPEN, two rulings nobody has made**. The dust sweep erases a lot inside a live book, which the fourth audit found with neither a row nor a tape (its item 32): lane H-THIN's A4-DUST. The count overwrite gives one hashed member two writers with two meanings: the kernel's lots in the current direction, Pine's opened entry slots. It is recorded here first and has no lane yet | +| fields of the kernel's rows the Pine host amends -- `PyramidEntry::entry_bar_index`, `PyramidEntry::time`, `PyramidEntry::max_runup`, `PyramidEntry::max_drawdown`, `Trade::exit_bar_index`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it gives the rows chart-bar indices and dates their fills at the chart bar's open (`on_native_applied`; design rows AG1 and AG2). It samples its lots' excursions itself (`sample_open_trade_extremes` in `src/source/pine_adapter.cpp`, `replace_masked_entry_bar_extremes`, the slippage mask in `on_native_bar`). It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:1277`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | +| the range-end report -- `range_end_trades_`, the extremes `max_equity_`, `min_equity_`, `max_drawdown_`, `max_runup_`, and `stream_warmup_mode_` (`include/pineforge/engine.hpp`) | the Pine host. `scheduler_record_range_end` clears the report rows, has the kernel's producer append them at the curve's last point and re-folds the four extremes from the re-marked curve (`range_end_trades_` `pine_strategy_host.cpp:1714`). `PineScheduler` raises `stream_warmup_mode_` for a stream's warmup, and the host clears it at the first realtime input or print | **adapter-policy, the `report_open_position_at_end` row above** (design RP5 and §3.7 part 2): report shape, not a mark-to-market row, and the kernel's run-end producer is gated out of every Pine run. The extremes are hashed; the report rows are waived as report-only | +| `host_mutation_guard_inert_` (`include/pineforge/engine.hpp`) | the Pine host, once, in its constructor (`host_mutation_guard_inert_ = true` `pine_strategy_host.cpp:168`) | **retained, the host's declared choice**: an in-run call of a frozen C setter is a no-op on a Pine handle, as `ab9714be`'s legacy consumer refused it, and throws on a native host. Outside the hashed region | +| `fold_exit_trail_peak_` (`include/pineforge/engine.hpp`, hashed) | nobody but the per-run reset to NaN (`reset_run_state`, `src/engine_run.cpp`) | **dead, kept by the hash** (design §3.7, "A dead branch found and reported"). Its readers test `isnan` and never take the branch, and the member's own comment says so. The broker-state hash folds it (`src/engine_state_hash.cpp`), so deleting it moves every run's hash and `BacktestEngine`'s layout: an epoch item | +| `NativeHostCache` (`src/native_execution_consumer.hpp`) | the Pine adapter adopts its own subclass per run (`PineRunCache` in `src/source/pine_host_reads.hpp`, through `adopt_host_cache`). The consumer owns it, drops it at every run begin and never reads it | **retained as generic** (R5 lane PERF-P7): lookup state a host derives from its own bookkeeping and cannot keep in its own object, because a generated script's ABI fixes that object's layout. Nothing in it is hashed, recorded or read by the kernel, and the host verifies what it reads back. The header is not installed, so the in-tree source layer is its only user | What N14 moved out of the archive, so the tables above are complete: the `barmerge` merge-flag rule (→ `src/source/pine_security_eval.cpp`), the `PineMatrix` / `PineGenericMatrix` names (→ @@ -621,7 +643,7 @@ Every line number in this section names its symbol on this tree, which | `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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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:2325-2327`) | 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:1583`): 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 runs the predicate leaves out keep every site on the source evaluator: a stream, the chart-timeframe magnifier, an aggregated chart, the KI-55 range-start cut, the historical lookahead projection, an auxiliary feed, a `lookahead_on` or Heikin-Ashi or lower-timeframe site, a forex/cfd intraday daily request (the OTC pins), a registry whose `sec_id`s are not dense from 0, and a pairing the kernel refuses. R5 lane H-MEASURE pins each reachable condition with what it changes (`tests/test_adapter_security_route_conditions.cpp`, a site through the evaluator against the same site through a bare kernel host): on the feeds measured the magnifier and an aggregated chart change no delivered value, while the range-start cut, the projection's incomplete tail, `lookahead_on` and the OTC pins do; the four lower-timeframe flags, the publish gate and the calling-bar latches are subsumed by other clauses, and the empty or undetected timeframe checks are unreachable after begin-time validation | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | +| `subscriptions` | **adapter-hook** | `declare_timeframe_subscriptions` from the begin-time hook (`pine_strategy_host.cpp:1583`): 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:1565`) 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 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 close ending within it of an interior lot boundary is one selected `Flatten` of whole lots (`source_fifo_prefix_openings`, `src/source/pine_adapter.cpp`), 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 | `tests/test_native_quantity_tolerance.cpp` | @@ -732,10 +754,10 @@ The table records, at the tree the doc wave read, what each block described and | `skip_entry_bar_high` / `skip_entry_bar_low` `include/pineforge/engine.hpp:153` | the intrabar-fill excursion mask: the part of a bar traversed before a priced entry filled is not that trade's excursion | the mask is generic — an excursion is measured from the fill — and since R5 lane E6 no host raises the flags: the excursion owner declares where its fill sat and the kernel derives the pair from the bar's path (`declare_opened_lot_entry_bar_mask` `src/engine_path_resolve.cpp:95`); with no declaration the kernel samples the whole bar | already spelled the generic way: `owns_lot_excursions()` + `closed_lot_excursion` hand the whole measurement to a host | `tests/test_l11a_host_excursion.cpp` | | the frozen `pf_pending_order_v1_t` field names | the reflection table of an append-only C ABI POD | already ruled: the residual tables above, held by `scripts/check_kernel_residuals.py` | renaming is an ABI break | `scripts/test_check_kernel_residuals.py` (the gate's own suite, CTest row `test_kernel_residuals`) | | the C ABI's documented vocabulary — `src/c_abi.cpp`, `include/pineforge/pineforge.h`, `src/engine_metrics.cpp`, `src/engine_report.cpp`, `src/engine_trade_accessors.cpp` | the exports are the contract; the comments say what a Pine consumer called the same number | already ruled: the `inputs_` / `get_input_*` / `syminfo_metadata_` row above. No kernel decision reads a Pine name | the export names are frozen | `scripts/check_c_abi_runtime.py`, `tests/test_native_c_api.c` | -| the neutral utility spellings — `map.hpp`, `drawing.hpp`, `series.hpp`, `window_sum.hpp`, `math.hpp`, `matrix.hpp`, `session_time.hpp`, `str_utils.hpp` | neutral public names with exact deprecated `pine_*` aliases for generated code | already ruled: "Neutral spellings" above | dropping the aliases breaks generated code | the ruled-name gate | +| the generated-code runtime's spellings — `session_time.hpp`, `str_utils.hpp`, `math.hpp`, `matrix.hpp`, `generic_matrix.hpp`; `map.hpp`, `color.hpp`, `drawing.hpp`, `log.hpp` | two cases. The first five headers have neutral primaries with exact deprecated `pine_*` forwards or aliases for generated code. The last four keep Pine-named primaries with no neutral alias (`PineMap`, `pine_color`, `pine_drawing_error`, `pine_log_*`) | already ruled, name by name, by the residual table's two rows above: the `pine_time` row (deprecated spellings) and the `PineMap` row (primaries, whose neutral spelling is an open codegen decision, AUDIT3-opus GAP-15) | dropping an alias or renaming a primary is a codegen change | the ruled-name gate | | `native_matching::ladder_trail_stop` `src/native_matching.hpp`, reaching the core as `native_order::ActivationGrid::ladder_tick` | a trailing stop that stands a whole number of price ticks from a running best that is itself a ladder point IS the ladder point that many ticks away, spelled so no binary64 ULP hides it from the side it is reached from; everything off the ladder keeps `best -/+ offset` raw | a stop `ticks` ticks behind a best is a ladder distance at every venue with a tick ladder, and the rule reads the run's own declared `price_tick` — data, never a platform. It is a defect of arithmetic, not a policy: `11.44 - 5 * 0.01` is `11.389999999999998792` against the ladder point `11.390000000000000568`, so a print that IS 11.39 missed a stop the run had put five ticks under 11.44. Both ends are decimal numbers measured on the host's ladder and only the ladder index is exact between them | nothing, because the run had already decided it — it declared the ladder (`NativeRunSpec::price_tick`) and the host spelled the distance on that ladder (`TrailTicks`, or an equal price distance), so a "raw subtraction or ladder point" switch would put a hash-visible choice on the public surface for an answer already given. Nor is this the per-order-kind quantization the `price_grid` row rejects: nothing is rounded ONTO the ladder, and a sub-tick best, a fractional-tick offset or a run with no declared tick keep the raw level bit for bit | `tests/test_native_trail_stop_ladder.cpp` (kernel-only, batch and stream: lane E14's own 11.44 / 5-tick case against a low of exactly 11.39, its buy mirror, and the three off-ladder shapes pinned bit for bit), with the tape row in `tests/test_trail_activation_tick_reach.cpp` (`e14-f-long-shallow-next` 7/7) and the two paths agreeing in `tests/test_adapter_grid_relower.cpp` section 5. Measured by TradingView's tape (R5 lane E14) and by the price-grid trial from the other side (lane R7: one ULP off on ~14% of (best, offset) pairs), closed by lane E16; design `native-feature-parity.md` §3.6.2 | | `native_order::Trail::best_seed` `include/pineforge/native_order.hpp` | where a trail's running best STARTS: absent, the arm's own print; present, a floor on it — the favourable one of the seed and that print | the level is a number the host hands over exactly as it hands over an `arm_price`: the kernel neither derives it nor knows what named it, and a second venue drives it by passing a different number. A trailing stop that rides from its activation rather than from the next print is an ordinary broker shape | nothing, because the old start was not a choice the run had made another way — it was wherever the arm happened to land (the arm threshold on a crossing, the first live print when the request was born already armed), so there is no second spelling to defer to and nothing for a hook to answer; a request field is admissible, opt-in and hashed only when present | `tests/test_native_trail_best_seed.cpp` (kernel-only, batch and stream, the absent-seed controls pinned as same-host properties of the continuation digest rather than as its pre-field values, because that digest folded the host's own zoneinfo root when the lane landed — E23 made it portable across hosts, but it still moves with the installed tzdata release, so the properties stay); the C tail by `check_trail_best_seed_tail` in `tests/test_native_c_api.c`. The need was measured by R5 lanes E5 and E9 — TradingView's `lab tv` tapes book every trail exit at activation ∓ the offset, never at the kernel's own start — and closed by lane E14; design `native-feature-parity.md` §3.6.2 | -| `build_close_trade_with_costs` and `record_close_trade` `src/engine_orders.cpp` | a closing row's percent P&L is its NET P&L (both commissions deducted) over its entry cost at the same account-currency rate; its excursions include the exit fill itself and sit on the net open-profit basis (the entry commission deducted, the favorable magnitude floored at 0); a row whose P&L is exactly zero counts as even | accounting definitions over the row's own booked numbers -- entry cost, commissions, fill price -- so another venue is driven by its own fees and fills; nothing branches on a platform | a gross/net or tolerance switch over report arithmetic the run's fee terms already decide. One question stays open, recorded at the site: the net basis is applied to a host-owned excursion's magnitudes too (RULING A48's hook) | the corpus parity gate (`scripts/check_corpus_parity.sh`: 312 recorded trade lists carrying the percent P&L and both excursion columns, byte-pinned to the baseline; the excursion columns are read against TradingView's exports as report-only deltas, not graded, `scripts/verify_corpus.py`) and `tests/test_max_contracts_held.cpp` (a zero-P&L row counted even); the calibration records are the comments at the site (R5 lane F6 reworded them to the mechanism) | +| `build_close_trade_with_costs` and `record_close_trade` `src/engine_orders.cpp` | a closing row's percent P&L is its NET P&L (both commissions deducted) over its entry cost at the same account-currency rate; the excursions the kernel samples itself include the exit fill and sit on the net open-profit basis (the entry commission deducted, the favorable magnitude floored at 0); a row whose P&L is exactly zero counts as even | accounting definitions over the row's own booked numbers -- entry cost, commissions, fill price -- so another venue is driven by its own fees and fills; nothing branches on a platform | a gross/net or tolerance switch over report arithmetic the run's fee terms already decide. The question this row used to leave open was whether the net basis also applied to a host-owned excursion's magnitudes (RULING A48's hook). R5 lane F3 closed it (`684800bf`): an owner's two magnitudes are recorded as it answered them, and the net basis applies only to the magnitudes the kernel samples | the corpus parity gate (`scripts/check_corpus_parity.sh`: 312 recorded trade lists carrying the percent P&L and both excursion columns, byte-pinned to the baseline; the excursion columns are read against TradingView's exports as report-only deltas, not graded, `scripts/verify_corpus.py`) and `tests/test_max_contracts_held.cpp` (a zero-P&L row counted even); the calibration records are the comments at the site (R5 lane F6 reworded them to the mechanism) | | the indicator library -- `include/pineforge/ta.hpp`, `src/ta_moving_averages.cpp`, `src/ta_oscillators.cpp`, `src/ta_extremes_volume.cpp`, `src/ta_volatility_trend.cpp`, `src/ta_misc.cpp`, `include/pineforge/window_sum.hpp`, `include/pineforge/math.hpp` | each indicator implements its Pine `ta.*` / `math.*` definition -- warm-up and na rules, the window call-site ring, the compensated window sum, tie and epsilon rules -- and its comments record the TradingView tape or export that calibrated each number (the `ta::ATR` and standard-deviation rows above are two of them) | an indicator is a function of its inputs, the same for every venue; generated strategies call exactly these classes, and where two textbook conventions genuinely compete the choice is a per-instance option (`ta::EmaSeeding`), never a branch on the platform | a convention switch on every indicator, for a choice the script already made by calling that Pine function | the kernel-only TA suites `tests/test_ta_*.cpp` and the corpus parity gate | | the formatting library -- `include/pineforge/str_utils.hpp`, `src/str_utils.cpp` (`str_tostring`, `str_format_values`, `str_format`) | the number text of Pine's `str.tostring` / `str.format` built-ins: a number's shortest round-trip decimal spelling, scaled and rounded half-up on its digits, the default, `percent`, `volume` (K / M / B / T) and pattern modes, MessageFormat placeholders and quoting, `NaN` and `Infinity` -- the published rendering TradingView's exported tapes pin (R5 lane C6), ported from the codegen formatter (lane C6b) by R5 lane B-ENGINE; the `mintick` mode, which generated code delegates to it, is kept as it was | a formatting library is a function of its inputs, the same for every venue, as the indicator library is: it spells a number the way the source language's own `str.*` library defines, and no kernel order, fill, settlement, margin or hash code calls it -- of the thirty-five kernel translation units only `src/str_utils.cpp` names these functions | a rendering switch on functions whose definition the calling script chose by calling them | `tests/test_str_number_format.cpp` (lane C6b's 42-case edge battery, every numeric string of lane C6's TradingView exports, byte-identical output on macOS arm64 and Linux x86-64) and `tests/test_str_utils.cpp` | | the language calendar functions -- `include/pineforge/session_time.hpp`, `src/session_time.cpp` (`timeframe_time`, `local_hour` and its siblings, `session_in_market`, `session_trading_day_open_ms`, ...) | the definitions of Pine's `time()` / `time_close()` / bare-time / `session.is*` built-ins for generated code: session and timezone parsing, the invalid- and 24-hour-session spellings, `time("60")` day keying, `session.is*` on daily charts, each comment naming the tape that pinned it | the inputs are data -- a session string, a timezone, a timeframe -- so another venue is driven by its own session template; nothing branches on a platform, and a native host's calendar is the separate generic `native_calendar` | a rules switch on functions whose definition the generated script chose by calling them | `tests/test_session_time.cpp`, `tests/test_pine_time_day_stamp_grid.cpp` and the corpus parity gate | @@ -743,14 +765,22 @@ The table records, at the tree the doc wave read, what each block described and | `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7877-7893` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | | `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7854-7875` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | | `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6605` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | - -The rows above account for every site the two audits named, and none of them is justified by -"because TradingView does it" over a mechanism that is not generic. They do not yet account for -the whole population: the final audit's closure grep found comment lines that justify a live -kernel convention by TradingView with no row here — the excursion basis, the net P&L percent and -`eventrades` notes in `engine_orders.cpp` and `engine.hpp`, the tick- and quantity-grid helpers, -the indicator library's convention notes and `session_time`'s — so this account is incomplete -for them until each gets a row or moves. Two entries were recorded here as open questions rather +| the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:334`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1541`, `:1556`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:1854`). A bare host never calls the builder, so its calendar stays nominal | `tests/test_o_close_pct_day_anchor.cpp` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | +| the bar-time accessors -- `_decompose_bar_time` and `_bar_hour` through `_bar_weekofyear` `include/pineforge/engine.hpp:1066-1093` | the presented bar's stamp broken into calendar fields in UTC, memoized per stamp | a function of the stamp alone, and UTC is the engine's storage timezone | none: they read no spec field and fold into no digest. They are protected `BacktestEngine` members, part of the codegen contract. Their comment claimed that generated code reads them for Pine's bare `hour` / `minute` / `dayofweek`. It does not: the codegen transpiler emits `pine_hour(current_bar_.timestamp, syminfo_.timezone)` and its siblings on the exchange timezone (`pineforge_codegen/codegen/tables.py` at the corpus's pinned commit `f3285d79`), which are the language calendar functions above. No generated strategy in `corpus/` names a `_bar_*` accessor. The comment now says so (R5 lane H-DOCGATES) | `tests/test_chart_timezone.cpp` (`_bar_hour()` is the UTC hour) and `tests/test_intraday_rollover_chart_tz_l4a.cpp` | +| the conventional offset spellings of `timezone_accepted` `include/pineforge/native_calendar.hpp:201` (its comment's "TV sign" line) | `UTC+05:30` and `GMT-4` mean what they say: five and a half hours ahead of UTC, four behind (ISO 8601's sign), where a POSIX `TZ` string of the same shape means the opposite; a POSIX rule string keeps POSIX's own sign | a zone spelling is data, and the sign is the one a venue printing `UTC+8` means; nothing branches on a platform | a sign-convention switch over a spelling whose meaning is fixed | `tests/test_native_calendar.cpp` (`resolve_civil("UTC+05:30", …)` and `resolve_civil("GMT-4", …)` against their fixed instants) | +| the grid and lot helpers -- `tick_grid_price` `include/pineforge/engine.hpp:799`, `price_grid_decimals` `include/pineforge/engine.hpp:833`, `level_on_price_grid` `include/pineforge/engine.hpp:843`, `apply_qty_step` `include/pineforge/engine.hpp:932`, `apply_exit_qty_step` `include/pineforge/engine.hpp:952` (their TradingView calibration comments sit above each) | nearest-tick and price-grid quantization and the lot-increment floors, each calibrated on TradingView tapes | **not a kernel mechanism**: no kernel or source-layer code calls any of them (`tests/test_f6_dead_kernel_members.cpp`, witness 2). The adapter runs its own copies of TradingView's per-order-kind tick rules (ruled native-only in the `price_grid` row above) and sizes its own lots; a native host declares a price grid and a quantity grid in its run spec | nothing to spell. They are rule 5's legacy, kept only because the twin-parity freeze (`scripts/check_twin_parity.py`, base `ab9714be`) holds the checks of the suites that reach them, and they leave with those checks | `tests/test_stop_tick_rounding_l4d.cpp`, `tests/test_level_grid_snap_l4d.cpp`, `tests/test_qty_step_epsilon_floor_l4b.cpp` | + +The rows above account for the whole population. R5 lane H-DOCGATES re-took it on its own tree +with the search this section describes: **369** comment lines in 44 of the 92 files of the +kernel's compile closure. Each line is one of four kinds: a row of this table (the site's own row +or a category row: the indicator, formatting and calendar libraries, the C ABI vocabulary, the +authoritative-feed store), a residual-table ruling, or descriptive. A descriptive line names the +adapter's own copy, contrasts a Pine identifier, points a reader at Pine's vocabulary, or records +history. No line justifies a live kernel convention by TradingView without a row. The fourth +audit's closure grep had found eighteen that did. They are the last four rows: the chart-day +partition, the bar-time accessors, the offset sign and the grid and lot helpers no production code +calls. No gate holds the classification: the vocabulary gate reads names, not comments, so a new +comment is held by review against rule 2. Two entries were recorded here as open questions rather than as violations; the R5 follow-up wave closed both: - **The C delivery words are untyped. CLOSED by R5 lane E7.** @@ -807,7 +837,18 @@ no new kernel policy. ## Boundary rules (for contributors) 1. TradingView/Pine parity for *new* work goes only in `src/source/` / `src/compat/pine/` or in - codegen — never add a new Pine-specific rule to the kernel. + codegen — never add a new Pine-specific rule to the kernel. The kernel archive also carries + the **generated-code language runtime**: the indicator library (`ta.hpp` and the `ta_*.cpp` + units), the formatting library (`str_utils`), the language calendar functions + (`session_time`), `math`, `matrix`, `series`, `window_sum` and the header-only `map`, `color`, + `drawing` and `log`. Its functions implement Pine built-ins by definition, so the rule reads, + for them: a new Pine built-in enters the kernel only through this runtime, and only when it + passes the runtime's **admission test**. The function is a function of its inputs, the same + for every venue. No kernel order, fill, settlement, margin or hash code calls it. Where two + textbook conventions genuinely compete, the choice is a per-instance option, never a branch on + the platform. Its calibration is recorded at the site. Section B's three category rows (the + indicator library, the formatting library, the language calendar functions) apply the test + and state their evidence. 2. The kernel changes only for a *generic* capability with a recorded ruling (e.g. per-lot excursion ownership, `owns_lot_excursions` `native_host.hpp:1011`; the market-if-touched `fill_through` flag, `native_order.hpp:239-242`). **The test is not whether the word @@ -823,12 +864,13 @@ no new kernel policy. A site that passes (a) and (b) belongs in the kernel **with its calibration recorded**: the comment names the pin that fixed the number, and the mechanism gets a row in - "TradingView-calibrated kernel mechanisms" below. A site that passes (a) and fails (b) gets a + "TradingView-calibrated kernel mechanisms" above. A site that passes (a) and fails (b) gets a hook, not a spec field. A site that fails (a) is the adapter's. This is the standard the R5 waves actually applied — it is what ruled the authoritative-feed partition and the `SymInfo::type` vocabulary generic (design §2.ii rows s and t) while the per-order-kind tick - rules and all of `strategy.risk.*` stayed adapter-side — and the section below accounts for - every TradingView-justified kernel site against it, with no site left unruled. + rules and all of `strategy.risk.*` stayed adapter-side — and the section above accounts for + every TradingView-justified kernel site against it, with no site left unruled. The count it + ends with is re-taken by reading; no gate holds it. 3. Every kernel capability added for a bare host is **opt-in** — a new spec field, a new request kind, or a new virtual with an empty default — so the adapter never sets it and adapter runs stay byte-identical by construction (`docs/design/native-feature-parity.md` §3.1). @@ -874,16 +916,18 @@ What "without Pine" still does **not** mean: indicator conventions carry numbers a TradingView tape fixed. Rule 2's amended test is what makes that legitimate, and "TradingView-calibrated kernel mechanisms" above is its account, site by site. -- **Not yet fully accounted for.** Section B rules every calibrated site the audits named, - not every comment of the population that names TradingView; the gap is stated where the - section ends. (The 312 lines of detached comment residue Section A lists are gone: R5 lane E6 - deleted them.) +- **Accounted for by reading, not by a gate.** Section B rules every calibrated site, and the + count it ends with classifies every comment line of the kernel's compile closure that names + TradingView. No gate holds that count: a new comment is held by review. (The 312 lines of + detached comment residue Section A lists are gone: R5 lane E6 deleted them.) The gates run in different places. `check_doc_anchors.py`, `check_doc_lint.py`, -`check_design_inventory.py` and `check_pine_to_native_coverage.py` are binding -`ci_preflight` stages. `check_kernel_residuals.py` runs in the kernel profile and -its CTest self-test; `check_native_feature_rulings.py` runs as CTest rows in the -profiles. The mechanism rulings and rule 2 test remain the documented review record. +`check_design_inventory.py`, `check_pine_to_native_coverage.py`, `check_doc_reverts.py` and +`check_kernel_seam_rows.py` are binding `ci_preflight` stages. `check_kernel_residuals.py` runs in +the kernel profile and its CTest self-test; `check_native_feature_rulings.py` runs as CTest rows in +the profiles. `check_kernel_seam_rows.py` holds the inventory of kernel seams and adapter-written +kernel members, not their rulings: the mechanism rulings and rule 2's test remain the documented +review record. ## C-SURFACE-1.0 transport ruling (rule 2) diff --git a/docs/ci.md b/docs/ci.md index 20874f4f..0219e8c3 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -511,6 +511,23 @@ The kernel-residual gate's ruled counts have floors beside the CTest floors (`ADR_RULED_IDENTIFIERS_MIN`, `ADR_RULED_TEXTS_MIN` in `scripts/ci_verify.py`), so a ruling that leaves ADR-0001 together with its name is an edit of that file too. +`scripts/check_kernel_seam_rows.py` (`kernel-seam-rows`, with +`kernel-seam-rows-tests`) holds the boundary the residual gate cannot name. A +kernel seam the source layer implements, or a kernel member it writes, is the +boundary itself, and its name need not hold a TradingView word. The gate reads +every `virtual source_*` a kernel header declares, and every `BacktestEngine` +member and `Trade` / `PyramidEntry` field that code under `src/source/`, +`src/compat/` and their include trees assigns, increments or mutates. It fails +when ADR-0001 has no table row naming one in its first cell; a row field counts +only spelled qualified (`Trade::exit_time`). It reads the class body itself, +because `scripts/check_broker_state_hash_coverage.py` splits a class at `;` +alone and loses the member declared right after an inline function body. `--list` +prints the inventory with each write site. + +```sh +python3 scripts/check_kernel_seam_rows.py --list +``` + The third source-only guard is `scripts/check_design_inventory.py` (`design-inventory`, with its own `design-inventory-tests` suite). It was fail-closed from the day it landed: it reads `docs/design/native-feature-parity.md` diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index b1f78952..63efbf25 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -163,7 +163,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | | RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9280, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | | RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1358, native_execution_consumer.cpp:10682; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | -| RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:538; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1931 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.8 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | +| RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:538; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1931 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:411 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:405 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:411 is its deprecated spelling, which the generic default forwards to (`tests/test_native_host_hash_extension.cpp`). | K | L2 (the virtual rides the shared bump) | F:I3 O:J1 S:X3 | — | ### 1.9 Other runtime surface @@ -934,7 +934,7 @@ this lane re-pins the affected v19 witnesses once. --- -### 3.8 Audit lane F9 — recording and continuation costs (E24's STOP bucket) +### 3.9 Audit lane F9 — recording and continuation costs (E24's STOP bucket) Per-bar recording (`set_broker_state_hash_recording(true)`, §1.8 RP9) appends one `broker_state_hash()` per script bar, and every row is a whole fold: the diff --git a/docs/native-refactor-progress.md b/docs/native-refactor-progress.md index bf42c117..cda94bac 100644 --- a/docs/native-refactor-progress.md +++ b/docs/native-refactor-progress.md @@ -247,6 +247,12 @@ Mac's 489 and 412 and 1003 of 1249, and no Mac output moved. `PineStrategyHost`'s and so of the generated script's ABI. They go at the next `engine_script_run` epoch, with their two rows in `scripts/broker_state_hash_waivers.txt`, which say so. +- **The dead stream seam.** `BacktestEngine::source_stream_entry_comment` + (`include/pineforge/engine.hpp`) has two implementations, the kernel's + default and the Pine host's override, and both do nothing. It is a virtual + of the class every generated strategy derives from, so deleting it moves the + vtable of every generated script. It goes at the next `engine_script_run` + epoch (ADR-0001, the row "`source_stream_entry_comment`"). ## Publication and acceptance diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 134cda4d..c1de9778 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -55,7 +55,12 @@ finding to report, not a step to take. `nm` over the built `libpineforge_kernel.a`, reads the headers the kernel profile installs and the string literals of the kernel's own translation units, and fails on a TradingView-shaped name with no row in ADR 0001's - residual table. + residual table. `scripts/check_kernel_seam_rows.py` fails on a kernel + `source_*` seam, or a kernel member or row field the source layer writes, + that no ADR 0001 row names in its first cell. The generated-code language + runtime (the indicator, formatting and calendar libraries and the + header-only runtime) implements Pine built-ins by definition; ADR 0001 + rule 1 gives the admission test a new built-in must pass to enter it. 2. **A kernel capability is opt-in.** A new `NativeRunSpec` field, a new request kind, or a new virtual with an empty default — never a changed diff --git a/include/pineforge/engine.hpp b/include/pineforge/engine.hpp index 9b881be0..cf7c013d 100644 --- a/include/pineforge/engine.hpp +++ b/include/pineforge/engine.hpp @@ -1044,13 +1044,13 @@ class BacktestEngine { // --- Time/date extraction from bar timestamp --- // The current bar's time components in UTC, the engine's storage - // timezone. Generated code reads them for Pine's bare ``hour`` / - // ``minute`` / ``dayofweek`` (the variable form), which the language - // defines on the symbol's exchange timezone -- UTC for the crypto symbols - // that are most of the corpus, so the cheap ``gmtime_r`` path is exact - // there. The 1-arg function form ``hour(time)`` is the codegen's own - // (codegen/visit_call.py) and honours ``syminfo_.timezone`` (set via - // ``strategy_set_chart_timezone``). + // timezone, memoized per stamp. Protected, so generated code could call + // them, but the codegen transpiler does not: it spells Pine's bare + // ``hour`` / ``minute`` / ``dayofweek`` ... as pine_hour(current_bar_. + // timestamp, syminfo_.timezone) and its siblings (session_time.hpp), on + // the symbol's exchange timezone, and no generated corpus strategy names + // one of these. Their callers are the tests that pin the UTC reading + // (test_chart_timezone, test_intraday_rollover_chart_tz_l4a). struct BarTime { int year, month, dayofmonth, hour, minute, second, dayofweek, weekofyear; }; @@ -1122,8 +1122,8 @@ class BacktestEngine { std::string input_tf_; std::string script_tf_; // Cached tf_to_seconds(script_tf_). MUST be refreshed immediately after - // every assignment to script_tf_ (both sites live in engine_run.cpp). - // Avoids a string parse per strategy.* call. + // every assignment to script_tf_ (the one writer is the Pine host's + // scheduler_prepare_script_run). Avoids a string parse per read. int script_tf_seconds_ = 0; TimeframeAggregator script_tf_agg_; int64_t prev_bar_timestamp_ = 0; diff --git a/scripts/check_kernel_seam_rows.py b/scripts/check_kernel_seam_rows.py new file mode 100644 index 00000000..0f50ede1 --- /dev/null +++ b/scripts/check_kernel_seam_rows.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +"""Every kernel seam the source layer implements, and every kernel member it +writes, has a row in ADR-0001 or the design record. + +Why this exists +--------------- +The fourth claimed-vs-actual audit (AUDIT4-opus §4, X10) found adapter-only +kernel state and seams with no ruling anywhere: `Trade::exit_from_bracket`, +`broker_fill_event_seq_` (hashed, advanced by the adapter alone), +`last_bar_index_` / `last_bar_time_`, the `source_aux_security_*` seams, +`source_stream_entry_comment` (a dead seam), and the Pine host's publication +of the presented clock. The residual gate cannot see them -- their names hold +no TradingView word -- and the ADR's own tables name only what a lane +remembered to write down. A seam or a member the adapter writes is the +boundary itself; this gate makes each one a row. + +What it reads +------------- +* KERNEL SEAMS: every `virtual ... source_(` declared in a kernel header + -- `include/pineforge/**` and `src/*.hpp` outside the `source/` and + `compat/` trees -- comments stripped. +* ADAPTER-WRITTEN KERNEL MEMBERS: the data members of `BacktestEngine` (the + trailing-underscore fields of its body) and the fields of its row structs + `Trade` and `PyramidEntry`, that source-layer code (`src/source/**`, + `src/compat/**`, `include/pineforge/source/**`, + `include/pineforge/compat/**`, comments stripped) WRITES: an assignment + (`=`, a compound assignment, an indexed assignment), an increment or + decrement, or a mutating container call (`push_back`, `clear`, `erase`, + ...). A read is not a write. A `BacktestEngine` member is written + unqualified from a source class's own code, or through an object whose + nearest declaration makes it a `PineStrategyHost` or a `BacktestEngine` + (`host.bar_index_ = index`). A member a source class declares again under + the same name is that class's own and is skipped. A row-struct field is + written through the kernel's rows only (see `inventory`). The member reader + is this script's own: scripts/check_broker_state_hash_coverage.py splits a + class body at `;` alone, so a member declared right after an inline + function body is invisible to it (`current_bar_`, + `bar_magnifier_enabled_`, `trace_buffer_` on the tree this gate landed on). + +What it requires +---------------- +Each name must appear inside a backticked span in the FIRST CELL of a table +row of docs/adr/0001-kernel-adapter-boundary.md -- the cell that says what the +row rules; the rest of the row states who writes it and its ruling. A mention +in prose, or in another cell (a design row that quotes `engine.current_bar_ = +open_view` is about the kernel's own write, not the adapter's), is not a row. +A row-struct field's name is often a plain word (`time`, `qty`), so it counts +only spelled qualified: `Trade::time`, `PyramidEntry::qty`. + +Exit 0 when every seam and written member has its row, 1 when one has not, +2 when a file cannot be read. `--list` prints the inventory. + +R5 lane H-DOCGATES (AUDIT4-opus X10). +""" +from __future__ import annotations + +import argparse +from dataclasses import dataclass, field +from pathlib import Path +import re +import sys + +import check_broker_state_hash_coverage as coverage + +ROOT = Path(__file__).resolve().parent.parent +ENGINE = "include/pineforge/engine.hpp" +ADR = "docs/adr/0001-kernel-adapter-boundary.md" +ROW_STRUCTS = ("Trade", "PyramidEntry") +SOURCE_TREES = ("src/source", "src/compat", "include/pineforge/source", + "include/pineforge/compat") +CXX = (".cpp", ".hpp", ".h", ".cc") +SEAM = re.compile(r"\bvirtual\b[^;{}()]*?\b(source_\w+)\s*\(") +ASSIGN = r"(?:\[[^\]\n]*\]\s*)?(?:[-+*/%|&^]|<<|>>)?=(?!=)" +MUTATE = (r"(?:\.|->)\s*(?:push_back|emplace_back|emplace|insert|erase|clear|assign|" + r"resize|swap|pop_back|pop_front|push_front|reserve)\s*\(") + + +class Unreadable(Exception): + """A file the gate must read is missing; exit status 2.""" + + +@dataclass +class Inventory: + seams: dict[str, str] = field(default_factory=dict) # name -> declaring header + written: dict[str, list[str]] = field(default_factory=dict) # name -> write sites + + +def read(root: Path, relative: str) -> str: + path = root / relative + if not path.is_file(): + raise Unreadable(f"{relative} is missing") + return path.read_text(encoding="utf-8", errors="replace") + + +def strip_comments(text: str) -> str: + """Comments out and every newline kept, so a write's line is its line.""" + text = coverage.BLOCK_COMMENT.sub(lambda m: " " + "\n" * m.group(0).count("\n"), text) + return coverage.LINE_COMMENT.sub("", text) + + +def declarations(body: str) -> list[str]: + """The top-level declarations of a class body: split at a `;` and at the + `}` that closes an inline function or a nested type.""" + found: list[str] = [] + current: list[str] = [] + depth = 0 + for char in body: + current.append(char) + if char == "{": + depth += 1 + elif char == "}": + depth -= 1 + if depth == 0: + found.append("".join(current)) + current = [] + elif char == ";" and depth == 0: + found.append("".join(current)) + current = [] + return found + + +def data_fields(body: str, *, trailing_underscore: bool) -> set[str]: + result: set[str] = set() + for statement in declarations(body): + value = re.sub(r"\b(?:public|private|protected)\s*:\s*", "", statement).strip() + value = value.rstrip(";").strip() + if not value or value.startswith(("using ", "friend ", "static_assert", "return ", + "template")): + continue + if value.endswith("}") and (re.match(r"(?:enum|struct|class|union)\b", value) + or "(" in value.split("{", 1)[0]): + continue # a nested type or an inline function body + if "=" in value: + value = value.split("=", 1)[0] + value = re.sub(r"\{[^{};]*\}\s*$", "", value).strip() + if "(" in value: + continue # a function declaration + match = re.search(r"([A-Za-z_]\w*)\s*(?:\[[^\]]*\]\s*)*$", value) + if match and (not trailing_underscore or match.group(1).endswith("_")): + result.add(match.group(1)) + return result + + +def class_body(text: str, kind: str, name: str) -> str: + return coverage._named_body(strip_comments(text), kind, name) + + +def source_class_members(root: Path) -> set[str]: + result: set[str] = set() + for relative, classes in coverage.SOURCE_CLASSES.items(): + text = read(root, relative) + for name in classes: + kind = "struct" if name == "PineLanguageState" else "class" + result |= data_fields(class_body(text, kind, name), trailing_underscore=True) + return result + + +def nearest_declaration(text: str, name: str, before: int) -> str | None: + """The line, up to the name, of `name`'s nearest declaration before + offset `before` (with the rest of that line), or None.""" + declared = None + for decl in re.finditer( + r"(?:\b[\w:<>]+\s*[&*]*\s+|\bauto\s*&?\s*|\(\s*(?:const\s+)?auto\s*&\s*)" + + re.escape(name) + r"\s*(?=[=:;{(),])", text[:before]): + declared = decl + if declared is None: + return None + line_start = text.rfind("\n", 0, declared.start()) + 1 + line_end = text.find("\n", declared.end()) + return text[line_start:line_end if line_end >= 0 else len(text)] + + +def kernel_headers(root: Path) -> list[Path]: + found: list[Path] = [] + for path in sorted((root / "include/pineforge").rglob("*")): + if path.suffix in (".h", ".hpp") and not {"source", "compat"} & set( + path.relative_to(root / "include/pineforge").parts[:-1]): + found.append(path) + found += sorted(p for p in (root / "src").glob("*.hpp")) + return found + + +def source_files(root: Path) -> list[Path]: + found: list[Path] = [] + for tree in SOURCE_TREES: + base = root / tree + if base.is_dir(): + found += sorted(p for p in base.rglob("*") if p.suffix in CXX) + return found + + +def inventory(root: Path) -> Inventory: + result = Inventory() + for header in kernel_headers(root): + text = strip_comments(header.read_text(encoding="utf-8", errors="replace")) + for match in SEAM.finditer(text): + result.seams.setdefault(match.group(1), header.relative_to(root).as_posix()) + + engine = read(root, ENGINE) + members = data_fields(class_body(engine, "class", "BacktestEngine"), trailing_underscore=True) + members -= source_class_members(root) + fields: set[str] = set() + for struct in ROW_STRUCTS: + fields |= data_fields(class_body(engine, "struct", struct), trailing_underscore=False) + texts = {p.relative_to(root).as_posix(): strip_comments( + p.read_text(encoding="utf-8", errors="replace")) for p in source_files(root)} + + def record(name: str, relative: str, text: str, offset: int) -> None: + line = text.count("\n", 0, offset) + 1 + result.written.setdefault(name, []).append(f"{relative}:{line}") + + host_types = re.compile(r"\b(?:source::)?(?:PineStrategyHost|BacktestEngine)\b") + for name in sorted(members): + n = re.escape(name) + plain = re.compile( + r"(?])\b" + n + r"\s*" + ASSIGN + + r"|(?:\+\+|--)\s*\b" + n + r"\b|(?])\b" + n + r"\s*(?:\+\+|--)" + + r"|(?])\b" + n + r"\s*" + MUTATE) + qualified = re.compile( + r"(?P\b\w+)\s*(?:\.|->)\s*" + n + r"\s*(?:" + ASSIGN + + r"|\+\+|--|(?=(?:\.|->))" + MUTATE + r")") + for relative, text in texts.items(): + for match in plain.finditer(text): + record(name, relative, text, match.start()) + for match in qualified.finditer(text): + obj = match.group("obj") + declared = None if obj == "this" else nearest_declaration(text, obj, match.start()) + if obj == "this" or (declared is not None and host_types.search(declared)): + record(name, relative, text, match.start()) + + # A row-struct field is written through the kernel's rows only: an element + # of `trades_` / `pyramid_entries_` (indexed or `.back()`), or a name whose + # NEAREST preceding declaration in the file binds it to one -- a `Trade&` / + # `PyramidEntry&` (or a `Trade` / `PyramidEntry` about to be booked), an + # `auto&` taken from those containers, a range-for over them or over a + # `std::vector&` / `std::vector&` parameter. A name + # declared again in between (`sized.time` on a native_order::Sized, a + # pending-order mirror `row`) is not a row. + row_types = "|".join(ROW_STRUCTS) + for relative, text in texts.items(): + containers = {"trades_", "pyramid_entries_"} | set(re.findall( + r"vector\s*<\s*(?:" + row_types + r")\s*>\s*&\s*(\w+)", text)) + box = "(?:this->)?(?:" + "|".join(sorted(map(re.escape, containers))) + ")" + direct = r"\b" + box + r"\s*(?:\[[^\]\n]*\]|\.\s*back\s*\(\s*\))" + binding = re.compile( + r"\b(?:const\s+)?(?:" + row_types + r")\s*&?\s*\w+\s*(?:[=;{(),].*)?$" + r"|\bauto\s*&\s*\w+\s*=\s*" + box + r"\s*(?:\[|\.\s*back\b)" + r"|\bfor\s*\(\s*(?:const\s+)?auto\s*&\s*\w+\s*:\s*" + box + r"\s*\)") + for name in sorted(fields): + n = re.escape(name) + write = re.compile( + r"(?P" + direct + r"|\b\w+\b)\s*(?:\.|->)\s*" + n + r"\s*(?:" + + ASSIGN + r"|\+\+|--|(?=(?:\.|->))" + MUTATE + r")") + for match in write.finditer(text): + obj = match.group("obj") + if not re.match(direct, obj): + declared = nearest_declaration(text, obj, match.start()) + if declared is None or not binding.search(declared): + continue + record(name, relative, text, match.start()) + return result + + +def first_cells(root: Path) -> str: + """The first cell of every table row of the ADR, one per line.""" + cells: list[str] = [] + for line in read(root, ADR).splitlines(): + row = line.strip() + if not row.startswith("|") or re.fullmatch(r"\|[\s|:-]*", row): + continue + # A cell ends at the first `|` outside a backticked span. + cell, ticks = [], False + for char in row[1:]: + if char == "`": + ticks = not ticks + elif char == "|" and not ticks: + break + cell.append(char) + cells.append("".join(cell)) + return "\n".join(cells) + + +def has_row(rows: str, name: str, *, row_field: bool = False) -> bool: + """A backticked span of a row's first cell names it; a row-struct field + only spelled qualified (`Trade::time`, `PyramidEntry::qty`).""" + if row_field: + qualified = "|".join(ROW_STRUCTS) + return re.search(r"`[^`\n]*\b(?:" + qualified + r")::" + re.escape(name) + + r"(?![\w])[^`\n]*`", rows) is not None + return re.search(r"`[^`\n]*(? int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--root", type=Path, default=ROOT) + parser.add_argument("--list", action="store_true", help="print the whole inventory") + args = parser.parse_args(argv) + root = args.root.resolve() + try: + found = inventory(root) + rows = first_cells(root) + except (Unreadable, ValueError) as error: + print(f"check_kernel_seam_rows: {error}", file=sys.stderr) + return 2 + missing: list[str] = [] + for name, header in sorted(found.seams.items()): + ok = has_row(rows, name) + if args.list: + print(f"{'row ' if ok else 'NONE'} seam {name} ({header})") + if not ok: + missing.append(f"kernel seam `{name}` ({header}) has no ADR-0001 row naming it in " + "its first cell: state who implements it and why it is the kernel's") + for name, sites in sorted(found.written.items()): + ok = has_row(rows, name, row_field=not name.endswith("_")) + if args.list: + print(f"{'row ' if ok else 'NONE'} member {name} (written at {sites[0]}" + + (f" and {len(sites) - 1} more" if len(sites) > 1 else "") + ")") + if not ok: + missing.append(f"kernel member `{name}` is written by the source layer " + f"({', '.join(sites[:3])}{', ...' if len(sites) > 3 else ''}) and " + "has no ADR-0001 row naming it in its first cell: state who writes " + "it, whether it is hashed, and its ruling") + for message in missing: + print("check_kernel_seam_rows: " + message, file=sys.stderr) + print(f"kernel seam rows: {len(found.seams)} kernel source_* seams and " + f"{len(found.written)} kernel members the source layer writes, " + f"{len(missing)} without a row ... {'FAIL' if missing else 'OK'}") + return 1 if missing else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index 7bd28c6c..26748494 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -237,6 +237,13 @@ def check_commands(source: Path) -> list[tuple]: [sys.executable, str(source / 'scripts/test_check_doc_reverts.py')]), ('doc-reverts', [sys.executable, str(source / 'scripts/check_doc_reverts.py')]), + # R5 lane H-DOCGATES (AUDIT4-opus X10): every kernel `source_*` seam and + # every kernel member or row field the source layer writes is named by + # an ADR-0001 row, which states who writes it and its ruling. + ('kernel-seam-rows-tests', + [sys.executable, str(source / 'scripts/test_check_kernel_seam_rows.py')]), + ('kernel-seam-rows', + [sys.executable, str(source / 'scripts/check_kernel_seam_rows.py')]), # NOT advisory: the migration page's coverage claim is L14-B's own, and # it is true on this tree. A `strategy.*` name added to the Pine v6 # inventory without a row on that page fails here immediately. diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index c2d38a7d..274c39d7 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -246,7 +246,7 @@ # registered, 278 run: the WebSocket row still skips on a system libcurl. # 279 = those 278 plus R5 lane H-DOCGATES' test_doc_reverts (the doc-revert # gate's self-test, a Python row every profile registers; INT26 pick, summed). -KERNEL_MIN_TESTS = 280 +KERNEL_MIN_TESTS = 281 # Release-row floor, the same gate for the default profile. Before lane P7 # only the kernel profile had one, so a row that left release alone (a # source-bound TU dropped from TEST_SOURCES, a deleted twin or ABI row) left a @@ -372,13 +372,16 @@ # No release row skips, so 690 registered is 690 run. # 691 = those 690 plus R5 lane H-DOCGATES' test_doc_reverts (see # KERNEL_MIN_TESTS; INT26 pick, summed). -RELEASE_MIN_TESTS = 692 +RELEASE_MIN_TESTS = 693 # R5 lane H-DOCGATES (AUDIT4-opus perf N-7) raises both floors by one more: # test_native_runtime_ambient_lifo, the runtime blocks' LIFO death row, is # source-free, so it registers and runs in the kernel profile (279 -> 280) and # in release (691 -> 692; INT26 pick, summed). Recorded below both floors so # neither floor's line moves: CONTRIBUTING.md and contributing-llm.md cite # them by line. +# R5 lane H-DOCGATES (AUDIT4-opus X10) raises both by one more: +# test_kernel_seam_rows, the kernel seam-row gate's self-test, is a Python row +# every profile registers (280 -> 281, 692 -> 693; INT26 pick, summed). # ADR-0001 ruled-count floors, beside the ctest floors (R5 lane H-DOCGATES, # AUDIT4-opus X13 / docs-a N7). check_kernel_residuals.py counts the rulings # its vocabulary reads -- 174 identifiers and 45 texts on the lane's tree: the diff --git a/scripts/test_check_kernel_seam_rows.py b/scripts/test_check_kernel_seam_rows.py new file mode 100644 index 00000000..cc80d710 --- /dev/null +++ b/scripts/test_check_kernel_seam_rows.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +"""Self-tests for the kernel seam-row gate: it must be able to FAIL. + +Each case writes a throwaway tree -- a kernel `engine.hpp`, the four source +headers the member reader skips, one source-layer file that writes kernel +state, and an ADR -- and drives the real ``check_kernel_seam_rows.main`` over +it. A kernel `source_*` seam, a member the source layer writes (unqualified, +through a host reference, or declared right after an inline function body) +and a row field written through the kernel's rows each fail without a row +naming them in its first cell, and pass with one. A read is not a write, a +member a source class declares again is that class's own, a row field spelled +bare or a name quoted in a later cell is not a row, and a field of an object +that is not one of the kernel's rows is not a row write. + +R5 lane H-DOCGATES (AUDIT4-opus X10). +""" +from __future__ import annotations + +import contextlib +import io +from pathlib import Path +import tempfile +import unittest + +import check_kernel_seam_rows as gate + +ENGINE = """ +namespace pineforge { +struct Trade { + long time = 0; + bool exit_from_bracket = false; +}; +struct PyramidEntry { + double qty = 0.0; +}; +class BacktestEngine { +public: + virtual bool source_feed_enabled() const; + int shown() const { return 1; } +protected: + Bar current_bar_; + int bar_index_ = 0; + int kernel_only_ = 0; + int shadowed_ = 0; + std::vector trades_; +}; +} +""" + +SOURCE_HEADERS = { + "include/pineforge/source/pine_adapter.hpp": "class PineExecutionAdapter {\n int a_ = 0;\n};\n", + "include/pineforge/source/pine_scheduler.hpp": "class PineScheduler {\n int b_ = 0;\n};\n", + "include/pineforge/source/pine_strategy_host.hpp": + "class PineStrategyHost : public BacktestEngine {\n int shadowed_ = 0;\n};\n", + "include/pineforge/source/pine_language_state.hpp": "struct PineLanguageState {\n int c_ = 0;\n};\n", +} + +SOURCE = """ +void PineStrategyHost::publish(const Bar& bar) { + current_bar_ = bar; // declared right after an inline function body + const int seen = kernel_only_; // a read, not a write + shadowed_ = seen; // the source class's own member +} +void scope(PineStrategyHost& host, int index) { + host.bar_index_ = index; // through a host reference +} +void label(std::vector& rows) { + for (auto& trade : rows) trade.exit_from_bracket = true; + Sized sized; + sized.time = 3; // not one of the kernel's rows +} +""" + +ADR_HEAD = """# ADR + +| kernel state | who writes it | ruling | +|---|---|---| +""" + +ALL_ROWS = ( + "| `source_feed_enabled` | the Pine host | retained |\n" + "| the presented bar -- `current_bar_`, `bar_index_` | the Pine host | retained |\n" + "| `Trade::exit_from_bracket` | the Pine host | retained |\n" +) + + +class Tree: + def __init__(self, adr_rows: str, source: str = SOURCE) -> None: + self.dir = tempfile.TemporaryDirectory(prefix="pineforge-seam-rows-") + self.root = Path(self.dir.name) + self.write("include/pineforge/engine.hpp", ENGINE) + for relative, text in SOURCE_HEADERS.items(): + self.write(relative, text) + self.write("src/source/pine_strategy_host.cpp", source) + self.write(gate.ADR, ADR_HEAD + adr_rows) + + def write(self, relative: str, text: str) -> None: + path = self.root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text) + + def run(self, *args: str) -> tuple[int, str]: + out, err = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): + code = gate.main(["--root", str(self.root), *args]) + return code, out.getvalue() + err.getvalue() + + def close(self) -> None: + self.dir.cleanup() + + +@contextlib.contextmanager +def tree(adr_rows: str, source: str = SOURCE): + made = Tree(adr_rows, source) + try: + yield made + finally: + made.close() + + +class Inventory(unittest.TestCase): + def test_what_the_gate_reads(self) -> None: + with tree(ALL_ROWS) as t: + found = gate.inventory(t.root) + self.assertEqual(set(found.seams), {"source_feed_enabled"}) + self.assertEqual(set(found.written), {"current_bar_", "bar_index_", "exit_from_bracket"}) + self.assertEqual(found.written["current_bar_"], ["src/source/pine_strategy_host.cpp:3"]) + self.assertEqual(found.written["bar_index_"], ["src/source/pine_strategy_host.cpp:8"]) + + def test_a_block_comment_keeps_the_line_numbers(self) -> None: + source = "/* two\n lines */\n" + SOURCE + with tree(ALL_ROWS, source) as t: + found = gate.inventory(t.root) + self.assertEqual(found.written["current_bar_"], ["src/source/pine_strategy_host.cpp:5"]) + + +class MustFail(unittest.TestCase): + def assert_fails_naming(self, rows: str, name: str) -> None: + with tree(rows) as t: + code, out = t.run() + self.assertEqual(code, 1, out) + self.assertIn(f"`{name}`", out) + self.assertIn("FAIL", out) + + def test_a_seam_without_a_row(self) -> None: + self.assert_fails_naming(ALL_ROWS.replace("| `source_feed_enabled` | the Pine host | retained |\n", ""), + "source_feed_enabled") + + def test_a_member_declared_after_an_inline_function(self) -> None: + self.assert_fails_naming(ALL_ROWS.replace("`current_bar_`, ", ""), "current_bar_") + + def test_a_member_written_through_a_host_reference(self) -> None: + self.assert_fails_naming(ALL_ROWS.replace(", `bar_index_`", ""), "bar_index_") + + def test_a_row_field_written_through_the_kernels_rows(self) -> None: + self.assert_fails_naming(ALL_ROWS.replace("| `Trade::exit_from_bracket` | the Pine host | retained |\n", ""), + "exit_from_bracket") + + def test_a_row_field_spelled_bare_is_not_its_row(self) -> None: + self.assert_fails_naming(ALL_ROWS.replace("`Trade::exit_from_bracket`", "`exit_from_bracket`"), + "exit_from_bracket") + + def test_a_name_quoted_in_a_later_cell_is_not_its_row(self) -> None: + rows = ALL_ROWS.replace("| the presented bar -- `current_bar_`, `bar_index_` | the Pine host | retained |\n", + "| the presented bar -- `bar_index_` | the kernel writes `current_bar_` too | retained |\n") + self.assert_fails_naming(rows, "current_bar_") + + def test_prose_is_not_a_row(self) -> None: + rows = ALL_ROWS.replace("| `source_feed_enabled` | the Pine host | retained |\n", "") + rows += "\nThe seam `source_feed_enabled` is the Pine host's.\n" + self.assert_fails_naming(rows, "source_feed_enabled") + + def test_a_missing_file_is_exit_2(self) -> None: + with tree(ALL_ROWS) as t: + (t.root / gate.ADR).unlink() + code, out = t.run() + self.assertEqual(code, 2, out) + + +class MustPass(unittest.TestCase): + def test_every_entry_has_its_row(self) -> None: + with tree(ALL_ROWS) as t: + code, out = t.run("--list") + self.assertEqual(code, 0, out) + self.assertIn("1 kernel source_* seams and 3 kernel members the source layer writes, " + "0 without a row ... OK", out) + self.assertNotIn("kernel_only_", out) # read, never written + self.assertNotIn("shadowed_", out) # the source class's own member + self.assertNotIn(" time ", out) # `sized.time` is not a kernel row + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_ci_preflight.py b/scripts/test_ci_preflight.py index 0b1c1b1c..d5e1ddb8 100644 --- a/scripts/test_ci_preflight.py +++ b/scripts/test_ci_preflight.py @@ -116,7 +116,8 @@ def test_every_documentation_stage_is_binding(self): plan = {entry[0]: entry for entry in check_commands(Path('/src'))} for name in ('doc-anchors', 'doc-lint', 'doc-anchors-tests', 'doc-lint-tests', 'doc-pine-coverage', 'design-inventory', 'design-inventory-tests', - 'doc-reverts', 'doc-reverts-tests'): + 'doc-reverts', 'doc-reverts-tests', + 'kernel-seam-rows', 'kernel-seam-rows-tests'): self.assertIn(name, plan) self.assertEqual(len(plan[name]), 2, name) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e4d79113..e13cc48f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2326,6 +2326,10 @@ add_test(NAME test_doc_lint COMMAND ${Python3_EXECUTABLE} -B # over throwaway git repositories; the gate itself is a ci_preflight stage. add_test(NAME test_doc_reverts COMMAND ${Python3_EXECUTABLE} -B ${PROJECT_SOURCE_DIR}/scripts/test_check_doc_reverts.py) +# R5 lane H-DOCGATES (AUDIT4-opus X10): the kernel seam-row gate's must-fail +# suite, over throwaway trees; the gate itself is a ci_preflight stage. +add_test(NAME test_kernel_seam_rows COMMAND ${Python3_EXECUTABLE} -B + ${PROJECT_SOURCE_DIR}/scripts/test_check_kernel_seam_rows.py) # R5 F2: the worked migration of docs/pages/pine-to-native.md, executed. The # extractor copies the page's six C++ blocks out verbatim at build time and From a8e6184946d9c0fe00f13ef05d7fbcc1da3e4c26 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 05:00:09 +0800 Subject: [PATCH 17/83] Docs+Gates: the 1.0 C boundary -- every C++ capability the C surface does not expose, its reason and the checker row that fails when the gap closes; no page claims C/C++ parity beyond it (R5 lane H-DOCGATES, C-surface 1.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supervisor ruling 2026-09-26: C-SURFACE-2 moves to 1.1.0, so the 1.0 docs list every C++ capability the C surface does not expose, each with its reason and checker row, and nothing claims C/C++ parity beyond that list. docs/pages/native-engine.md "The 1.0 C boundary" replaces "What is not exposed, and why", which named the five excluded host members and said of the preview that "`strategy_native_execute_current_v1` answers the same verdicts" -- it applies the command, so it is no preview. The table has 27 rows: the five COVERAGE `[--]` members, the deprecated hash_source_extension seam, the KernelRecordedAtHostMarks exclusion (reworded: the kernel's mark is reached only by the Pine source host; a C++ host records its own points), and 21 C_V1_EXCLUSIONS rows -- the replace options, the applied event's tail, a closed row's entry comment, closed rows in place, the working request's whole definition, the open-ended session-day fact and the rest of the callback contexts, host-sized openings, the terms hook's units answer, the excursion facts' rate, the hash sink, the entry-bar mask outside on_applied, timeframe_undetected, the inherited statistics, the hook views, the command events' payloads, the command results, native_state()'s spec and failure context, trace rows and hook re-declaration, the C++ library around the host, and native_toolkit.hpp. The four C-SURFACE-2 items say 1.1.0; the rest say they have no lane. scripts/check_native_c_api_surface.py: each C_V1_EXCLUSIONS row pins its C++ declaration (a whole-word match) and one or more (C record, pattern) pairs whose pattern must stay absent; every pattern tolerates a prefix and a suffix (`\w*`), because the old draft's pins caught only the plain spelling and let strategy_closed_trade_entry_comment_v1, a keep_handle-carrying replace call, strategy_native_submit_bracket_v1 and a request_origin / request_label applied tail through. The page must cite every row exactly once and no row the checker lacks. Ten new self-tests: those four spellings, a pf_native_applied_ext_v1 beside v1, an input interval on the decision, a C++ declaration that goes, and a row missing, doubled or made up on the page. Against this commit's parent checker all ten fail (7 failures, 3 errors); here 29/29 pass. Parity claims narrowed where they went past the boundary: native-engine "`` is the C spelling of all of it", the reading table's state / working / events / current-point rows (partial), native_sized_units's C cell (it has one), the preview's C cell, the report mark "the host calls the kernel's report mark", the spec extension's "The one field of `NativeRunSpec` it deliberately does not carry is `identity`" (also timeframe_undetected), on_close_units "the only half this header exposes"; pine-to-native "needs no C++", the host-sized answer "A C host answers the same question through `on_close_units`", "Three rules make the C door behave like the C++ one" and the "Five members have no direct C spelling" paragraph; README's three front doors ("so they match trigger, price fills, book lots and settle identically") and "to its C++ **and** C spelling" (also index.md and contributing-llm); the ADR's C API bullet, its route-or-ruling sentence and the KernelRecordedAtHostMarks row; design §2.iii marked as the L13 plan. Header comments (comment-only, line-count preserving; all 86 library objects byte-identical with and without them): native_c_api.h's opening ("It is the C spelling of ... NativeStrategyHost"), the inspect_current_execution COVERAGE row, the spec extension's omission, on_close_units; native_host.hpp's C spellings of native_state, events, current_execution_point, resolve_execution_terms, inspect_current_execution and the two configure calls; native_order.hpp's HostSized note ("the C surface refuses HostSized under any owner but BindCohort" -- a Book-scoped WaitForApplied close is admitted too). Sentences deleted or rewritten, by the commit that wrote them: f22a34e (N8's "What is not exposed, and why" lead), d3cca9e (its five-member list and the market-only note), 69228e6 (L14-C's native_sized_units and preview C cells), 074f648 (F4's KernelRecordedAtHostMarks row reason), 2fcbb52 (F2's "Not every member of the C++ host has a C spelling"), ca0aac5 (INT16's five-member sentence on the migration page) and 4fb9db3 (L14-B's CONTRIBUTING row for a C kernel-driving export). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 77a3eacb6e24fec8d98c1d074bfaf8943cdfdb9a) INT26 pick note: this commit was written before INT25's KERNEL-EDGE (409ebad1), which made the kernel's report mark installed C++ API (`NativeStrategyHost::mark_native_report_point`, a COVERAGE `[--]` row of native_c_api.h). Its sentences "The kernel's mark is reached only by the Pine source host (it is not installed API); a C++ host records its own points instead" are false on the merged tree, so: - the ADR's KernelRecordedAtHostMarks row keeps INT25's text (a C++ host reaches the kernel producer through mark_native_report_point); - native-engine.md's report-mark paragraph keeps INT25's text and adds this commit's C half: "A C host has no call that marks one (the 1.0 C boundary table)."; - the boundary table's KernelRecordedAtHostMarks row states the C++ mark route and cites COVERAGE `[--] mark_native_report_point` beside its ENUM_TWINS exclusion (the sixth excluded NativeStrategyHost member). The C configure paragraph keeps INT25's "a second extended configure of a Ready handle is refused without changing it" and takes this commit's two omitted fields (`identity`, `timeframe_undetected`). The ADR paragraph on report ownership keeps INT25's digits and takes this commit's boundary pointer. check_native_c_api_surface: 64 members, 58 with a C spelling, 6 excluded; 21 C++ capabilities outside the 1.0 C surface, each pinned and in the boundary table. --- CONTRIBUTING.md | 4 +- README.md | 18 ++- docs/adr/0001-kernel-adapter-boundary.md | 12 +- docs/design/native-feature-parity.md | 11 +- docs/pages/contributing-llm.md | 8 +- docs/pages/index.md | 5 +- docs/pages/native-engine.md | 90 ++++++++++---- docs/pages/pine-to-native.md | 32 +++-- include/pineforge/native_c_api.h | 20 ++-- include/pineforge/native_host.hpp | 22 ++-- include/pineforge/native_order.hpp | 4 +- scripts/check_native_c_api_surface.py | 133 ++++++++++++++++++++- scripts/test_check_native_c_api_surface.py | 94 +++++++++++++++ 13 files changed, 375 insertions(+), 78 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e75e196..8e208bf3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,7 +66,7 @@ Two consequences that are easy to miss: | Add or change a TA class | the right `ta_*.cpp` partition + its declaration in `` | add a unit test against a hand-computed series | | Change what codegen may emit | the contract, not this repo's runtime | say so in the PR; the transpiler lives in `pineforge-codegen-oss` | | Add a runtime `PF_API` export | `src/c_abi.cpp` + `include/pineforge/pineforge.h` | update `EXPECTED_RUNTIME` check_c_abi_runtime.py:28, the ctypes harnesses, and the README symbol table — all in the same commit | -| Add a C kernel-driving export | `src/native_c_host.cpp` + `include/pineforge/native_c_api.h` | update that header's COVERAGE block; `scripts/check_native_c_api_surface.py` proves it is exactly the host's public surface | +| Add a C kernel-driving export | `src/native_c_host.cpp` + `include/pineforge/native_c_api.h` | update that header's COVERAGE block, and retire the 1.0 C boundary row the export closes; `scripts/check_native_c_api_surface.py` proves the block is exactly the host's public surface and fails on a closed gap | | Document something | `docs/pages/`, `README.md`, this file | cite the tree by `file:line`; the anchor guard checks that the line still holds the symbol | ## Development setup @@ -135,7 +135,7 @@ What each gate refuses: | Gate | Refuses | |---|---| | `check_c_abi_runtime.py` | a `PF_API` runtime export added or removed without its inventory row | -| `check_native_c_api_surface.py` | a public `NativeStrategyHost` member with no C spelling and no recorded reason | +| `check_native_c_api_surface.py` | a public `NativeStrategyHost` member with no C spelling and no recorded reason; a 1.0 C boundary row whose gap closed, or that `docs/pages/native-engine.md` stopped listing | | `check_native_feature_rulings.py` | a `NativeRunSpec` field the adapter does not declare and the ADR does not rule | | `check_kernel_residuals.py` | a TradingView-shaped name reaching the kernel archive or its installed headers without an ADR 0001 row | | `check_kernel_seam_rows.py` | a kernel `virtual source_*` seam, or a kernel member or row field the source layer writes, that no ADR 0001 row names in its first cell | diff --git a/README.md b/README.md index 542b7452..b34644d4 100644 --- a/README.md +++ b/README.md @@ -115,9 +115,11 @@ Lifecycle-aware compiled modules reset Pine variables, indicator/history buffers ## Three front doors -The engine can be driven three ways. All three run the same kernel, so they -match trigger, price fills, book lots and settle identically; what differs is -who writes the strategy and who owns TradingView's quirks. +The engine can be driven three ways. All three run the same kernel, so for the +same requests they match trigger, price fills, book lots and settle +identically; what differs is who writes the strategy, who owns TradingView's +quirks and, for C, the C++ capabilities the 1.0 C surface does not spell (the +1.0 C boundary table in the [native engine guide](docs/pages/native-engine.md)). ### 1. PineScript, through codegen @@ -192,7 +194,8 @@ strategy_native_run_v1(s, bars, n, &report); **Coming from PineScript?** [PineScript to native C++](docs/pages/pine-to-native.md) maps every `strategy.*` builtin, the 19 covered `strategy()` declaration -parameters and every `request.*` form to its C++ **and** C spelling, names the example that +parameters and every `request.*` form to its C++ spelling and, where the 1.0 C surface has one, +its C spelling, names the example that exercises each, and walks one six-feature strategy from Pine to a native host end to end. The [native engine guide](docs/pages/native-engine.md) is the reference underneath it. @@ -395,7 +398,7 @@ The gates a pull request passes, one line each: | Kernel residuals | `scripts/check_kernel_residuals.py` | A TradingView-shaped name reaching the kernel archive or its installed headers without an ADR 0001 row. | | Kernel seams | `scripts/check_kernel_seam_rows.py` | A kernel `source_*` seam, or a kernel member or row field the source layer writes, with no ADR 0001 row. | | Feature rulings | `scripts/check_native_feature_rulings.py` | A `NativeRunSpec` field the adapter does not declare and the ADR does not rule. | -| C surface | `scripts/check_c_abi_runtime.py`, `scripts/check_native_c_api_surface.py` | A `PF_API` export added without its inventory row; a public host member with no C spelling and no recorded reason. | +| C surface | `scripts/check_c_abi_runtime.py`, `scripts/check_native_c_api_surface.py` | A `PF_API` export added without its inventory row; a public host member with no C spelling and no recorded reason; a 1.0 C boundary row whose gap has closed or that the native engine guide no longer lists. | | Twin parity | `scripts/check_twin_parity.py` | A frozen assertion quietly rewritten instead of a behaviour change being argued. | | Documentation | `scripts/check_doc_anchors.py`, `scripts/check_doc_lint.py`, `scripts/check_pine_to_native_coverage.py` | A `file:line` citation that no longer points at its symbol; a stale epoch, roadmap label or negative claim; a Pine builtin with no row on the migration page. | | Doc reverts | `scripts/check_doc_reverts.py` | A published sentence deleted, or older wording restored over newer, by a commit whose message does not name it. | @@ -501,7 +504,10 @@ The header's **COVERAGE** block lists every public member of `scripts/check_native_c_api_surface.py` proves that list is exactly that class's public surface — a member added without a row, a row naming a member that no longer exists, or a spelling naming a symbol the C headers do not -declare all fail CI. +declare all fail CI. Every other C++ capability the 1.0 C surface lacks is a +row of the 1.0 C boundary table in the native engine guide, and a +`C_V1_EXCLUSIONS` row of the same checker fails when that gap closes or its C++ +declaration goes. Every struct is tagged and size-prefixed (`struct_size`, `version`); an unknown size, version or enumerator is refused with a documented negative status and diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index fa780bb1..9adc3ba6 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -378,7 +378,11 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files spelling or the reason it has none, and `scripts/check_native_c_api_surface.py` proves the block is exactly that class's public surface. `strategy_create` / `run_backtest` stay codegen-emitted (`include/pineforge/pineforge.h:631`), which is why a C host frees its report - with `strategy_native_report_free_v1`. + with `strategy_native_report_free_v1`. The 1.0 boundary is the table "The 1.0 C boundary" in + `docs/pages/native-engine.md`: every C++ capability the C surface does not expose, with its + reason and the checker row that pins it (a COVERAGE `[--]` row, an `ENUM_TWINS` exclusion or a + `C_V1_EXCLUSIONS` row of `scripts/check_native_c_api_surface.py`). 1.0 claims no C/C++ parity + beyond the declared fields and calls. - **The examples are a gated target with executed assertions.** Eighteen Pine-free hosts ship under `examples/native/` — sixteen C++ and two C. Each C++ host includes one public kernel header: ``, or `native_toolkit.hpp` @@ -657,7 +661,9 @@ this section to what a run *declares*. A C host drives the same kernel a C++ host does, so every generic capability a C++ host reaches owes a C host either a route or a ruling that says why not — and "no size-prefixed POD yet" is a to-do, not a -ruling. The census of `NativeStrategyHost` is the COVERAGE block of `include/pineforge/native_c_api.h`, +ruling. In 1.0 each such to-do is a row of the 1.0 C boundary table (`docs/pages/native-engine.md`) +that names its scheduled lane, or says it has none: C-SURFACE-2 (1.1.0) takes the execution preview, +the applied event's origin and label, a closed row's entry comment and the replace options. The census of `NativeStrategyHost` is the COVERAGE block of `include/pineforge/native_c_api.h`, held by `scripts/check_native_c_api_surface.py`, which since this lane also classifies every C enumeration against its kernel twin value by value. This section records the lane's decisions on the surfaces the third audit named (AUDIT3 §3.1 "What breaks G1" 4, findings E12 f7 and E13 f4). Every line @@ -894,7 +900,7 @@ no new kernel policy. ## Consequences "Use the engine without Pine" means, on this tree: program against `NativeStrategyHost` in C++ -**or** against `` in C; link `PineForge::kernel`, which is a separately +**or** against `` in C, less the capabilities its 1.0 boundary table lists; link `PineForge::kernel`, which is a separately buildable archive with no source-layer object in it; size with `Sized` or with your own terms; let the kernel record the equity curve, the per-bar broker hashes and the open-position row, or record them yourself; declare higher-timeframe series and an auxiliary finer feed in the run diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 63efbf25..c83393bf 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -59,7 +59,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:13004; lowering `submit_one` pine_adapter.cpp:9040-9052, `BindCohort` pine_adapter.cpp:9541-9547, `submit_leg` pine_adapter.cpp:9565-9589; 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:1884); 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:8645-8648; `materialize_relative_exits` pine_adapter.cpp:17322-17363, pine_adapter.hpp:1820-1832) | **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:5611-5645. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19083-19112) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10880-10904), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15631, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15865) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:662-664 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10880-10904), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15631, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15865) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:651-653 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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 @@ -225,6 +225,11 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa ### 2.iii C-level order submission and callbacks (L13, post-v1 — R5-7) +*History: the plan L13 worked from. The surface shipped as +``, R5 lane F4 routed every policy hook, and what the +1.0 C surface still does not expose is the 1.0 C boundary table of +`docs/pages/native-engine.md`. "Today" below is the tree L13 read.* + Today a C host can configure (`strategy_configure_native_v1` c_abi.cpp:861-926), stream (`strategy_stream_*` c_abi.cpp:589-698) and read results, but strategy logic is C++-only and the batch entry points (`strategy_create`, `run_backtest*`) are per-strategy symbols a native author hand-writes. Design retained from F (most complete), with O's event polling and S's hardening rules: ```c @@ -266,7 +271,7 @@ PF_API int pf_native_events_v1(pf_strategy_t, uint64_t after_ordinal, pf_native_ | Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1398-1405): inside a callback, or between realtime inputs | F | | Hardening | Tagged, size-prefixed PODs; `struct_size` + version; unknown-tag refusal; never cast the C request to a C++ variant; a C fixture compiles against the frozen previous header. S puts the API in a separate header `include/pineforge/native_c_api.h` | S | | SOP | Every symbol goes into `src/c_abi.cpp`, `pineforge.h`, `EXPECTED_RUNTIME` + both counts (check_c_abi_runtime.py:19-79), the README table and the Python harnesses, or all CI matrix jobs fail at the "C ABI runtime source check" step (`CLAUDE.md`) | F O S | -| Not in v1 of the C API | `resolve_execution_terms` / `validate_execution_precommit` are the adapter's seam; a C host that needs sizing uses `Sized` (F). O offers an optional `resolve_terms` callback: deferred | F O | +| Not in v1 of the C API | `resolve_execution_terms` / `validate_execution_precommit` are the adapter's seam; a C host that needs sizing uses `Sized` (F). O offers an optional `resolve_terms` callback: deferred. *History: R5 lane F4 routed both (`on_execution_terms`, `on_precommit`); the terms hook's units answer outside a host-sized close is still C++-only (the 1.0 C boundary table)* | F O | | Open | Naming: F and O use `pf_native_*`; S uses `strategy_native_*`, which matches the existing runtime symbols. The v1 C spec `pf_native_run_spec_v1` pineforge.h:548 omitted `intrabar`, `path_order` and `abort_reporting`; the extension's policy tail carries all three now (`pf_native_abort_reporting_e` native_c_api.h:907) | F S | Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the epoch batch. @@ -1396,7 +1401,7 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1630-1631) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6982-6994) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1609-1610) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6982-6994) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). 2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5616-5620) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1659-1666) is calibrated against it. 3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6835-6842). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2557); valid in bar-open, applied and tick callbacks. diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index c1de9778..5688bef8 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -82,7 +82,10 @@ finding to report, not a step to take. 5. **Every public host member has a C spelling or a recorded reason.** The COVERAGE comment block (`COVERAGE` `sha256:71e3e35c1a2409524f511b5ee0e02df496852dc5ff629728a78c5c0401cb8ad0` native_c_api.h:52-55) lists them; a member added without a - row, or a row naming a member that no longer exists, fails. + row, or a row naming a member that no longer exists, fails. Every other C++ + capability the 1.0 C surface lacks is a row of the 1.0 C boundary table + (@ref native_engine), and the checker's `C_V1_EXCLUSIONS` row for it fails + when the gap closes or its C++ declaration goes. *Enforced:* `scripts/check_native_c_api_surface.py`. 6. **The C ABI is append-only within a major version.** Fields and functions @@ -288,7 +291,8 @@ no individual regression, and negative movement is outside it. ## Where to read next - [PineScript to native C++](@ref pine_to_native) — every Pine builtin mapped - to its C++ and C spelling, with the example that exercises it. + to its C++ spelling and, where the 1.0 C surface has one, its C spelling, + with the example that exercises it. - [Native engine](@ref native_engine) — the lifecycle, the run spec, the request vocabulary and the C ABI contract. - [ADR 0001](../adr/0001-kernel-adapter-boundary.md) — the boundary, its rules, diff --git a/docs/pages/index.md b/docs/pages/index.md index 6d115a50..83979c6f 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -42,7 +42,8 @@ the one a host programs against. Read **[Native engine](@ref native_engine)** for `NativeStrategyHost`, `configure_native`, execution terms and the C ABI contract. Coming from PineScript, start with **[PineScript to native C++](@ref pine_to_native)** — - every `strategy.*` builtin mapped to its C++ and C spelling, the runnable + every `strategy.*` builtin mapped to its C++ spelling and, where the 1.0 C + surface has one, its C spelling, the runnable host that exercises each, and a six-feature strategy migrated end to end. - I'm integrating from CMake @@ -226,7 +227,7 @@ Build the generated strategy module with the repository recipe (or | Page | What it covers | | --- | --- | | [Native engine](@ref native_engine) | The reference: lifecycle, run spec, request vocabulary, the C ABI contract. | -| [PineScript to native C++](@ref pine_to_native) | Every Pine builtin mapped to its C++ and C spelling, with a worked migration. | +| [PineScript to native C++](@ref pine_to_native) | Every Pine builtin mapped to its C++ spelling and, where the 1.0 C surface has one, its C spelling, with a worked migration. | | [Contributing as an LLM](@ref contributing_llm) | The repo map, the boundary invariants and the lane recipe, for an agent. | **How execution works** diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 7a335fd7..4defbc99 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -76,7 +76,9 @@ page does not re-list every enumerator. Request-value members live in builder and the id book over the primitives below. `` is separate: it is needed only to export a host as a loadable module (see @ref native_engine_examples). -`` is the C spelling of all of it. +`` is the C host surface: it drives the same kernel +through a callback table, and what it does not spell in 1.0 is the 1.0 C +boundary table (*Driving the kernel from C*). Coming from PineScript? **[PineScript to native C++](@ref pine_to_native)** maps each `strategy.*` concept to its native counterpart @@ -1843,19 +1845,21 @@ Every query below is a `const` member of `NativeStrategyHost` that copies owning values out: a row already returned is never invalidated by a later command or by the reset of the next run. Each has a C spelling unless the row says otherwise, and the C spellings are in -`` (see *Driving the kernel from C*). +`` (see *Driving the kernel from C*). A C spelling +marked partial carries a subset of the C++ record; the 1.0 C boundary table +there lists what it leaves out. | query | answers | C spelling | |---|---|---| -| `native_state()` | `NativeStateView` `native_host.hpp:282`: the lifecycle `kind`, the staged `spec`, the `phase`, the `completion`, the durable `failure`, the consumed high water and the decision floor | `strategy_native_state_v1` | +| `native_state()` | `NativeStateView` `native_host.hpp:282`: the lifecycle `kind`, the staged `spec`, the `phase`, the `completion`, the durable `failure`, the consumed high water and the decision floor | `strategy_native_state_v1` (partial: no staged spec, no failure context) | | `physical_position()` | `NativePhysicalPosition` `native_host.hpp:296`: `signed_units`, `average_price`, `lot_count` | `strategy_native_position_v1` | | `native_open_lots(mark)` | one `NativeOpenLot` `native_host.hpp:328` per open physical lot, oldest first, marked at `mark` | `strategy_native_open_lot_count_v1` / `_get_v1` | | `native_marked_equity(mark)` | the account's marked equity at `mark` | `strategy_native_marked_equity_v1` | -| `native_working_requests()` | one `NativeWorkingRequest` `native_host.hpp:669` per live request: its `definition`, its `remaining` and its `trigger_state` | `strategy_native_working_len_v1` / `_get_v1` | +| `native_working_requests()` | one `NativeWorkingRequest` `native_host.hpp:669` per live request: its `definition`, its `remaining` and its `trigger_state` | `strategy_native_working_len_v1` / `_get_v1` (partial: part of the definition) | | `trail_state(handle)` | `NativeTrailState` `native_host.hpp:657`: `activated`, `best_price`, `current_level`, `activation_ordinal`; `nullopt` when the handle is not a live trail | `strategy_native_trail_state_v1` | -| `native_events(after)` | `NativeMarketEvent` `native_host.hpp:370` rows: a `NativeEventKind` `native_host.hpp:360` (`Command`, `Driver`, `Account`) and exactly one of `command`, `driver` (`NativeDriverPoint` `market_driver.hpp:97`) or `account` (`NativeAccountObservation` `native_host.hpp:351`) — the rows the run's `event_retention` keeps (*What a run keeps of its events*) | `strategy_native_events_v1` | +| `native_events(after)` | `NativeMarketEvent` `native_host.hpp:370` rows: a `NativeEventKind` `native_host.hpp:360` (`Command`, `Driver`, `Account`) and exactly one of `command`, `driver` (`NativeDriverPoint` `market_driver.hpp:97`) or `account` (`NativeAccountObservation` `native_host.hpp:351`) — the rows the run's `event_retention` keeps (*What a run keeps of its events*) | `strategy_native_events_v1` (partial: each kind's documented fields) | | `native_event_window_start()` | the oldest ordinal a read can still return: every command event at or above it is retained; 1 while nothing was dropped | `strategy_native_event_window_v1` | -| `current_execution_point()` | `NativeCurrentPointView` `native_host.hpp:646`: the active callback's decision context, its price, the `NativeCurrentQuoteKind` `native_host.hpp:451` and the ordinal the quote came from; `nullopt` outside a decision point | `pf_native_decision_v1::price` / `::quote_kind` | +| `current_execution_point()` | `NativeCurrentPointView` `native_host.hpp:646`: the active callback's decision context, its price, the `NativeCurrentQuoteKind` `native_host.hpp:451` and the ordinal the quote came from; `nullopt` outside a decision point | `pf_native_decision_v1::price` / `::quote_kind` (partial: no quote origin) | | `native_risk_state()` | `NativeRiskState` `native_host.hpp:631`: whether openings are blocked and why, the risk day, the fills counted in it, the loss-day streak, the peak equity and the day's opening equity | `strategy_native_risk_state_v1` | | `native_liquidation_price()` | the solved level `L`, or `nullopt` | `strategy_native_liquidation_price_v1` | | `current_partial_bar()` | the lookahead-free bar so far, through the last path point consumed | `strategy_native_partial_bar_v1` | @@ -1864,8 +1868,8 @@ says otherwise, and the C spellings are in | `native_decision_floor()` (`native_host.hpp:1343`) | the run's monotonic decision floor in epoch ms — the same value `NativeStateView::decision_floor_ms` carries, and the lower bound every request's birth is compared against | `pf_native_state_v1::decision_floor_ms` | | `native_consumed_high_water()` (`native_host.hpp:1348`) | the highest `run_number` this host has consumed. It lives **outside** per-run reset, so the next configure on the same host needs a strictly larger number; a fresh host reads 0 | `pf_native_state_v1::consumed_high_water` | | `native_continuation_hash()` | the consumer's continuation identity: a fold of its live state (*What the continuation and the broker-state hash fold*), the timezone folded by its content, so the same spec over the same bars and zone rules answers the same value on every host | `strategy_native_continuation_hash_v1` | -| `native_sized_units(sized, price, equity, fx)` | the kernel's own `Sized` resolution as a pure query | none — see *Previewing a basis* | -| `inspect_current_execution(cmd)` | `NativeCurrentExecutionPreview`, with a `NativeCurrentRefusal` `native_host.hpp:685` when the command cannot be consumed here | none — `strategy_native_execute_current_v1` answers the same verdicts | +| `native_sized_units(sized, price, equity, fx)` | the kernel's own `Sized` resolution as a pure query | `strategy_native_sized_units_v1` | +| `inspect_current_execution(cmd)` | `NativeCurrentExecutionPreview`, with a `NativeCurrentRefusal` `native_host.hpp:685` when the command cannot be consumed here | none in 1.0 (scheduled for 1.1.0): `strategy_native_execute_current_v1` applies the command and is no preview | **Reading a failure.** `native_state().failure` is a `NativeFailure` (`native_host.hpp:237`): a `NativeFailureCode`, the `NativeFailureOperation` @@ -2086,6 +2090,7 @@ the host calls `mark_native_report_point(report_ts)` from inside its own callback, naming the label the point carries. The kernel performs the extremes fold and curve append. The call returns `true` when it appends; it returns `false` and changes nothing outside a running `KernelRecordedAtHostMarks` run. +A C host has no call that marks one (the 1.0 C boundary table). The Pine adapter is one marking host — its report has one point per published *source* slot, which is not the same series of instants: a `calc_on_order_fills` re-entry marks the slot it opened at the fill and the @@ -3822,8 +3827,9 @@ subscriptions, and the generic risk limits — travel in `strategy_configure_native_ext_v1`. It replaces `strategy_configure_native_v1` rather than following it, because the kernel configures each run exactly once: a second extended configure of a Ready -handle is refused without changing it. The one field of `NativeRunSpec` it deliberately does not carry is -`identity`, which the base spec owns. +handle is refused without changing it. It does not carry `identity`, which the +base spec owns, or `timeframe_undetected`, which no C spec can set: a C run +always names both timeframes. The nine enum-valued words of the two specs are `uint32_t` words holding a value of a C enumeration that names its kernel enumeration's values one for @@ -4007,7 +4013,7 @@ host that must abort does it from an observation callback, where the established "non-zero ends the run `Failed`" rule is untouched. A fifth answering hook, `on_close_units`, is the **units half** of -`resolve_execution_terms` — and the only half this header exposes. It is +`resolve_execution_terms`; the price and shape half is `on_execution_terms`. It is consulted for `PF_NATIVE_INTENT_HOST_SIZED` candidates and nothing else, and it is what makes `PF_NATIVE_OWNER_BIND_COHORT` reachable: the kernel pairs that owner with exactly one intent, a host-sized close, whose quantity comes @@ -4059,21 +4065,61 @@ BASE-CLASS SEAMS (below). The entry-bar mask scenario of `tests/test_native_c_api.c` reproduces the C++ witness `tests/test_e6_entry_bar_mask_declaration.cpp` number for number. -**What is not exposed, and why.** The header opens with a **COVERAGE** block: -one line per public member of `NativeStrategyHost`, carrying either the C -spelling (`[C]`) or the reason there is none (`[--]`). Five members are excluded today — `prepare_native_begin` (it borrows the codegen ingress a C -host never supplies), `inspect_current_execution` (its preview carries the -account-effect projection and a variable-length closed-row P&L vector with no -size-prefixed POD; `strategy_native_execute_current_v1` answers the same -verdicts) and `submit_market` / `replace_market` (C++ conveniences that refuse -non-market extras; `native_closed_rows_amended` is the C++-only row-amendment -notification; the same request is `strategy_native_submit_v1` with -`PF_NATIVE_TRIGGER_MARKET`). Every policy hook has a C route since R5 lane F4: +**The 1.0 C boundary.** A C host drives the same kernel a C++ host does, and +where the C surface declares a field or a call it answers the C++ value +(`tests/test_native_c_api.c` and its C++ twins). It does not declare +everything. Every C++ capability the 1.0 C surface does not expose is a row +below, with its reason, the C route where there is one, and the row that fails +when the gap closes or the capability goes. 1.0 makes no C/C++ parity claim +beyond the declared fields and calls, and none for these rows. Four gaps are +scheduled for 1.1.0 (lane C-SURFACE-2): the execution preview, the applied +event's origin and label, a closed row's entry comment and the replace +options. The rest have no lane. The checker rows are three kinds: a `[--]` row +of the header's **COVERAGE** block (one line per public member of +`NativeStrategyHost`, with its C spelling or the reason it has none), a named +exclusion in `ENUM_TWINS`, and a `C_V1_EXCLUSIONS` row. The last two live in +`scripts/check_native_c_api_surface.py`. A `C_V1_EXCLUSIONS` row fails when its +C++ declaration goes, or when its C spelling appears in the record it names +(each pattern also catches a suffixed or prefixed spelling), and the checker +fails when this table cites a row once too few or too many. + +| C++ capability absent from the 1.0 C surface | reason, and the C route | checker row | +|---|---|---| +| `prepare_native_begin` | Offered once per begin with that begin's arguments (`NativeBeginArgs`: the bars, the timeframe literals, the magnifier settings, the stream facts, and the rich overload's inputs, symbol and opaque overrides). A C host supplies each of them itself: it declares its run with `strategy_configure_native_ext_v1` and begins in `on_run_begin`. | COVERAGE `[--] prepare_native_begin` | +| `inspect_current_execution` | Scheduled for 1.1.0. The preview (`NativeCurrentExecutionPreview`) carries the refusal, the settlement readiness, the account projection, the closed rows' P&L and the terms outcome, and moves nothing. C has no non-mutating call: `strategy_native_execute_current_v1` applies the command, so it is not a preview. The facts are representable (`pf_native_precommit_view_v1` flattens them for `on_precommit`); the call is what is missing. | COVERAGE `[--] inspect_current_execution` | +| `submit_market`, `replace_market` | C++ conveniences that refuse a non-market trigger, capacity, owner, group, anchor or late-sized intent instead of dropping it. C has no market-only guard: the same request is `strategy_native_submit_v1` / `_replace_v1` with `PF_NATIVE_TRIGGER_MARKET` and a zero-filled struct. | COVERAGE `[--] submit_market`, `[--] replace_market` | +| `native_closed_rows_amended` | A C++ host that writes the kernel's closed rows names the first one it changed here. C has no write access to the rows, so it has nothing to name. | COVERAGE `[--] native_closed_rows_amended` | +| the deprecated `hash_source_extension` | Kept so an existing C++ subclass compiles; C has only ever had the current spelling, `on_hash_extension`. | BASE-CLASS SEAMS `[--] hash_source_extension` | +| report points at the host's own cadence (`NativeReportPolicy::KernelRecordedAtHostMarks`) | Under it the host marks each report point from inside its own callbacks; a C++ host marks through `NativeStrategyHost::mark_native_report_point`, which returns `false` outside a running `KernelRecordedAtHostMarks` run. C has no call that marks one: `pf_native_report_policy_e` leaves value 2 unnamed and the C host refuses it (`PF_NATIVE_E_TAG`), because a C host could only declare a series nobody records. A C host's recorded series is `PF_NATIVE_REPORT_KERNEL_RECORDED`. | COVERAGE `[--] mark_native_report_point`, `ENUM_TWINS["pf_native_report_policy_e"]`, the `KernelRecordedAtHostMarks` exclusion | +| `ReplaceOptions` (`retain_trigger_state`, `keep_handle`, `keep_binding`) | Scheduled for 1.1.0. The C replace calls take no options word: a plain replace is available, and carrying the trigger state, the handle or a book binding across it needs C++. | `C_V1_EXCLUSIONS["replace_options"]` | +| `ExecutionAppliedEvent` beyond the fill | The event carries its definition (origin, label, comment, the request), the closed-row range it booked, the remaining units and allowance before and after, the execution scope and the whole cursor. `pf_native_applied_v1` carries the fill's prices, units, ticket, cycles, terminal reason and incarnations only. Origin and label are scheduled for 1.1.0; the rest has no lane. Inside `on_applied` a C host cannot tell a kernel liquidation from its own fill (`on_margin_call` arrives after it), and it finds the rows the fill booked only by probing `strategy_closed_trade_entry_id` for the new row count. | `C_V1_EXCLUSIONS["applied_event_tail"]` | +| a closed row's entry comment (`closed_trade(i).entry_comment`) | Scheduled for 1.1.0. No C accessor returns it and `pf_trade_t` carries no strings. `strategy_closed_trade_entry_incarnation` names the request whose fill opened the lot, whose comment the C host wrote itself; while the lot is open, `pf_native_open_lot_v1::entry_comment` reads it. | `C_V1_EXCLUSIONS["closed_entry_comment"]` | +| closed rows in place (`closed_trade_count()` / `closed_trade(i)`, `report_trade_count()` / `get_report_trade(i)`) | A C++ host reads a closed row whole, at any time, every `Trade` field included. A C host reads the numeric fields from a filled `pf_report_t` (the run's `strategy_native_run_v1` output, or `strategy_stream_fill_report`), and the strings, the close cause and the entry incarnation by report index. | `C_V1_EXCLUSIONS["closed_rows_in_place"]` | +| the working request's whole definition (`NativeWorkingRequest::definition`) | `pf_native_working_v1` reads back the request's tags, levels, relation tail, origin, label and comment. It leaves out a reduce's size kind, claim and basis, the `Sized` block (side, basis, time, price, grid policy, fee reserve), `Limit::fill_through`, a host-sized request's kind and side, every `BindOpenings` handle past the first, the predecessor, root, priority and kept binding, and the trail's seed. The seed is a submission value (`pf_native_request_v1::trail_best_seed`); the live ride is `strategy_native_trail_state_v1`. | `C_V1_EXCLUSIONS["working_definition"]` | +| `NativeDecisionContext::closes_session_day_open_ended` | The C decision carries the other three session-day facts (`in_session`, `opens_session_day`, `closes_session_day`). The fourth differs from `closes_session_day` only on a batch's final bar, for a host recomputing a batch whose last input is still forming; a C host's live edge is a `strategy_stream_*` stream, and the decision's session tail gives the calendar facts for a host that applies its own rule. | `C_V1_EXCLUSIONS["open_ended_session_day"]` | +| the rest of the callback contexts | The decision has no input interval, no source price time, no driver statistics and no quote origin ordinal (`NativeCurrentPointView::quote_origin_ordinal`). `on_input` gets the bar, its index and whether it completes a script interval, without the input or script interval; `on_tick` gets no sequence. | `C_V1_EXCLUSIONS["callback_contexts"]` | +| host-sized openings, and host-sized closes outside two shapes (`HostSized{Open}`, `HostSized{Close}` under `Independent`, `BindOpening` or `BindOpenings`) | The C surface accepts `PF_NATIVE_INTENT_HOST_SIZED` as a cohort close or a Book-scoped `WAIT_FOR_APPLIED` child only, and refuses every other use as `PF_NATIVE_E_UNSUPPORTED`. A C host sizes an opening with `Sized`. | `C_V1_EXCLUSIONS["host_sized_opening"]`; the refusal is asserted in `tests/test_native_c_api.c` | +| the units answer of `resolve_execution_terms` | A C++ hook may replace the kernel's units for an unresolved `Sized` or `ScopeFraction` candidate. `on_execution_terms` answers the price, the opening shape and the grid policy only, and `on_close_units` is consulted for a host-sized candidate and nothing else. | `C_V1_EXCLUSIONS["terms_units"]` | +| `ClosedLotExcursionFacts::account_fx` | The rate a closing row's P&L and excursion columns take, so an excursion owner answering in the row's currency converts at it. `pf_native_lot_excursion_v1` has no rate, so a C owner in an FX-converted run cannot. | `C_V1_EXCLUSIONS["excursion_fx"]` | +| folding any bytes into the broker-state hash (`hash_host_extension`'s `BrokerStateHashSink`) | A C++ override folds what it likes and may replace the default. `on_hash_extension` answers one 64-bit digest, which the kernel folds after its own bytes under a tag, so a C host's extended hash never equals a C++ host's own fold of the same state. | `C_V1_EXCLUSIONS["hash_sink"]` | +| `declare_opened_lot_entry_bar_mask` outside `on_applied` | The C++ seam has no phase rule; the C spelling is legal inside `on_applied` alone and `PF_NATIVE_E_STATE` everywhere else. | `C_V1_EXCLUSIONS["entry_bar_mask_phase"]`; the refusals before and after a run are asserted in `tests/test_native_c_api.c` | +| `NativeRunSpec::timeframe_undetected` | No C spec carries it and the base spec requires both timeframe literals, so a C run always names its timeframes. | `C_V1_EXCLUSIONS["timeframe_undetected"]` | +| run statistics through the inherited accessors (`net_profit()`, `open_profit()`, `open_trades_capital_held()`, `max_runup_percent()`, `max_drawdown_percent()`, `active_account_currency_fx()`, `max_contracts_held_all()` and its long / short siblings, `eventrades()`) | Live, C reads `strategy_current_equity`, `strategy_position_size` and `strategy_native_marked_equity_v1`. Everything else comes from a filled report's `pf_metrics_t`, whose drawdown and run-up are walked from the recorded curve; C has no maximum contracts held and no capital held. | `C_V1_EXCLUSIONS["inherited_statistics"]` | +| the hook views' definitions (`NativeExecutionTermsFacts`, `NativePrecommitView`) and the cursors | The C++ views carry the request's definition, its allowance or scope, and the settlement readiness; the C views carry the intent and trigger tags. A C cursor carries the ordinal, the effective time, `t`, the interval index, the provenance and the phase, never the coordinate's interval instants or source price time, and event rows and the decision drop `t`. | `C_V1_EXCLUSIONS["hook_views"]` | +| the command events' payloads | `pf_native_event_v1` flattens the event kinds into one tagged record with each kind's documented fields. It drops the definitions and the rejected or attempted requests, the command surface, the authority before and after, the remaining, allowance and pending projections, the cause and receipt ids, the attempted terms and the terms' resolved input, the requested delta, prior adjustment ids and `ReplacedEvent::kept_handle()`. | `C_V1_EXCLUSIONS["event_payloads"]` | +| the command results (`SubmitResult`, `ReplaceResult`, `CancelResult`, `NativeCurrentExecutionResult`) | C returns the handle or the refusal, or a status, without the event ordinal; `strategy_native_execute_current_v1` returns the outcome's code, not the event. The event history (`strategy_native_events_v1`) is the route. | `C_V1_EXCLUSIONS["command_results"]` | +| `native_state()`'s staged spec and failure context (`NativeStateView::spec`, `NativeFailure::context`) | `pf_native_state_v1` carries the lifecycle, the failure's code, operation, discriminator and ordinal, and the phase facts, but not the spec or the failure's cause ordinal, recipient and cursor. | `C_V1_EXCLUSIONS["state_view"]` | +| two minor ones: `trace(name, value)`, and re-declaring the optional hooks after creation | A C++ host writes report trace rows; C can turn tracing on (`strategy_set_trace_enabled`) and read the rows a report carries, not write one. A C++ declaration of `on_bar_open` or `on_precommit` stands until the host makes another; a C table fixes both once, at `strategy_native_host_create_v1`, which moves no value. | `C_V1_EXCLUSIONS["host_minor"]` | +| the C++ library around the host | Spec and curve validation and digests (`validate_native_run_spec`, `normalize_native_run_spec`, the subscription and auxiliary-feed validators, `native_run_spec_digest` and the other digests, `validate_native_fx_curve`, `native_fx_curve_digest`); the input preflight (`preflight_native_inputs`, `native_bar_structurally_valid`); the calendar utilities of `native_calendar.hpp`; the request helpers (`quantity_on_grid`, `market_request`); the module export macro of `native_module.hpp`; the run identity a `RequestHandle` carries. A C host validates by configuring (`strategy_configure_native_ext_result_v1` leaves the handle unconfigured on a refusal) and has no digest. The kernel's own machinery (`WorkingRequestCore`, `INativeDriverSink`) is no host surface in either language. | `C_V1_EXCLUSIONS["library_facilities"]` | +| `native_toolkit.hpp` | Header-only C++ over the host's own commands: `submit_bracket` and its receipt, and the `OrderBook` id book. A C host writes the same commands itself: each bracket leg is a `strategy_native_submit_v1` with `PF_NATIVE_OWNER_WAIT_FOR_APPLIED` and a `PF_NATIVE_GROUP_MEMBER` group, answering its own refusal, and an id book is its own key-to-handle map over `_submit_v1` / `_replace_v1` / `_cancel_v1`. | `C_V1_EXCLUSIONS["toolkit"]` | + +Every policy hook has a C callback since R5 lane F4: `validate_execution_precommit` is `on_precommit`, `resolve_anchored_level` `on_anchored_level`, the price and shape half of `resolve_execution_terms` `on_execution_terms`, `declare_auxiliary_feed` `strategy_native_declare_auxiliary_feed_v1` and `native_sized_units` -`strategy_native_sized_units_v1`. A table that leaves `on_bar_open` or +`strategy_native_sized_units_v1`. A callback carries the facts and answers its +C struct declares; what it leaves out is in the table above. A table that leaves `on_bar_open` or `on_precommit` out declares that hook absent when the host is created (`declare_native_bar_open_hook` / `declare_native_precommit_hook`, R5 lane D2-A), which moves no value. diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 99a3b476..43368c4a 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -72,7 +72,9 @@ gain: (native_order.hpp:1043) — readable in order from `native_events` (native_host.hpp:1317). - **Any language.** The same kernel drives from C through a callback table - (`pf_native_callbacks_v1` native_c_api.h:2456), so a host in Rust, Go, Python or Zig needs no C++. + (`pf_native_callbacks_v1` native_c_api.h:2456), so a host in Rust, Go, Python or Zig needs no C++ + unless it needs a capability the 1.0 C surface does not spell (the 1.0 C boundary table, + @ref native_engine). - **One binary, no transpiler.** Link `libpineforge`, ship an executable or a loadable module. @@ -267,7 +269,10 @@ source-only features state why a native counterpart does not exist. host answers the units itself from `resolve_execution_terms` (native_host.hpp:935), which sees the kernel-resolved quantity as the facts' `RemainingUnits` native_order.hpp:550 and may return its own. A C host answers -the same question through `on_close_units` native_c_api.h:2548 for a close. +it only for the two host-sized closes the C surface admits, a cohort close and a +Book-scoped `WAIT_FOR_APPLIED` child, through `on_close_units` +native_c_api.h:2548; a host-sized opening, and replacing the kernel's own `Sized` +or `ScopeFraction` units, are C++-only in 1.0. ## Orders {#pine_to_native_map_orders} @@ -411,7 +416,7 @@ lane N12 measured why — TradingView's drawdown latch samples at the close and still admits a reversal, its loss-day streak counts trades, its intraday loss closes at the path's adverse extreme and withdraws the book, and its fill cap charges slots and transfers quota on a chart-day key. The ruling of record is -ADR-0001's `risk` row and `native-feature-parity.md:474`; the permanent witness +ADR-0001's `risk` row and `native-feature-parity.md:479`; the permanent witness is `tests/test_adapter_risk_relower.cpp`. ## Margin and liquidation {#pine_to_native_map_margin} @@ -744,7 +749,7 @@ native_c_api.h:2267 through `strategy_configure_native_ext_v1` native_c_api.h:3113, and `strategy_native_run_v1` native_c_api.h:2652 drives the bars into a `pf_report_t` pineforge.h:469. -Three rules make the C door behave like the C++ one: +Three rules govern the C door: 1. **Every struct is tagged and size-prefixed.** Set `struct_size` native_c_api.h:2096 and `version` native_c_api.h:2097; a mismatch is refused @@ -756,19 +761,22 @@ Three rules make the C door behave like the C++ one: only the recalculate hook and still expects the bar hook silently trades nothing. -Not every member of the C++ host has a C spelling, and each that has none -carries a recorded reason. Five members have no direct C spelling: `prepare_native_begin` (it -borrows the codegen ingress a C host never supplies), `inspect_current_execution` -native_host.hpp:1087 (its preview has no size-prefixed POD; -`strategy_native_execute_current_v1` answers the same verdicts) and the two -market-only conveniences `submit_market` / `replace_market`, and `native_closed_rows_amended` (the C++-only row-amendment notification). Every policy hook -has a C route: `validate_execution_precommit` native_host.hpp:948 is +Not every C++ capability has a C spelling in 1.0. Five members of the C++ host +have none: `prepare_native_begin` (a C host supplies each begin argument itself), +`inspect_current_execution` native_host.hpp:1087 (scheduled for 1.1.0: +`strategy_native_execute_current_v1` applies the command and is no preview), the +two market-only conveniences `submit_market` / `replace_market`, and +`native_closed_rows_amended` (a C host cannot write the rows it would name). +Every other C++ capability the 1.0 C surface lacks (record fields, hook answers, +conveniences, library functions) is a row of the 1.0 C boundary table in +@ref native_engine, with its reason and the checker row that pins it. Every policy hook +has a C callback: `validate_execution_precommit` native_host.hpp:948 is `on_precommit`, `resolve_anchored_level` native_host.hpp:1000 `on_anchored_level`, the price and opening-shape halves of `resolve_execution_terms` native_host.hpp:935 `on_execution_terms`, `native_sized_units` native_host.hpp:1302 `strategy_native_sized_units_v1` and the `hash_host_extension` seam `on_hash_extension`; only its deprecated spelling -`hash_source_extension` has none. The `COVERAGE` comment block at `sha256:71e3e35c1a2409524f511b5ee0e02df496852dc5ff629728a78c5c0401cb8ad0` native_c_api.h:52-55 +`hash_source_extension` has none. What each callback carries is its C struct. The `COVERAGE` comment block at `sha256:71e3e35c1a2409524f511b5ee0e02df496852dc5ff629728a78c5c0401cb8ad0` native_c_api.h:52-55 lists every public member with either its C spelling or that reason, and `scripts/check_native_c_api_surface.py` proves the list is exactly that class's public surface. diff --git a/include/pineforge/native_c_api.h b/include/pineforge/native_c_api.h index 0d6d81d6..6381ffd3 100644 --- a/include/pineforge/native_c_api.h +++ b/include/pineforge/native_c_api.h @@ -6,8 +6,8 @@ * A host that is not written in C++ drives the kernel through this header: * it hands the runtime a callback table, runs a batch of bars, and submits, * replaces, cancels or executes order requests from inside those callbacks. - * It is the C spelling of 's NativeStrategyHost — - * the same kernel, the same events, no PineScript and no codegen. + * It drives the same kernel as 's NativeStrategyHost; + * the C++ it does not spell in 1.0 is docs/pages/native-engine.md's boundary table. * * SCOPE * ───── @@ -98,10 +98,10 @@ * its script interval and session days are the decision's * session tail * [C] trail_state strategy_native_trail_state_v1 - * [--] inspect_current_execution its preview carries the account-effect projection and a variable- - * length closed-row P&L vector with no size-prefixed POD; - * strategy_native_execute_current_v1 answers the same verdicts as - * pf_native_execute_outcome_e and pf_native_refusal_e + * [--] inspect_current_execution scheduled for 1.1.0 (C-SURFACE-2): C has no non-mutating call, + * and strategy_native_execute_current_v1 applies the command, so + * it is no preview; the preview's refusal, readiness, account + * projection and closed-row P&L have no size-prefixed POD in 1.0 * [C] execute_current strategy_native_execute_current_v1 * [--] mark_native_report_point the C spec does not name KernelRecordedAtHostMarks, * so a C host cannot select its mark cadence @@ -169,7 +169,7 @@ * read, nothing is submitted * * Three asymmetries this list does not reach, recorded here because a C host - * will look for them. + * will look for them; the 1.0 C boundary table lists every other one. * * pf_native_working_v1 has two trigger numbers, p1 and p2 -- plus, in its own * additive tail, trail_has_arm_price, which tells an absent arm from an arm at @@ -2243,8 +2243,8 @@ typedef struct pf_native_subscription_v1 { * what #strategy_configure_native_v1 would have. * * Every field of NativeRunSpec that is not fixed by - * #pf_native_run_spec_v1 now travels here. The one deliberate omission is - * `identity`, which the base spec owns. + * #pf_native_run_spec_v1 now travels here, except `identity`, which the base + * spec owns, and `timeframe_undetected`: a C run always names both timeframes. * * This struct has SIX published layouts and the runtime accepts any of * them: the base layout the L13 lane first shipped @@ -2538,7 +2538,7 @@ typedef struct pf_native_callbacks_v1 { double* favorable, double* adverse); /** How many units a host-sized CLOSE takes — the units half of - * `resolve_execution_terms`, and the only half this header exposes. + * `resolve_execution_terms` (the price half is `on_execution_terms`). * Consulted for #PF_NATIVE_INTENT_HOST_SIZED candidates and nothing * else; without it a cohort close resolves no quantity and stands * deferred, which is exactly what the C++ default does. ANSWERING diff --git a/include/pineforge/native_host.hpp b/include/pineforge/native_host.hpp index b8d63749..80073641 100644 --- a/include/pineforge/native_host.hpp +++ b/include/pineforge/native_host.hpp @@ -278,7 +278,7 @@ static_assert(std::is_nothrow_move_assignable_v); /// pointer to the staged spec (nullptr when Unconfigured), the phase, how a /// Completed run ended, the durable failure, the consumed run-number high water /// and the monotonic decision floor. The spec pointer is valid until the next -/// configure or begin. C spelling: strategy_native_state_v1. +/// configure or begin. C spelling: strategy_native_state_v1 (no spec, no failure context). struct NativeStateView { NativeLifecycleKind kind = NativeLifecycleKind::Unconfigured; const NativeRunSpec* spec = nullptr; @@ -365,8 +365,8 @@ enum class NativeEventKind : std::uint8_t { /// One owning row of native_events(after_ordinal): a command event, a driver point /// or an account observation, tagged by kind. Later commands and the next run's -/// reset do not invalidate a row already returned. C spelling: -/// strategy_native_events_v1, which flattens the same rows into one tagged POD. +/// reset do not invalidate a row already returned. C spelling: strategy_native_events_v1, +/// one tagged POD with each kind's documented fields; the rest is C++-only in 1.0. struct NativeMarketEvent { NativeEventKind kind = NativeEventKind::Command; uint64_t ordinal = 0; @@ -642,7 +642,7 @@ struct NativeRiskState { /// What current_execution_point() answers inside a callback: the point's decision /// context, its price, which quote that price is, and the ordinal the quote came /// from. nullopt outside a decision point. C spelling: pf_native_decision_v1's -/// price and quote_kind, on every callback. +/// price and quote_kind, on every callback (no quote origin ordinal). struct NativeCurrentPointView { NativeDecisionContext decision; double price = 0.0; @@ -930,8 +930,8 @@ class NativeStrategyHost : public BacktestEngine { /// price, and units for an unresolved HostSized request; the default is the /// identity price with no units, which is what every bare host wants. Answering no /// units for a HostSized candidate is MatchRejectReason::TermsUnresolved. It does - /// not supply a second matcher, book or cash path. C spelling: the units half - /// only, pf_native_callbacks_v1::on_close_units. + /// not supply a second matcher, book or cash path. C spelling: on_execution_terms + /// (price, shape, grid) and on_close_units (a host-sized close's units only). virtual native_order::ExecutionTerms resolve_execution_terms( const NativeExecutionTermsFacts& facts) const { return {facts.default_resolved_price, std::nullopt, @@ -1082,8 +1082,8 @@ class NativeStrategyHost : public BacktestEngine { /// A read-only preview of a current execution: the settlement readiness, any typed /// refusal or terms outcome, and the ordered closed-row P&L for an Applied-ready /// command. It is never an apply token — execute_current revalidates, and editing - /// the preview cannot authorize or alter a fill. No C spelling: - /// strategy_native_execute_current_v1 answers the same verdicts. + /// the preview cannot authorize or alter a fill. No C spelling in 1.0 (C-SURFACE-2, + /// 1.1.0): strategy_native_execute_current_v1 applies the command, so it is no preview. NativeCurrentExecutionPreview inspect_current_execution(const NativeCurrentExecution&) const; /// Consume, synchronously, a request accepted or replaced in this very callback. /// Answers a refusal or the applied outcome; applied effects and relationship @@ -1179,14 +1179,14 @@ class NativeStrategyHost : public BacktestEngine { /// The only setup call. Copies the candidate spec, normalizes it and stages it /// atomically: Unconfigured or a Completed run with a larger run number becomes /// Ready, and a refusal is Failed with no partial apply. Calling it again while - /// Ready is a Contract failure — use a new host to change unconsumed setup. C - /// spelling: strategy_configure_native_v1 / strategy_configure_native_ext_v1. + /// Ready is a Contract failure — use a new host to change unconsumed setup. C spelling: + /// strategy_configure_native_v1 / _ext_v1, typed refusal _ext_result_v1. NativeSetupResult configure_native(const NativeRunSpec& spec); /// Stage the run's immutable FX epoch, legal only while Ready. Parallel /// timestamp/rate arrays of equal length, strictly increasing timestamps, finite /// positive rates; an empty curve clears it and restores the scalar account_fx /// fallback. Refused with WrongPhase once the run is Running. C spelling: - /// strategy_configure_native_fx_curve_v1. + /// strategy_configure_native_fx_curve_v1, typed refusal _fx_curve_ext_v1. NativeFxCurveSetupResult configure_native_fx_curve(const NativeFxCurve& curve); /// The whole run state as one owning read. The only observation of the lifecycle /// and of the durable failure; last_error() is presentation text beside it. C diff --git a/include/pineforge/native_order.hpp b/include/pineforge/native_order.hpp index db7e3c17..a5caf782 100644 --- a/include/pineforge/native_order.hpp +++ b/include/pineforge/native_order.hpp @@ -68,8 +68,8 @@ enum class Side : std::uint8_t { Long = 0, Short = 1 }; /// opening, which must declare where it trades. A host-sized request that reaches /// a candidate with no units answered is MatchRejectReason::TermsUnresolved. It is /// the adapter's sizing seam; a bare host that wants the kernel to resolve a cash -/// or equity basis uses Sized instead, and the C surface refuses HostSized under -/// any owner but BindCohort. +/// or equity basis uses Sized instead. The C surface admits HostSized as a cohort +/// close or a Book-scoped WaitForApplied close only (the 1.0 C boundary table). struct HostSized { HostSizedKind kind = HostSizedKind::Open; std::optional side; diff --git a/scripts/check_native_c_api_surface.py b/scripts/check_native_c_api_surface.py index 75638cd4..cd3d6e3a 100644 --- a/scripts/check_native_c_api_surface.py +++ b/scripts/check_native_c_api_surface.py @@ -36,6 +36,19 @@ value. So a kernel enumerator appended without a C name fails here -- the static_asserts in src/native_c_host.cpp pin the names they list, and cannot see one that nobody listed -- and so does a C enumeration nobody classified. + +R5 lane H-DOCGATES adds the 1.0 C boundary (supervisor ruling, 2026-09-26: +C-SURFACE-2 moves to 1.1.0, so 1.0 lists every C++ capability the C surface +does not expose, with its reason and the row that pins it). The censuses above +are existence checks: a `[C]` row proves a C spelling exists, not that the C +record carries every C++ fact, and ENUM_TWINS starts from the C side. Each +C_V1_EXCLUSIONS row names one capability the C surface lacks: the C++ +declaration that must still exist, and the C spellings that must stay absent +from a C record or from every C declaration. A pattern ends in `\w*`, so a +suffixed or prefixed spelling (`strategy_..._entry_comment_v1`, +`request_origin`) closes the gap as visibly as the plain one. The boundary +table of docs/pages/native-engine.md cites every row exactly once, and cites no +row this checker does not have. """ from __future__ import annotations @@ -49,6 +62,7 @@ ENGINE = INCLUDE / "engine.hpp" C_API = INCLUDE / "native_c_api.h" PUBLIC_C = INCLUDE / "pineforge.h" +DOC_C_BOUNDARY = ROOT / "docs" / "pages" / "native-engine.md" # Members that are not part of the host's own surface: the copy/move deletions # the class spells out, and the friend declaration. @@ -83,8 +97,10 @@ "pf_native_open_directions_e": ("native_run_spec.hpp", "NativeOpenDirections", {}), "pf_native_report_policy_e": ("native_run_spec.hpp", "NativeReportPolicy", { "KernelRecordedAtHostMarks": - "its host names each report point from inside its own callbacks, and " - "the C callback table has no call that marks one (lane E22)", + "under it the host marks each report point, and the kernel's mark is " + "reached only by the Pine source host (not installed API); a C host has " + "no call that marks one and could only declare a series nobody records " + "(lane E22; the 1.0 C boundary table)", }), "pf_native_price_grid_e": ("native_run_spec.hpp", "NativePriceGrid", {}), "pf_native_grid_rounding_e": ("native_run_spec.hpp", "NativeGridRounding", {}), @@ -164,6 +180,70 @@ "pf_native_plan_e": "alternative index of the std::variant native_order::ExecutionPlan", } +# The 1.0 C boundary: key -> (the header under include/pineforge that declares +# the C++ capability, a spelling of it that must still be there, and the pairs +# (C region, pattern) whose pattern must stay absent from that region). A region +# is a C struct's body, or "@declarations": every declaration of the two C +# headers, comments cut out. docs/pages/native-engine.md's boundary table gives +# each row's reason and C route. +C_V1_EXCLUSIONS: dict[str, tuple[str, str, tuple[tuple[str, str], ...]]] = { + "replace_options": ("native_order.hpp", "struct ReplaceOptions", ( + ("@declarations", r"\b\w*(?:keep_handle|keep_binding|retain_trigger_state|replace_opt)\w*"),)), + "applied_event_tail": ("native_order.hpp", "struct ExecutionAppliedEvent", ( + ("pf_native_applied_v1", r"\b\w*(?:origin|label|comment|definition|first_trade" + r"|trade_count|allowance|scope|cursor)\w*"), + ("@declarations", r"\bpf_native_applied_(?!v1\b)\w+"))), + "closed_entry_comment": ("engine.hpp", "std::string entry_comment;", ( + ("@declarations", r"\bstrategy_\w*entry_comment\w*"), + ("pf_trade_s", r"\b\w*comment\w*"))), + "closed_rows_in_place": ("engine.hpp", "const Trade& closed_trade(std::size_t i) const", ( + ("@declarations", r"\bstrategy_(?:native_)?(?:closed_trade_count|closed_trade_get" + r"|closed_trade_row|report_trade)\w*"),)), + "working_definition": ("native_order.hpp", "std::optional best_seed", ( + ("pf_native_working_v1", r"\b(?:reduce_\w+|size_\w+|side|grid_policy|reserve_percent_fee" + r"|fill_through|\w*best_seed\w*|predecessor\w*|root\w*|priority\w*" + r"|kept_binding\w*|host_sized\w*)\b"),)), + "open_ended_session_day": ("market_driver.hpp", "bool closes_session_day_open_ended", ( + ("pf_native_decision_v1", r"\b\w*open_ended\w*"),)), + "callback_contexts": ("market_driver.hpp", "native_calendar::NativeInterval input_interval", ( + ("pf_native_decision_v1", r"\b\w*(?:input_interval|source_price_time|driver_statistics" + r"|sub_bars_processed|quote_origin)\w*"),)), + "host_sized_opening": ("native_order.hpp", "enum class HostSizedKind", ( + ("pf_native_request_v1", r"\b\w*(?:host_sized|sized_kind)\w*"),)), + "terms_units": ("native_order.hpp", "struct ExecutionTerms", ( + ("pf_native_terms_v1", r"\b\w*units\w*"),)), + "excursion_fx": ("engine.hpp", "double account_fx = 1.0;", ( + ("pf_native_lot_excursion_v1", r"\b\w*fx\w*"),)), + "hash_sink": ("engine.hpp", "class BrokerStateHashSink", ( + ("@declarations", r"\b\w*hash_sink\w*|\bpf_native_hash\w*"),)), + "entry_bar_mask_phase": ("engine.hpp", "void declare_opened_lot_entry_bar_mask(", ( + ("@declarations", r"\bstrategy_native_declare_opened_lot_entry_bar_mask_(?!v1\b)\w+"),)), + "timeframe_undetected": ("native_run_spec.hpp", "bool timeframe_undetected", ( + ("@declarations", r"\b\w*undetected\w*"),)), + "inherited_statistics": ("engine.hpp", "double max_contracts_held_all() const", ( + ("@declarations", r"\bstrategy_\w*(?:max_contracts_held|capital_held|open_profit" + r"|gross_profit|gross_loss|net_profit|runup|drawdown)\w*"),)), + "hook_views": ("native_host.hpp", "struct NativeExecutionTermsFacts", ( + ("pf_native_terms_view_v1", r"\b\w*(?:definition|allowance)\w*"), + ("pf_native_precommit_view_v1", r"\b\w*(?:definition|readiness)\w*"))), + "event_payloads": ("native_order.hpp", "enum class CommandSurface", ( + ("pf_native_event_v1", r"\b\w*(?:surface|authority|definition|attempted|requested_delta" + r"|kept_handle|allowance|projection)\w*"),)), + "command_results": ("native_order.hpp", "struct SubmitResult", ( + ("@declarations", r"\bpf_native_(?:submit|replace|cancel|execute)\w*result\w*" + r"|\b\w*event_ordinal\w*"),)), + "state_view": ("native_host.hpp", "struct NativeStateView", ( + ("pf_native_state_v1", r"\b\w*(?:spec|context|recipient|cause)\w*"),)), + "host_minor": ("engine.hpp", "void trace(const std::string& name, double value);", ( + ("@declarations", r"\bstrategy_(?:native_)?(?:trace|declare_\w*hook)\w*"),)), + "library_facilities": ("native_run_spec.hpp", "std::uint64_t native_run_spec_digest(", ( + ("@declarations", r"\b\w*(?:spec_digest|fx_curve_digest|validate_native|preflight_native" + r"|parse_timeframe|parse_session|quantity_on_grid)\w*"),)), + "toolkit": ("native_toolkit.hpp", "inline BracketReceipt submit_bracket", ( + ("@declarations", r"\b\w*(?:bracket|order_book)\w*"),)), +} +_CITATION = re.compile(r'C_V1_EXCLUSIONS\["(\w+)"\]') + _C_ENUM = re.compile(r"typedef\s+enum\s+(\w+)\s*\{(.*?)\}\s*\w+\s*;", re.DOTALL) _ENUMERATOR = re.compile(r"^\s*([A-Za-z_]\w*)\s*(?:=\s*(.+?))?\s*$", re.DOTALL) @@ -173,6 +253,51 @@ def _strip_comments(text: str) -> str: return re.sub(r"//[^\n]*", " ", text) +def _squeeze(text: str) -> str: + return re.sub(r"\s+", " ", text) + + +def c_v1_exclusion_failures(c_api_text: str, public_text: str) -> list[str]: + """Hold the 1.0 C boundary table to the declarations: each row's C++ + capability still exists, its C spelling is still absent, and the page + cites every row once and no row that is not here.""" + failures: list[str] = [] + declarations = _strip_comments(c_api_text) + "\n" + _strip_comments(public_text) + records = {name: body for name, body in re.findall( + r"\btypedef\s+struct\s+(\w+)\s*\{(.*?)\}\s*\w+\s*;", declarations, re.DOTALL)} + page = DOC_C_BOUNDARY.read_text(encoding="utf-8") + cited = _CITATION.findall(page) + for key in sorted(set(cited) - set(C_V1_EXCLUSIONS)): + failures.append(f'C_V1_EXCLUSIONS["{key}"]: the boundary table cites a row this ' + "checker does not have") + for key, (header, cpp_spelling, pins) in C_V1_EXCLUSIONS.items(): + cpp = _squeeze(_strip_comments((INCLUDE / header).read_text(encoding="utf-8"))) + spelling = _squeeze(cpp_spelling) + whole = ((r"(? int: """An enumerator's value: an integer literal (decimal or hex, any u/l suffix, optionally negated), a `1u << n` shift, or an earlier @@ -413,6 +538,7 @@ def main() -> int: enum_failures, twinned, covered = enum_twin_failures((c_api_text, public_text)) failures.extend(enum_failures) + failures.extend(c_v1_exclusion_failures(c_api_text, public_text)) # Every claimed spelling must name something the C headers declare. for name, detail in sorted({**spelled, **seam_spelled}.items()): @@ -431,7 +557,8 @@ def main() -> int: f"{len(seams)} marked BacktestEngine seams, {len(seam_spelled)} with a C spelling, " f"{len(seam_excluded)} excluded with a reason; {twinned + len(C_ONLY)} C enumerations, " f"{twinned} twinned to a kernel enumeration ({covered} enumerators, each with a C " - f"name), {len(C_ONLY)} C-only with a reason") + f"name), {len(C_ONLY)} C-only with a reason; {len(C_V1_EXCLUSIONS)} C++ capabilities " + "outside the 1.0 C surface, each pinned and in the boundary table") return 0 diff --git a/scripts/test_check_native_c_api_surface.py b/scripts/test_check_native_c_api_surface.py index 77845600..6d5a8ae5 100644 --- a/scripts/test_check_native_c_api_surface.py +++ b/scripts/test_check_native_c_api_surface.py @@ -100,6 +100,100 @@ def test_a_row_for_a_member_that_no_longer_exists_fails(self) -> None: self.assertEqual(code, 1) self.assertIn("cancel_gone", err) + # --- the 1.0 C boundary (R5 lane H-DOCGATES) --- + # Each case adds the spelling a 1.1.0 lane would most likely give a + # capability the 1.0 C surface lacks. A pin that caught only the plain + # spelling let the first four through (AUDIT4 cparity mutants M1, M3, M4 + # and M5): the gap would close and the boundary table would still list it. + + def assert_boundary_fails(self, key: str, anchor: str, addition: str) -> None: + self.assertEqual(C_API_TEXT.count(anchor), 1, anchor) + self.c_api.write_text(C_API_TEXT.replace(anchor, anchor + addition, 1), encoding="utf-8") + code, err = self.run_guard() + self.assertEqual(code, 1, err) + self.assertIn(f'C_V1_EXCLUSIONS["{key}"]', err) + self.assertIn("is now declared", err) + + def test_a_suffixed_entry_comment_accessor_closes_the_gap(self) -> None: + self.assert_boundary_fails( + "closed_entry_comment", "PF_API int strategy_native_submit_v1(", + "\n/* x */ const char* strategy_closed_trade_entry_comment_v1(pf_strategy_t s, int i);\n" + "PF_API int strategy_native_submit_v1(") + + def test_a_replace_call_with_options_closes_the_gap(self) -> None: + self.assert_boundary_fails( + "replace_options", "PF_API int strategy_native_submit_v1(", + "\nPF_API int strategy_native_replace_opts_v1(pf_strategy_t s, uint64_t incarnation," + " uint32_t keep_handle);\nPF_API int strategy_native_submit_v1(") + + def test_a_bracket_call_closes_the_gap(self) -> None: + self.assert_boundary_fails( + "toolkit", "PF_API int strategy_native_submit_v1(", + "\nPF_API int strategy_native_submit_bracket_v1(pf_strategy_t s);\n" + "PF_API int strategy_native_submit_v1(") + + def test_a_prefixed_applied_tail_closes_the_gap(self) -> None: + self.assert_boundary_fails( + "applied_event_tail", + " uint64_t opened_lot_incarnation; /**< The lot this fill opened, 0 when none. */", + "\n uint32_t request_origin;\n const char* request_label;") + + def test_an_applied_struct_beside_v1_closes_the_gap(self) -> None: + self.assert_boundary_fails( + "applied_event_tail", "PF_API int strategy_native_submit_v1(", + "\ntypedef struct pf_native_applied_ext_v1 { uint32_t origin; } " + "pf_native_applied_ext_v1;\nPF_API int strategy_native_submit_v1(") + + def test_a_decision_input_interval_closes_the_gap(self) -> None: + self.assert_boundary_fails( + "callback_contexts", " uint8_t closes_session_day; /**< It closes its session day. */", + "\n int64_t input_interval_open_ms;") + + def test_a_cpp_capability_that_goes_retires_its_row(self) -> None: + saved = guard.INCLUDE + self.addCleanup(setattr, guard, "INCLUDE", saved) + copy = Path(self.c_api.parent, "include") + shutil.copytree(saved, copy) + order = copy / "native_order.hpp" + text = order.read_text(encoding="utf-8") + self.assertEqual(text.count("struct ReplaceOptions"), 1) + order.write_text(text.replace("struct ReplaceOptions", "struct ReplaceOptionsRenamed", 1), + encoding="utf-8") + guard.INCLUDE = copy + code, err = self.run_guard() + self.assertEqual(code, 1, err) + self.assertIn('C_V1_EXCLUSIONS["replace_options"]: the C++ declaration', err) + + def page_with(self, old: str, new: str) -> None: + saved = guard.DOC_C_BOUNDARY + self.addCleanup(setattr, guard, "DOC_C_BOUNDARY", saved) + page = Path(self.c_api.parent, "native-engine.md") + text = saved.read_text(encoding="utf-8") + self.assertEqual(text.count(old), 1, old) + page.write_text(text.replace(old, new, 1), encoding="utf-8") + guard.DOC_C_BOUNDARY = page + + def test_a_boundary_row_cannot_disappear(self) -> None: + self.page_with('`C_V1_EXCLUSIONS["replace_options"]`', "(gone)") + code, err = self.run_guard() + self.assertEqual(code, 1, err) + self.assertIn('C_V1_EXCLUSIONS["replace_options"]: the boundary table', err) + self.assertIn("cites it 0 times", err) + + def test_a_boundary_row_cannot_be_doubled(self) -> None: + self.page_with('`C_V1_EXCLUSIONS["toolkit"]` |', + '`C_V1_EXCLUSIONS["toolkit"]`, `C_V1_EXCLUSIONS["toolkit"]` |') + code, err = self.run_guard() + self.assertEqual(code, 1, err) + self.assertIn("cites it 2 times", err) + + def test_the_page_cannot_cite_a_row_the_checker_lacks(self) -> None: + self.page_with('`C_V1_EXCLUSIONS["toolkit"]` |', + '`C_V1_EXCLUSIONS["toolkit"]`, `C_V1_EXCLUSIONS["made_up"]` |') + code, err = self.run_guard() + self.assertEqual(code, 1, err) + self.assertIn('C_V1_EXCLUSIONS["made_up"]: the boundary table cites a row', err) + # --- the BASE-CLASS SEAMS census (R5 lane E15) --- def test_a_marked_seam_without_a_row_fails(self) -> None: From 15dcae36aa9aa77b9049396b84fdecc62b637981 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 05:05:34 +0800 Subject: [PATCH 18/83] Gates: a sanitizer build's quoted type name is judged as the name, and Eigen's assert text a debug archive carries has its own row (R5 lane H-DOCGATES, X12) The sanitizers profile's run of the X12 vocabulary (lab remote job rj-20260925t190344-64918a, NTU2, x86-64 GCC) failed test_kernel_residuals on two strings a release archive does not carry: - `'struct SymInfo'`: UBSan's type descriptor quotes a type's name. The line IS the name `SymInfo`, which is ruled, but X12 judged it as a text naming a ruled identifier. check_kernel_residuals.py now reads a quoted type name (GCC's `'struct X'`, clang's `'pineforge::X'`) as its names. - `matrix.cols() == matrix.rows()`: the eigen_assert text of Eigen's eigenvalue solvers, which src/matrix.cpp instantiates, compiled in only when NDEBUG is off. X12's dotted-namespace vocabulary reads `matrix.`. It gets an ADR-0001 row by its exact words ("not residue": Eigen's parameter, not Pine's namespace), and a row whose second column says "absent under NDEBUG" is no longer called stale when no surface carries it, so the same row holds in release, debug and sanitizers. ADR_RULED_TEXTS_MIN 45 -> 46. Five self-tests: both quoted spellings pass as the ruled name and an unruled quoted type still fails, a quoted Pine text is still a text, the debug-only text is not stale when absent and covered when present, only a marked row may be absent, and an unruled assert-shaped text still fails. Dropping the type-name rule fails the first; dropping the debug-only rule fails the release table. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 68b974f2d2921fbe79c72d1b9642ad393cc67d66) INT26 pick note: scripts/ci_verify.py conflicted beside INT25's PR-only floors and ctest bound; ADR_RULED_TEXTS_MIN takes this commit's 46 and every INT25 line stays. The residual gate on the merged tree: 174 ruled identifiers, 46 ruled texts, 0 findings. --- docs/adr/0001-kernel-adapter-boundary.md | 7 ++- scripts/check_kernel_residuals.py | 24 +++++++++- scripts/ci_verify.py | 6 ++- scripts/test_check_kernel_residuals.py | 61 ++++++++++++++++++++++-- 4 files changed, 90 insertions(+), 8 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 9adc3ba6..c56bcda3 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -456,7 +456,11 @@ text that contains it: until lane H-DOCGATES the bare words `Pine` and `syminfo` ruled as names, passed any text such as "match Pine semantics exactly" or "syminfo.tickerid must be set". A row whose second column says "no archive symbol" rules its names and texts for the installed headers alone, and the checker fails when -the archive or a kernel source literal carries one. A name that is not in these tables fails the +the archive or a kernel source literal carries one. A row whose second column says "absent under NDEBUG" +rules an `assert` text that only the debug and sanitizers profiles compile in; a release +archive carries none, so the checker does not call that row stale. A sanitizer build's type +descriptors quote a type's name (`'struct SymInfo'`), and such a line is judged as that name, +not as a text. A name that is not in these tables fails the `kernel` profile. The key line counts only the rulings the vocabulary reads — the mechanism tables below are parsed too, but their first columns (`priority`, `keep_binding`, `NativeRunSpec::event_retention`, ...) match no pattern — and the @@ -496,6 +500,7 @@ ruled by its words. Each is listed here with its ruling. | `pine_time`, `pine_time_close`, `pine_time_tradingday`, `pine_hour`, `pine_minute`, `pine_second`, `pine_dayofmonth`, `pine_dayofweek`, `pine_month`, `pine_year`, `pine_weekofyear`, `pine_session_ismarket`, `pine_session_ispremarket`, `pine_session_ispostmarket`, `PF_PINE_TIME_HAS_SYMINFO_TZ`, `PF_PINE_TIME_HAS_SESSION_DAY`, `pine_str_format`, `pine_str_format_time`, `pine_str_match`, `pine_str_split`, `pine_str_tostring`, `pine_random`, `PineMatrix`, `PineGenericMatrix` | installed headers `include/pineforge/session_time.hpp`, `include/pineforge/str_utils.hpp`, `include/pineforge/math.hpp`, `include/pineforge/matrix.hpp`, `include/pineforge/generic_matrix.hpp`; inline forwards and aliases, so no archive symbol | **retained as deprecated spellings for generated code** (design §2.ii h, lanes L11 and N14): each is an exact inline forward to, or alias of, a neutral primary (`timeframe_time`, `local_hour`, `session_ismarket`, `str_format`, `deterministic_random`, `NumericMatrix`, `GenericMatrix`, ...), and the two macros are the feature tests generated code compiles against. The codegen transpiler emits them (`pine_hour` alone in 66 corpus strategies), so dropping one is a codegen change. | | `PineMap`, `PineMapBare`, `PineMapKeyEqual`, `PineMapKeyHash`, `is_pine_map_key_v`, `is_pine_map_snapshot_value_v`, `PineMap key must be a Pine fundamental or enum type`, `PineMap ordered storage must support no-throw swap`, `PineMap index must support no-throw swap`, `PineMap::Snapshot supports primitive Pine values only;`, `pine_color`, `pine_drawing_error`, `pine_log_info`, `pine_log_warning`, `pine_log_error`, `pine_runtime_error` | installed headers `include/pineforge/map.hpp` (the type, its detail traits and its four `static_assert` texts, each ruled by its exact words: until R5 lane H-DOCGATES a ruling of the bare word `Pine` covered them, and would have covered any new text naming the language), `include/pineforge/color.hpp`, `include/pineforge/drawing.hpp`, `include/pineforge/log.hpp`; header-only, outside the kernel's compile closure, so no archive symbol | **retained**: the header-only runtime of generated strategies, primary spellings with no neutral alias (the codegen transpiler emits `PineMap`, `pine_color`, `pine_log_*` and `pine_runtime_error`). No kernel translation unit includes these headers; the kernel profile installs them because its install rule ships all of `include/pineforge` but `source/` and `compat/`. A neutral spelling, or a move under `source/`, is a codegen change and an open decision (AUDIT3-opus, GAP-15). | | `line.get_price requires xloc.bar_index`, `matrix.reshape: dimension overflow`, `matrix.elements_count: total exceeds int range`, `matrix.new: negative dimensions`, `matrix.new: no-init overload requires default-constructible T`, `matrix.get: row index out of range`, `matrix.get: column index out of range`, `matrix.set: row index out of range`, `matrix.set: column index out of range`, `matrix.row: row index out of range`, `matrix.col: column index out of range`, `matrix.row_ref: row index out of range`, `matrix.add_row: row index out of range`, `matrix.add_row: values size must equal columns()`, `matrix.add_col on empty matrix: use add_row first`, `matrix.add_col: column index out of range`, `matrix.add_col: values size must equal rows()`, `matrix.remove_row: row index out of range`, `matrix.remove_col: column index out of range`, `matrix.swap_rows: row index out of range`, `matrix.swap_columns: column index out of range`, `matrix.submatrix: row index out of range`, `matrix.submatrix: column index out of range`, `matrix.submatrix: from_row must be <= to_row`, `matrix.submatrix: from_col must be <= to_col`, `matrix.transpose: requires default-constructible element type`, `matrix.concat: row count mismatch`, `matrix.concat: column count mismatch`, `matrix.reshape: requires default-constructible element type`, `matrix.sort: requires int, bool, or std::string element type`, `matrix.sort: not supported on bool element type`, `map.size: result exceeds int range` | the argument-check texts of the installed generated-code runtime: `include/pineforge/generic_matrix.hpp` (`GenericMatrix` and `NumericMatrix`, the Pine `matrix.*` built-ins), `include/pineforge/map.hpp` (`PineMap::size`) and `include/pineforge/drawing.hpp` (the line-price lookup); header-only, outside the kernel's compile closure, so no archive symbol; read since R5 lane H-DOCGATES, whose vocabulary reads Pine's dotted namespaces | **retained**, on the terms of the row above: each text names the Pine built-in whose argument check failed (`matrix.get`, `map.size`, `line.get_price`), the name a script's author wrote, and is thrown only inside a generated strategy. No kernel translation unit includes these headers, and the gate holds the texts out of the archive and the kernel's own literals. Rewording one is a runtime-diagnostic change for generated code, part of the GAP-15 decision above. | +| `matrix.cols() == matrix.rows()` | the `eigen_assert` text of Eigen's eigenvalue solvers (`Eigen/src/Eigenvalues/RealSchur.h`, `ComplexSchur.h`, `SelfAdjointEigenSolver.h`), which `src/matrix.cpp` instantiates. An `assert` compiles in only when `NDEBUG` is off -- the debug and sanitizers profiles -- so the text is absent under NDEBUG and no release archive carries it | **not residue**: `matrix` is Eigen's parameter, not Pine's `matrix.` namespace, and the kernel does not write the text. It is ruled by its exact words so that the dotted-namespace vocabulary (R5 lane H-DOCGATES) keeps reading every other text of a debug archive; the first sanitizers run of that vocabulary found it (lab remote job rj-20260925t190344-64918a). | Names the vocabulary does not match but the audits named, with their rulings: diff --git a/scripts/check_kernel_residuals.py b/scripts/check_kernel_residuals.py index 8a870b02..2aef046f 100644 --- a/scripts/check_kernel_residuals.py +++ b/scripts/check_kernel_residuals.py @@ -339,11 +339,17 @@ class Ruled: # installed headers carry them; the archive and the kernel's own literals # must not. header_only: frozenset[str] = frozenset() + # The texts whose row says a build with NDEBUG defined never carries them + # ("absent under NDEBUG" in its second column): an `assert` text the + # debug and sanitizers profiles compile in and release does not. Such a + # ruling is not stale when no surface carries it. + debug_only: frozenset[str] = frozenset() # The words a row's second column uses for a name the kernel profile installs # but never compiles into the archive: the gate holds the row to that claim. HEADER_ONLY_MARK = "no archive symbol" +DEBUG_ONLY_MARK = "absent under NDEBUG" def ruled_entries(adr_text: str) -> Ruled: @@ -365,12 +371,14 @@ def ruled_entries(adr_text: str) -> Ruled: identifiers: set[str] = set() phrases: set[str] = set() header_only: set[str] = set() + debug_only: set[str] = set() for line in section.splitlines(): if not line.startswith("|"): continue cells = line.split("|") first = cells[1] scoped = len(cells) > 2 and HEADER_ONLY_MARK in cells[2] + debug = len(cells) > 2 and DEBUG_ONLY_MARK in cells[2] for token in BACKTICKED.findall(first): token = token.strip() if "/" in token: @@ -383,9 +391,12 @@ def ruled_entries(adr_text: str) -> Ruled: phrases.add(token) if scoped: header_only.add(token) + if debug: + debug_only.add(token) if not identifiers: raise InfrastructureError("ADR residual section lists no ruled identifier") - return Ruled(frozenset(identifiers), frozenset(phrases), frozenset(header_only)) + return Ruled(frozenset(identifiers), frozenset(phrases), frozenset(header_only), + frozenset(debug_only)) def scan(lines: list[str], source: str, *, texts: bool = True, @@ -440,6 +451,13 @@ def symbol_name(token: str, source: str) -> str: r"(?:\.(?:cold|part|isra|constprop|lto_priv|localalias|llvm|clone|specialized)" r"(?:\.\d+)*)+$") +# A type's name as UndefinedBehaviorSanitizer stores it in a sanitizer build's +# type descriptors -- quoted, with GCC's elaborated keyword (`'struct SymInfo'`) +# or clang's qualified name (`'pineforge::SymInfo'`). The line IS the type's +# name, so its identifiers are judged as names, not as a text. +SANITIZER_TYPE_NAME = re.compile( + r"^'(?:(?:struct|class|union|enum)\s+)?[A-Za-z_][\w:<>,\s*&\[\]]*'$") + # The surfaces whose lines are texts a program prints or compiles in: an archive # string, a header literal, a kernel-source literal. (A symbol line and a code # line are names.) @@ -459,6 +477,8 @@ def is_text(hit: Hit) -> bool: text = hit.text.strip() if hit.source == "strings": text = CLONE_SUFFIX.sub("", text) + if SANITIZER_TYPE_NAME.match(text): + return False return ARCHIVE_IDENTIFIER.fullmatch(text) is None @@ -552,7 +572,7 @@ def report(kind: str, token: str, detail: str) -> None: "ruled in ADR-0001 but no longer in the archive, the installed headers or " "a kernel source literal; drop its row")) for phrase in sorted(ruled.phrases): - if phrase in covered: + if phrase in covered or phrase in ruled.debug_only: continue if vocabulary_text(phrase): findings.append(Finding( diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 274c39d7..ed074326 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -390,9 +390,11 @@ # map.hpp's 4 static_assert texts, the runtime's 32 dotted argument checks). # A row the vocabulary reads cannot leave silently -- its name turns unruled # -- unless its name left in the same change; the floor makes that drop an -# edit of this file. A lane that adds rulings raises the floor with them. +# edit of this file. A lane that adds rulings raises the floor with them: +# 46 texts once the lane also ruled Eigen's `matrix.cols() == +# matrix.rows()` assert text, which a debug or sanitizers archive carries. ADR_RULED_IDENTIFIERS_MIN = 174 -ADR_RULED_TEXTS_MIN = 45 +ADR_RULED_TEXTS_MIN = 46 # PR-only registration floors: the complete CTest populations of the three # excluded profiles at INT25, counted with ctest -N on the integrated tree -- # 653/653/662 at 91d65ad6 (INT24) plus wave G's six rows (C-SURFACE-1 +1, diff --git a/scripts/test_check_kernel_residuals.py b/scripts/test_check_kernel_residuals.py index a2f36953..8048502f 100644 --- a/scripts/test_check_kernel_residuals.py +++ b/scripts/test_check_kernel_residuals.py @@ -166,7 +166,7 @@ def table_archive() -> tuple[list[str], list[str]]: it but the header-only rows (R5 lane H-DOCGATES), which table_headers() ships where the kernel profile ships them.""" strings = (sorted(RULED.identifiers - RULED.header_only) - + sorted(RULED.phrases - RULED.header_only) + NOISE_STRINGS) + + sorted(RULED.phrases - RULED.header_only - RULED.debug_only) + NOISE_STRINGS) return strings, list(NOISE_NM) @@ -186,6 +186,58 @@ def evaluate_table(strings: list[str], nm: list[str], **surfaces): return guard.evaluate(strings, nm, RULED, **surfaces) +class SanitizerAndDebugBuildTests(unittest.TestCase): + """R5 lane H-DOCGATES: the sanitizers profile's first run of the dotted- + namespace vocabulary (lab remote job rj-20260925t190344-64918a) found two + strings a release archive does not carry: UBSan's quoted type name + `'struct SymInfo'` and Eigen's assert text `matrix.cols() == + matrix.rows()`.""" + + def test_a_sanitizer_type_name_is_judged_as_the_name(self) -> None: + for line in ("'struct SymInfo'", "'pineforge::SymInfo'"): + with self.subTest(line=line): + strings, nm = table_archive() + findings, _ = evaluate_table(strings + [line], nm) + self.assertEqual(findings, [], [str(f) for f in findings]) + strings, nm = table_archive() + findings, _ = evaluate_table(strings + ["'struct PineBogusType'"], nm) + self.assertEqual([(f.kind, f.token) for f in findings], [("unruled", "PineBogusType")]) + + def test_a_quoted_text_is_still_a_text(self) -> None: + strings, nm = table_archive() + findings, _ = evaluate_table(strings + ["'match Pine semantics exactly'"], nm) + self.assertGreaterEqual(len(findings), 1) + self.assertEqual({f.kind for f in findings}, {"unruled"}) + + def test_the_debug_only_assert_text(self) -> None: + text = "matrix.cols() == matrix.rows()" + self.assertIn(text, RULED.debug_only) + strings, nm = table_archive() + self.assertNotIn(text, strings) # a release archive: not stale + findings, _ = evaluate_table(strings, nm) + self.assertEqual(findings, [], [str(f) for f in findings]) + findings, summary = evaluate_table(strings + [text], nm) # a debug archive: covered + self.assertEqual(findings, [], [str(f) for f in findings]) + self.assertEqual(summary["phraseHits"], 4) + + def test_only_a_marked_row_may_be_absent(self) -> None: + adr = guard.ADR.read_text(encoding="utf-8") + row = "so the text is " + guard.DEBUG_ONLY_MARK + self.assertEqual(adr.count(row), 1) + adr = adr.replace(row, "so the text is absent") + ruled = guard.ruled_entries(adr) + self.assertNotIn("matrix.cols() == matrix.rows()", ruled.debug_only) + strings, nm = table_archive() + findings, _ = guard.evaluate(strings, nm, ruled, header_lines=table_headers()) + self.assertEqual([(f.kind, f.token) for f in findings], + [("stale", "matrix.cols() == matrix.rows()")]) + + def test_an_unruled_assert_text_still_fails(self) -> None: + strings, nm = table_archive() + findings, _ = evaluate_table(strings + ["map.size() == map.capacity()"], nm) + self.assertEqual([f.kind for f in findings], ["unruled"]) + + class RuledTableTests(unittest.TestCase): def test_the_adr_table_rules_every_family_the_audit_named(self) -> None: names = RULED.identifiers @@ -264,9 +316,12 @@ def test_the_ruled_texts_pass_by_phrase(self) -> None: # (expectation corrected: 3 -> every ruled text the vocabulary reads, # because R5 lane H-DOCGATES rules each text by its exact words: the # three feed texts, the deprecated aliases' texts, map.hpp's - # static_assert texts and the runtime's dotted argument checks.) + # static_assert texts and the runtime's dotted argument checks; the + # table archive models a release archive, which carries no assert + # text, so the debug-only row is not among them.) self.assertEqual(summary["ruledPhrasesCovered"], - sum(1 for p in RULED.phrases if guard.vocabulary_text(p))) + sum(1 for p in RULED.phrases - RULED.debug_only + if guard.vocabulary_text(p))) def test_a_stale_ruling_fails(self) -> None: strings, nm = table_archive() From f767380e7537b28f216a3995bb6c75088b0a9030 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:04:26 +0800 Subject: [PATCH 19/83] Kernel+C: the four deprecated public spellings are removed for 1.0 (lane REL10, item 1) ADR-0001 "Deprecated public spellings" scheduled pf_equity_stats_t::sharpe_tv / sortino_tv for removal at the next PF_ABI_VERSION and exit_legs::Domain::Coof / MagnifierCoof at the namespace after lifecycle_v1. They are removed for 1.0 without either epoch: an alias shares its storage or its value with the primary spelling, so no offset, size, value, hash fold or mangled name moves. PF_ABI_VERSION stays 4 and lifecycle_v1 stays lifecycle_v1; the PF_DEPRECATED and PF_ANONYMOUS_UNION helpers leave pineforge.h with them. ADR-0001 rule 2: the kept spellings are the generic ones, named for the resampling period and the fill-recalculation pass, not for a Pine or TradingView setting. - test_removed_public_spellings (F6's deprecation row, renamed; still one row): the 1.0 spellings compile clean under -Werror in strict C99 and in C++17, and each removed spelling alone fails with the unknown-member diagnostic that names it. - Tests and the Python mirrors name the 1.0 spellings; the twin-parity guard matches the renamed CHECKs by literal signature, counts unchanged. - The JSON report keys stay sharpe_tv / sortino_tv: docker/run_json.py writes the two C fields under them (EQUITY_REPORT_KEYS), pinned by scripts/test_report_schema_keys.py, a new ci_preflight stage. - Every page that named an old spelling says what is true now; the ADR section keeps its heading and records the removal. Anchor digits follow in the lane's re-anchor commit. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 38e8ad1e0f6a022e03d91e82f31d43ea66bef652) INT26 pick note: dropping the four spellings drops their ADR residual row, which H-DOCGATES (33aaf0b9) had widened to rule each alias's six texts by their exact words; the row leaves whole, as this commit removes it, and the residual gate's ruled-count floors in scripts/ci_verify.py fall with it (ADR_RULED_IDENTIFIERS_MIN 174 -> 170, ADR_RULED_TEXTS_MIN 46 -> 40; the gate on the merged kernel archive counts 170 / 40, 0 findings). ci_preflight.py keeps H-MEASURE's subset-cover stage and adds this commit's report-schema-key stage. pine-to-native.md takes this commit's text (line 141 on its tree), digits carried. The sentences the removal retires, by the commits that wrote them: lane P2c's deprecation ruling and its page notes (329e021), lane F4's strict-C99 anonymous-union note (0a46b45), the int12 guard pass's migration sentences (9b50973) and INT16's wave-F reconciliation of the same pages (ca0aac5). --- benchmarks/throughput/grid_search_repro.py | 8 +- docker/run_json.py | 22 ++-- docs/adr/0001-kernel-adapter-boundary.md | 87 +++++++-------- docs/pages/abi-stability.md | 34 +++--- docs/pages/examples-rust.md | 2 +- docs/pages/exit-leg-lifecycle.md | 8 +- docs/pages/ffi-python.md | 8 +- docs/pages/metrics.md | 11 +- docs/pages/native-engine.md | 24 ++--- docs/pages/pine-to-native.md | 2 +- docs/pages/report-schema.md | 15 ++- include/pineforge/exit_leg_lifecycle.hpp | 13 +-- include/pineforge/pineforge.h | 52 ++------- scripts/check_doc_anchors.py | 2 +- scripts/ci_preflight.py | 3 + scripts/ci_verify.py | 10 +- scripts/crossvalidate_metrics.py | 45 ++++---- scripts/run_strategy.py | 8 +- scripts/test_report_schema_keys.py | 93 ++++++++++++++++ src/c_abi.cpp | 29 ++--- tests/CMakeLists.txt | 41 +++---- tests/deprecated_spellings.cmake | 119 --------------------- tests/deprecated_spellings_probe.c | 12 --- tests/deprecated_spellings_probe.cpp | 18 ---- tests/removed_spellings.cmake | 101 +++++++++++++++++ tests/removed_spellings_probe.c | 17 +++ tests/removed_spellings_probe.cpp | 23 ++++ tests/test_c_abi.c | 39 +++---- tests/test_exit_lifecycle_availability.cpp | 4 +- tests/test_exit_lifecycle_clock.cpp | 16 +-- tests/test_exit_lifecycle_clock_l4c.cpp | 16 +-- tests/test_metrics.cpp | 28 ++--- tests/test_metrics_l4d.cpp | 28 ++--- tests/test_native_c_api_c99.c | 36 +++---- tutorial/run.py | 7 +- 35 files changed, 490 insertions(+), 491 deletions(-) create mode 100644 scripts/test_report_schema_keys.py delete mode 100644 tests/deprecated_spellings.cmake delete mode 100644 tests/deprecated_spellings_probe.c delete mode 100644 tests/deprecated_spellings_probe.cpp create mode 100644 tests/removed_spellings.cmake create mode 100644 tests/removed_spellings_probe.c create mode 100644 tests/removed_spellings_probe.cpp diff --git a/benchmarks/throughput/grid_search_repro.py b/benchmarks/throughput/grid_search_repro.py index cdd68e0d..5603ffef 100644 --- a/benchmarks/throughput/grid_search_repro.py +++ b/benchmarks/throughput/grid_search_repro.py @@ -59,11 +59,9 @@ class pf_equity_stats_t(ctypes.Structure): ("max_equity_drawdown", ctypes.c_double), ("max_equity_drawdown_pct", ctypes.c_double), ("max_equity_runup", ctypes.c_double), ("max_equity_runup_pct", ctypes.c_double), ("buy_hold_return", ctypes.c_double), ("buy_hold_return_pct", ctypes.c_double), - # Historical member names of the C fields sharpe_monthly / - # sortino_monthly. ctypes matches by offset, never by the C field's - # name, and these strings are the serialized report keys, so the mirror - # keeps them (ADR-0001, "Deprecated public spellings"). - ("sharpe_tv", ctypes.c_double), ("sortino_tv", ctypes.c_double), + # The C field names. The JSON report keys of these two stay + # sharpe_tv / sortino_tv (ADR-0001, "Deprecated public spellings"). + ("sharpe_monthly", ctypes.c_double), ("sortino_monthly", ctypes.c_double), ("sharpe_bar", ctypes.c_double), ("sortino_bar", ctypes.c_double), ("cagr", ctypes.c_double), ("calmar", ctypes.c_double), ("recovery_factor", ctypes.c_double), ("time_in_market_pct", ctypes.c_double), diff --git a/docker/run_json.py b/docker/run_json.py index d63d728f..dfb0c0d5 100755 --- a/docker/run_json.py +++ b/docker/run_json.py @@ -611,11 +611,10 @@ class EquityStatsC(ctypes.Structure): ("max_equity_drawdown", ctypes.c_double), ("max_equity_drawdown_pct", ctypes.c_double), ("max_equity_runup", ctypes.c_double), ("max_equity_runup_pct", ctypes.c_double), ("buy_hold_return", ctypes.c_double), ("buy_hold_return_pct", ctypes.c_double), - # Historical member names of the C fields sharpe_monthly / - # sortino_monthly. ctypes matches by offset, never by the C field's - # name, and these strings are the serialized report keys, so the mirror - # keeps them (ADR-0001, "Deprecated public spellings"). - ("sharpe_tv", ctypes.c_double), ("sortino_tv", ctypes.c_double), + # The C field names. _stats_dict writes these two under their JSON + # report keys sharpe_tv / sortino_tv (ADR-0001, "Deprecated public + # spellings"), so the report schema does not change. + ("sharpe_monthly", ctypes.c_double), ("sortino_monthly", ctypes.c_double), ("sharpe_bar", ctypes.c_double), ("sortino_bar", ctypes.c_double), ("cagr", ctypes.c_double), ("calmar", ctypes.c_double), ("recovery_factor", ctypes.c_double), ("time_in_market_pct", ctypes.c_double), @@ -843,14 +842,23 @@ def _num(x): return f if math.isfinite(f) else None +# The JSON report keys of pf_equity_stats_t's two monthly ratios. They are +# report-schema names: the C fields are sharpe_monthly / sortino_monthly (their +# pre-1.0 spellings were removed for 1.0), the keys did not change (ADR-0001, +# "Deprecated public spellings"). +EQUITY_REPORT_KEYS = {"sharpe_monthly": "sharpe_tv", "sortino_monthly": "sortino_tv"} + + def _stats_dict(s) -> dict: """Serialize a pf_trade_stats_t / pf_equity_stats_t ctypes struct to a dict, keying off each field's ctype: integer counters stay ints, every double is - sanitized through _num. Driven by _fields_ so it tracks the struct verbatim.""" + sanitized through _num. Driven by _fields_ so it tracks the struct verbatim; + an equity struct's two monthly ratios take their EQUITY_REPORT_KEYS.""" + keys = EQUITY_REPORT_KEYS if isinstance(s, EquityStatsC) else {} out = {} for name, ctype in s._fields_: v = getattr(s, name) - out[name] = _num(v) if ctype is ctypes.c_double else int(v) + out[keys.get(name, name)] = _num(v) if ctype is ctypes.c_double else int(v) return out diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index c56bcda3..2aae4fb2 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -496,7 +496,6 @@ ruled by its words. Each is listed here with its ruling. | `strategy_position_size`, `strategy_position_avg_price` | frozen `PF_API` exports (`src/c_abi.cpp`, `include/pineforge/pineforge.h`), pinned by `scripts/check_c_abi_runtime.py`; read since lane F6, whose vocabulary names the underscore spelling of every Pine `strategy.*` member | **retained**: frozen C ABI export names. The value is the kernel's own -- signed position units and their volume-weighted entry price -- and the spelling is the Pine built-in a porting consumer looks for, the vocabulary row of Section B ("the C ABI's documented vocabulary"). Renaming either is a C ABI break. | | `set_syminfo_mintick`, `set_syminfo_pointvalue`, `set_syminfo_session`, `set_syminfo_string`, `set_syminfo_timezone`, `set_syminfo_type`, `get_syminfo_metadata`, `syminfo`, `syminfo_`, `syminfo_mintick_`, `syminfo_tz`, `strategy_set_syminfo_metadata`, `strategy_set_syminfo_mintick`, `strategy_set_syminfo_pointvalue`, `strategy_set_syminfo_session`, `strategy_set_syminfo_string`, `strategy_set_syminfo_timezone`, `strategy_set_syminfo_type` | `BacktestEngine`'s symbol-information ingress and members (`include/pineforge/engine.hpp`), the `syminfo` begin argument (`NativeBeginArgs`, `include/pineforge/native_host.hpp`; `include/pineforge/execution_consumer.hpp`), the `syminfo_tz` parameter of the calendar functions (`include/pineforge/session_time.hpp`) and the frozen C setters (`src/c_abi.cpp`, `include/pineforge/pineforge.h`); read since lane F6, whose vocabulary names `syminfo` | **retained**, the whole family on the terms of the `SymInfo` / `set_syminfo_metadata` row below: `syminfo` is the frozen ABI's word for instrument metadata, the kernel reads the values it carries (tick, point value, session, timezone, instrument type) and never a Pine name. The members are codegen ABI as well -- generated strategies read `syminfo_` (e.g. `syminfo_.timezone`), `syminfo_mintick_` and `get_syminfo_metadata` -- so a rename is a C ABI break and a codegen change. | | `barstate_islast_` | a protected `BacktestEngine` member (`include/pineforge/engine.hpp`); read since lane F6, whose vocabulary names `barstate` | **retained as codegen ABI**: the Pine language flag generated strategies read directly (`barstate.islast`; protected members of `BacktestEngine` are the codegen contract). Only the source host writes it (`source::PineStrategyHost`); no kernel translation unit reads or writes it. The three session flags this row also ruled when lane F6 wrote it -- `session_ismarket_`, `session_isfirstbar_`, `session_islastbar_` -- left the kernel class in R5 lane F5, inside the then-unshipped `engine_script_run_v18` layout (since advanced to `engine_script_run_v19`): they are `PineStrategyHost` members now, which generated code reaches unqualified as before, selected from the kernel's session-day facts (E26-7 below). | -| `sharpe_tv`, `sortino_tv`, `Coof`, `MagnifierCoof`, `sharpe_tv is the historical spelling of sharpe_monthly;`, `sortino_tv is the historical spelling of sortino_monthly;`, `Coof is the historical spelling of FillRecalc; removed at lifecycle_v2`, `MagnifierCoof is the historical spelling of`, `the deprecated sharpe_tv spelling must name the sharpe_monthly storage`, `the deprecated sortino_tv spelling must name the sortino_monthly storage` | `pf_equity_stats_t` (`include/pineforge/pineforge.h`) and `exit_legs::Domain` (`include/pineforge/exit_leg_lifecycle.hpp`), their `PF_DEPRECATED` / `[[deprecated]]` texts, and the two `static_assert` texts of `src/c_abi.cpp` that pin the aliases to their storage; read since lane F6, whose gate reads the installed headers (each text is ruled by its exact words since R5 lane H-DOCGATES, where the alias names ruled them before) | **deprecated aliases**, ruled in "Deprecated public spellings" below: value-identical historical spellings of `sharpe_monthly` / `sortino_monthly` / `FillRecalc` / `MagnifierFillRecalc`, compiler-deprecated since lane F6, removed at `PF_ABI_VERSION` 5 / `lifecycle_v2`. | | `pine_time`, `pine_time_close`, `pine_time_tradingday`, `pine_hour`, `pine_minute`, `pine_second`, `pine_dayofmonth`, `pine_dayofweek`, `pine_month`, `pine_year`, `pine_weekofyear`, `pine_session_ismarket`, `pine_session_ispremarket`, `pine_session_ispostmarket`, `PF_PINE_TIME_HAS_SYMINFO_TZ`, `PF_PINE_TIME_HAS_SESSION_DAY`, `pine_str_format`, `pine_str_format_time`, `pine_str_match`, `pine_str_split`, `pine_str_tostring`, `pine_random`, `PineMatrix`, `PineGenericMatrix` | installed headers `include/pineforge/session_time.hpp`, `include/pineforge/str_utils.hpp`, `include/pineforge/math.hpp`, `include/pineforge/matrix.hpp`, `include/pineforge/generic_matrix.hpp`; inline forwards and aliases, so no archive symbol | **retained as deprecated spellings for generated code** (design §2.ii h, lanes L11 and N14): each is an exact inline forward to, or alias of, a neutral primary (`timeframe_time`, `local_hour`, `session_ismarket`, `str_format`, `deterministic_random`, `NumericMatrix`, `GenericMatrix`, ...), and the two macros are the feature tests generated code compiles against. The codegen transpiler emits them (`pine_hour` alone in 66 corpus strategies), so dropping one is a codegen change. | | `PineMap`, `PineMapBare`, `PineMapKeyEqual`, `PineMapKeyHash`, `is_pine_map_key_v`, `is_pine_map_snapshot_value_v`, `PineMap key must be a Pine fundamental or enum type`, `PineMap ordered storage must support no-throw swap`, `PineMap index must support no-throw swap`, `PineMap::Snapshot supports primitive Pine values only;`, `pine_color`, `pine_drawing_error`, `pine_log_info`, `pine_log_warning`, `pine_log_error`, `pine_runtime_error` | installed headers `include/pineforge/map.hpp` (the type, its detail traits and its four `static_assert` texts, each ruled by its exact words: until R5 lane H-DOCGATES a ruling of the bare word `Pine` covered them, and would have covered any new text naming the language), `include/pineforge/color.hpp`, `include/pineforge/drawing.hpp`, `include/pineforge/log.hpp`; header-only, outside the kernel's compile closure, so no archive symbol | **retained**: the header-only runtime of generated strategies, primary spellings with no neutral alias (the codegen transpiler emits `PineMap`, `pine_color`, `pine_log_*` and `pine_runtime_error`). No kernel translation unit includes these headers; the kernel profile installs them because its install rule ships all of `include/pineforge` but `source/` and `compat/`. A neutral spelling, or a move under `source/`, is a codegen change and an open decision (AUDIT3-opus, GAP-15). | | `line.get_price requires xloc.bar_index`, `matrix.reshape: dimension overflow`, `matrix.elements_count: total exceeds int range`, `matrix.new: negative dimensions`, `matrix.new: no-init overload requires default-constructible T`, `matrix.get: row index out of range`, `matrix.get: column index out of range`, `matrix.set: row index out of range`, `matrix.set: column index out of range`, `matrix.row: row index out of range`, `matrix.col: column index out of range`, `matrix.row_ref: row index out of range`, `matrix.add_row: row index out of range`, `matrix.add_row: values size must equal columns()`, `matrix.add_col on empty matrix: use add_row first`, `matrix.add_col: column index out of range`, `matrix.add_col: values size must equal rows()`, `matrix.remove_row: row index out of range`, `matrix.remove_col: column index out of range`, `matrix.swap_rows: row index out of range`, `matrix.swap_columns: column index out of range`, `matrix.submatrix: row index out of range`, `matrix.submatrix: column index out of range`, `matrix.submatrix: from_row must be <= to_row`, `matrix.submatrix: from_col must be <= to_col`, `matrix.transpose: requires default-constructible element type`, `matrix.concat: row count mismatch`, `matrix.concat: column count mismatch`, `matrix.reshape: requires default-constructible element type`, `matrix.sort: requires int, bool, or std::string element type`, `matrix.sort: not supported on bool element type`, `map.size: result exceeds int range` | the argument-check texts of the installed generated-code runtime: `include/pineforge/generic_matrix.hpp` (`GenericMatrix` and `NumericMatrix`, the Pine `matrix.*` built-ins), `include/pineforge/map.hpp` (`PineMap::size`) and `include/pineforge/drawing.hpp` (the line-price lookup); header-only, outside the kernel's compile closure, so no archive symbol; read since R5 lane H-DOCGATES, whose vocabulary reads Pine's dotted namespaces | **retained**, on the terms of the row above: each text names the Pine built-in whose argument check failed (`matrix.get`, `map.size`, `line.get_price`), the name a script's author wrote, and is thrown only inside a generated strategy. No kernel translation unit includes these headers, and the gate holds the texts out of the archive and the kernel's own literals. Rewording one is a runtime-diagnostic change for generated code, part of the GAP-15 decision above. | @@ -563,54 +562,50 @@ structurally blind to the eighty-nine pending-row names whose spelling contains sets; the families are ruled above by name, the global by mechanism, and `scripts/check_kernel_residuals.py` holds the tables. -## Deprecated public spellings (R5 gap lane P2c rulings) - -Two public surfaces still spelled a TradingView name, and both are load-bearing: a public C ABI -field a compiled consumer reads by offset, and an enumerator of a shipped standalone C++ ABI. The -lane's decision is **alias and deprecate, never break**. The generic name is the primary spelling -in code and in the documentation; the old name stays a valid, value-identical alias, and its -removal is scheduled for the next epoch of the ABI that carries it. Nothing here changes a value, -an offset, a size, a serialized key or a behaviour: `PF_ABI_VERSION` stays 4 and `lifecycle_v1` -stays `lifecycle_v1`, because an alias needs no epoch. - -This table is not one of the residual-vocabulary tables above. -`scripts/check_kernel_residuals.py` rules by presence: it reads the first column of every table in -the section "Residual TradingView-named surface in the kernel-only archive" and fails on a ruled -name that is in neither the archive nor the installed headers. None of the four names below is a -symbol or a string literal in `libpineforge_kernel.a` — a struct field name and an enumerator exist -only at compile time, and neither is written into any `last_error` text — but since lane F6 the -gate reads the installed headers as well, where all four are spelled, so the residual section -rules them in a row of its own that points here. Verified on a Release and on a Debug archive: -`0 findings`, both profiles, with these aliases in the tree. - -| name | generic spelling | ABI that carries it | removal epoch | why retained | -|---|---|---|---|---| -| `pf_equity_stats_t::sharpe_tv` | `sharpe_monthly` | public C ABI (`include/pineforge/pineforge.h`, `PF_ABI_VERSION` 4) | the next `PF_ABI_VERSION` (5) | The field is month-end-resampled equity simple returns (chart timezone, open-time bucketing), risk-free 2 %/yr, annualized ×√12, sample (N−1) stddev — a construction whose name is its resampling period, not its calibration source. The published header is the contract of every compiled FFI consumer, so both names are one `double` behind a C11 anonymous union of two same-typed members: identical offset (48), identical `sizeof(pf_equity_stats_t)` (120), identical `offsetof(pf_metrics_t, equity)` (648), pinned by `static_assert` in `src/c_abi.cpp` and exercised from C by `tests/test_c_abi.c`. Compiler-deprecated since R5 lane F6: the member carries `PF_DEPRECATED` (`__attribute__((deprecated))` on GCC and Clang), so every use outside the pinning `static_assert` and `tests/test_c_abi.c` draws `-Wdeprecated-declarations`; `test_deprecated_public_spellings` holds both that and the alias still compiling. | -| `pf_equity_stats_t::sortino_tv` | `sortino_monthly` | public C ABI (as above) | the next `PF_ABI_VERSION` (5) | Same resampling as `sharpe_monthly`, population downside deviation vs the monthly risk-free. Aliased and compiler-deprecated on the same terms; offset 56. | -| `exit_legs::Domain::Coof` | `FillRecalc` | standalone C++ ABI `pineforge::exit_legs::lifecycle_v1` (`include/pineforge/exit_leg_lifecycle.hpp`) | `lifecycle_v2` | The domain is the fill-recalculation re-entry pass: the host re-runs its script after a fill and observes the rest of the same bar. `coof` abbreviates `calc_on_order_fills`, the Pine adapter's name for it (the same abbreviation the `coof_*` reflection rows carry, ruled above). An enumerator alias adds a name and no value: `Coof == FillRecalc == 1`, the underlying type stays `uint8_t`, `Domain::RawTicks` stays 4 (so `valid_frame`'s range check is unchanged), and no `switch` gains a case. Compiler-deprecated since R5 lane F6 (a C++17 `[[deprecated]]` enumerator attribute, pinned by `test_deprecated_public_spellings`); the twin-parity-frozen TU that keeps the old spelling, `test_exit_lifecycle_clock_l4c`, builds with `-Wno-deprecated-declarations`, its CHECK texts unchanged. | -| `exit_legs::Domain::MagnifierCoof` | `MagnifierFillRecalc` | standalone C++ ABI `lifecycle_v1` (as above) | `lifecycle_v2` | The same re-entry pass on a magnified sub-bar. `MagnifierCoof == MagnifierFillRecalc == 3`; compiler-deprecated on the same terms. | +## Deprecated public spellings (R5 gap lane P2c rulings; removed for 1.0, lane REL10) + +Two public surfaces spelled a TradingView name, and both were load-bearing: a public C ABI field a +compiled consumer reads by offset, and an enumerator of a shipped standalone C++ ABI. Lane P2c ruled +**alias and deprecate, never break**: the generic name became the primary spelling, the old name +stayed a value-identical alias (compiler-deprecated since lane F6), and its removal was scheduled for +the next epoch of the ABI that carries it: the next `PF_ABI_VERSION`, and the namespace after +`lifecycle_v1`. + +Lane REL10 removes the four aliases for 1.0.0, before the 1.0 compatibility promise begins, and +without those epochs: an alias shares its storage or its value with the primary spelling, so +removing it moves no offset, size, value, hash fold or mangled name, and neither epoch would have +anything to version. `PF_ABI_VERSION` stays 4 and `lifecycle_v1` stays `lifecycle_v1`. A consumer +that still names an old spelling renames it and recompiles. The generic names are rule 2's: the +fields are named for their resampling period, the domains for the fill-recalculation pass, and +neither for a Pine or TradingView setting. + +This table is not one of the residual-vocabulary tables above. None of the four names is a symbol +or a string literal in `libpineforge_kernel.a`, and none is declared in the installed headers, which +the gate reads with comments stripped, so `scripts/check_kernel_residuals.py` has nothing of theirs +to rule: the row that pointed here from the residual section left with them. + +| removed spelling | 1.0 spelling | ABI that carries it | what holds it | +|---|---|---|---| +| `pf_equity_stats_t::sharpe_tv` | `sharpe_monthly` | public C ABI (`include/pineforge/pineforge.h`, `PF_ABI_VERSION` 4) | Month-end-resampled equity simple returns (chart timezone, open-time bucketing), risk-free 2 %/yr, annualized ×√12, sample (N−1) stddev. A plain `double` again, at offset 48; `sizeof(pf_equity_stats_t)` 120 and `offsetof(pf_metrics_t, equity)` 648 are unmoved, pinned by `static_assert` in `src/c_abi.cpp` and checked from C by `tests/test_c_abi.c`. `test_removed_public_spellings` fails unless naming the old spelling fails to compile. | +| `pf_equity_stats_t::sortino_tv` | `sortino_monthly` | public C ABI (as above) | Same resampling, population downside deviation vs the monthly risk-free; offset 56. Held on the same terms. | +| `exit_legs::Domain::Coof` | `FillRecalc` | standalone C++ ABI `pineforge::exit_legs::lifecycle_v1` (`include/pineforge/exit_leg_lifecycle.hpp`) | The fill-recalculation re-entry pass: the host re-runs its script after a fill and observes the rest of the same bar (`coof` abbreviated `calc_on_order_fills`, the Pine adapter's name for it). `FillRecalc == 1`, the underlying type stays `uint8_t` and `RawTicks` stays 4. `test_removed_public_spellings` fails unless `Domain::Coof` fails to compile. | +| `exit_legs::Domain::MagnifierCoof` | `MagnifierFillRecalc` | standalone C++ ABI `lifecycle_v1` (as above) | The same pass on a magnified sub-bar; `MagnifierFillRecalc == 3`. Held on the same terms. | **The serialized report keys do not change.** `sharpe_tv` and `sortino_tv` remain the JSON keys of -the report dictionaries built by `docker/run_json.py` (`_stats_dict`, driven off the ctypes -`_fields_` list) and of the engine dict `scripts/crossvalidate_metrics.py` builds the same way. They -are ruled **report-schema names**: a schema key is a wire format, not an identifier, and renaming it -would break every Python/FFI consumer for no mechanical gain. The ctypes mirrors in +the report's `metrics.equity` dictionary, built by `docker/run_json.py`. They are ruled +**report-schema names**: a schema key is a wire format, not an identifier, and renaming it would +break every consumer of the report for no mechanical gain. The ctypes mirrors in `scripts/run_strategy.py`, `docker/run_json.py`, `tutorial/run.py`, -`benchmarks/throughput/grid_search_repro.py` and `docs/pages/ffi-python.md` therefore keep the -historical member name (ctypes matches by offset, never by the C field's name); each carries a -comment saying it is the historical spelling of `sharpe_monthly` / `sortino_monthly`. - -**Portability note for the C alias.** An anonymous union is C11. This project already compiles C at -C11 (`CMAKE_C_STANDARD 11`, and the native C examples document `cc -std=c11`), and C++ has had -anonymous unions since C++98, so every consumer in this repository is covered. A consumer compiling -the public header as strict C99 compiles it too, `-pedantic-errors` included: the union is spelled -`PF_ANONYMOUS_UNION`, which GCC and Clang expand to `__extension__ union` — an exemption for that one -declaration and nothing else of the consumer's. (The first version of this note promised such a -consumer "a warning, not an error"; under `-pedantic-errors` it was an error, which R5 lane F4 fixed.) -The CTest row `test_native_c_api_c99` compiles both public C headers at `-std=c99 -pedantic-errors` -with extensions off and executes the alias both ways. The rejected alternative was -`#define sharpe_tv sharpe_monthly`: a macro leaks into every translation unit that includes the header -and would rewrite an unrelated consumer's own `sharpe_tv`. +`benchmarks/throughput/grid_search_repro.py` and `docs/pages/ffi-python.md` name the C fields, and +`_stats_dict` in `docker/run_json.py` writes these two under their report keys +(`EQUITY_REPORT_KEYS`). `scripts/test_report_schema_keys.py`, a `ci_preflight` stage, pins the whole +`metrics.equity` key list in its pre-1.0 order and the value behind each of the two keys. + +**Portability.** The anonymous union behind the C alias was the one C11 construct in the public C +headers; lane F4 made strict C99 accept it by spelling it `PF_ANONYMOUS_UNION` (`__extension__ +union` on GCC and Clang). The union, `PF_ANONYMOUS_UNION` and the `PF_DEPRECATED` macro that marked +the two old fields left `pineforge.h` with them, and `test_native_c_api_c99` still compiles both +public C headers at `-std=c99 -pedantic-errors` with extensions off. ## Kernel capabilities the Pine adapter does not declare (the rulings the feature gate holds) diff --git a/docs/pages/abi-stability.md b/docs/pages/abi-stability.md index a7c7f9ed..d936a8bb 100644 --- a/docs/pages/abi-stability.md +++ b/docs/pages/abi-stability.md @@ -185,24 +185,22 @@ moves -- a declaring host's run is its empty-hook run, value for value -- and no C symbol is added: a C host declares both from its `pf_native_callbacks_v1`, a table without `on_bar_open` or `on_precommit`. -R5 gap lane P2c gives two TradingView-named public surfaces a generic primary -spelling without an epoch, because an alias needs none. -`pf_equity_stats_t::sharpe_tv` / `sortino_tv` are now -`sharpe_monthly` / `sortino_monthly`: each pair is one `double` behind a C11 -anonymous union of two same-typed members, so `sizeof(pf_equity_stats_t)` -(120), the field offsets (48, 56) and `offsetof(pf_metrics_t, equity)` (648) -are unchanged, `static_assert`s in `src/c_abi.cpp` pin them, and a consumer -compiled against either spelling reads the same storage. The standalone -`pineforge::exit_legs::lifecycle_v1` enumerators `Domain::Coof` / -`MagnifierCoof` are now `Domain::FillRecalc` / `MagnifierFillRecalc`, with the -old names kept as value-identical aliases (`== 1` and `== 3`, underlying type -still `uint8_t`, `RawTicks` still 4). Both old spellings are DEPRECATED: the C -fields are removed at the next `PF_ABI_VERSION`, the enumerators at -`lifecycle_v2`. Serialized report keys are unaffected — a report dictionary -still carries `sharpe_tv` / `sortino_tv`. Compile the public C header as C11 or -later (the project's own `CMAKE_C_STANDARD` is 11 and the native C examples -document `cc -std=c11`); strict C99 accepts the anonymous union with a -`-Wc11-extensions` warning. Rulings of record: ADR-0001, "Deprecated public +R5 gap lane P2c gave two TradingView-named public surfaces a generic primary +spelling without an epoch, because an alias needs none, and lane REL10 removed +the old spellings for 1.0 without one, because removing an alias moves +nothing. The C fields are `pf_equity_stats_t::sharpe_monthly` / +`sortino_monthly` (pre-1.0 also `sharpe_tv` / `sortino_tv`): +`sizeof(pf_equity_stats_t)` (120), the field offsets (48, 56) and +`offsetof(pf_metrics_t, equity)` (648) are unchanged, and `static_assert`s in +`src/c_abi.cpp` pin them. The standalone `pineforge::exit_legs::lifecycle_v1` +enumerators are `Domain::FillRecalc` / `MagnifierFillRecalc` (pre-1.0 also +`Coof` / `MagnifierCoof`), still `== 1` and `== 3`, underlying type still +`uint8_t`, `RawTicks` still 4. `test_removed_public_spellings` holds that each +old spelling fails to compile. Serialized report keys are unaffected: a report +dictionary still carries `sharpe_tv` / `sortino_tv` +(`scripts/test_report_schema_keys.py`). With the anonymous union gone, the +public C headers are plain C99, which `test_native_c_api_c99` compiles at +`-std=c99 -pedantic-errors`. Rulings of record: ADR-0001, "Deprecated public spellings". The two relocation manifests remain reviewed descriptions of the v16→v18 source diff --git a/docs/pages/examples-rust.md b/docs/pages/examples-rust.md index 8182fd82..a4b98c97 100644 --- a/docs/pages/examples-rust.md +++ b/docs/pages/examples-rust.md @@ -94,7 +94,7 @@ struct PfEquityStats { max_equity_drawdown: f64, max_equity_drawdown_pct: f64, max_equity_runup: f64, max_equity_runup_pct: f64, buy_hold_return: f64, buy_hold_return_pct: f64, - sharpe_monthly: f64, sortino_monthly: f64, // shipped as sharpe_tv / sortino_tv + sharpe_monthly: f64, sortino_monthly: f64, // JSON report keys sharpe_tv / sortino_tv sharpe_bar: f64, sortino_bar: f64, cagr: f64, calmar: f64, recovery_factor: f64, time_in_market_pct: f64, diff --git a/docs/pages/exit-leg-lifecycle.md b/docs/pages/exit-leg-lifecycle.md index a32279a2..0e9af509 100644 --- a/docs/pages/exit-leg-lifecycle.md +++ b/docs/pages/exit-leg-lifecycle.md @@ -47,10 +47,10 @@ barriers retain their requesting event/domain; completion identifies that exact barrier and an actual after-margin hook. Completion cannot follow the action's processing receipt; separate events caused by an owner bind retain that hook's phase. Ordinary, fill-recalculation (`Domain::FillRecalc`, the host's -re-entry pass after a fill — shipped as the deprecated `Domain::Coof`, for -TradingView's `calc_on_order_fills`) and both magnifier hooks -(`Domain::Magnifier`, `Domain::MagnifierFillRecalc`, the latter shipped as -`Domain::MagnifierCoof`) remain explicit. Raw-tick bar advancement supplies no such completion. +re-entry pass after a fill — spelled `Domain::Coof`, for TradingView's +`calc_on_order_fills`, before 1.0) and both magnifier hooks +(`Domain::Magnifier`, `Domain::MagnifierFillRecalc`, the latter spelled +`Domain::MagnifierCoof` before 1.0) remain explicit. Raw-tick bar advancement supplies no such completion. ## Bounded replay contract diff --git a/docs/pages/ffi-python.md b/docs/pages/ffi-python.md index 479a0c83..2c55b296 100644 --- a/docs/pages/ffi-python.md +++ b/docs/pages/ffi-python.md @@ -80,11 +80,9 @@ class pf_equity_stats_t(ctypes.Structure): ("max_equity_drawdown", ctypes.c_double), ("max_equity_drawdown_pct", ctypes.c_double), ("max_equity_runup", ctypes.c_double), ("max_equity_runup_pct", ctypes.c_double), ("buy_hold_return", ctypes.c_double), ("buy_hold_return_pct", ctypes.c_double), - # Historical member names of sharpe_monthly / sortino_monthly. ctypes - # matches by offset, never by the C field's name, and these strings are - # also the serialized report keys, so the mirror keeps them (ADR-0001, - # "Deprecated public spellings"). - ("sharpe_tv", ctypes.c_double), ("sortino_tv", ctypes.c_double), + # The C field names. The JSON report keys of these two stay + # sharpe_tv / sortino_tv (ADR-0001, "Deprecated public spellings"). + ("sharpe_monthly", ctypes.c_double), ("sortino_monthly", ctypes.c_double), ("sharpe_bar", ctypes.c_double), ("sortino_bar", ctypes.c_double), ("cagr", ctypes.c_double), ("calmar", ctypes.c_double), ("recovery_factor", ctypes.c_double), ("time_in_market_pct", ctypes.c_double), diff --git a/docs/pages/metrics.md b/docs/pages/metrics.md index cb3a7ce0..5a0a1d83 100644 --- a/docs/pages/metrics.md +++ b/docs/pages/metrics.md @@ -73,7 +73,7 @@ every **script-bar close**, timestamped with the script-bar **open** time | `max_equity_drawdown` (+`_pct`) | Peak-to-trough equity drop, positive currency; pct vs the peak in effect | — (0 when flat) | | `max_equity_runup` (+`_pct`) | Trough-to-peak rise, **trough resets on each new equity peak** (mirrors the engine's intra-run extremes); pct vs that trough | — | | `buy_hold_return` (+`_pct`) | `initial_capital · (last_close/first_open − 1)` | first open non-finite or ≤ 0 | -| `sharpe_monthly` / `sortino_monthly` | Month-end-resampled equity simple returns (chart tz, open-time bucketing), risk-free 2 %/yr (2/12 per month), annualized ×√12. Sharpe: sample (N−1) stddev. Sortino: population downside deviation vs the monthly risk-free. Deprecated alias: `sharpe_tv` / `sortino_tv` — the same `double` at the same offset, removed at the next `PF_ABI_VERSION`; the **serialized report key stays `sharpe_tv` / `sortino_tv`** (see @ref report_schema) | < 2 monthly returns, or zero deviation | +| `sharpe_monthly` / `sortino_monthly` | Month-end-resampled equity simple returns (chart tz, open-time bucketing), risk-free 2 %/yr (2/12 per month), annualized ×√12. Sharpe: sample (N−1) stddev. Sortino: population downside deviation vs the monthly risk-free. The pre-1.0 C spelling `sharpe_tv` / `sortino_tv` (the same `double` at the same offset) was removed for 1.0; the **serialized report key stays `sharpe_tv` / `sortino_tv`** (see @ref report_schema) | < 2 monthly returns, or zero deviation | | `sharpe_bar` / `sortino_bar` | Same construction over per-script-bar returns, annualized by **observed bar density** (`bars/yr = (len−1)/calendar span`) — not a fixed calendar formula | < 2 returns, or zero deviation | | `cagr` | `100 · ((final_equity/initial_capital)^(1/years) − 1)`, calendar span | span ≤ 0, or either side ≤ 0 | | `calmar` | `cagr / max_equity_drawdown_pct` (both percent → dimensionless) | zero drawdown | @@ -103,8 +103,8 @@ exactly from the engine curve: | TV panel row | TV's actual construction | Engine field & difference | | --- | --- | --- | -| Sharpe ratio | Monthly returns of **realized** equity (open profit excluded), account-currency, UTC months, rf 2 %/12, **population** stddev, **not annualized** | `sharpe_monthly` (historical spelling `sharpe_tv`): mark-to-market equity, chart-tz months, sample stddev, ×√12 | -| Sortino ratio | Same series, population downside dev vs rf, not annualized | `sortino_monthly` (historical spelling `sortino_tv`) = TV × √12 (same convention otherwise — matches to 4 decimals after de-annualizing) | +| Sharpe ratio | Monthly returns of **realized** equity (open profit excluded), account-currency, UTC months, rf 2 %/12, **population** stddev, **not annualized** | `sharpe_monthly` (report key `sharpe_tv`): mark-to-market equity, chart-tz months, sample stddev, ×√12 | +| Sortino ratio | Same series, population downside dev vs rf, not annualized | `sortino_monthly` (report key `sortino_tv`) = TV × √12 (same convention otherwise — matches to 4 decimals after de-annualizing) | | Max/avg drawdown & run-up "(close-to-close)" | Realized equity sampled at **trade exits only**, split into alternating phases at the global max/min; phase value = endpoint-to-endpoint change (durations in days corroborate) | `max_equity_drawdown/runup`: per-script-bar curve with trough-reset walk | | Max DD / run-up "(intrabar)" | Settled realized curve (with entry-commission dips) vs per-trade excursion extremes | closest to the engine's per-bar walk; reproduced exactly from trade MFE/MAE | | CAGR | Net over the **configured backtesting range** day count, 365-day year | `cagr`: traded calendar span, 365.25 | @@ -118,9 +118,8 @@ durations, intrabar excursion variants, account-size/margin rows. The retired re ```python report.metrics.all.profit_factor # ctypes mirror, see FFI page -report.metrics.equity.sharpe_tv # the ctypes mirror keeps the historical - # member name of equity.sharpe_monthly; - # it is also the serialized JSON key +report.metrics.equity.sharpe_monthly # the C field name; the serialized + # JSON key of this field is sharpe_tv curve = report.equity_curve[:report.equity_curve_len] ``` diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 4defbc99..1fc6a0f3 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -4380,18 +4380,18 @@ surface"); the short version: layer alone, and a bare host's `strategy_pending_orders_len` is 0. The neutral view is `native_working_requests()`. Each family has its ADR row, listing every name, and the gate above holds the list. -- **Deprecated public spellings** (`docs/adr/0001-kernel-adapter-boundary.md`, - "Deprecated public spellings" — a separate table, deliberately outside the - residual section the gate parses, because none of these names is a symbol or - a literal in the archive). The C ABI's `pf_equity_stats_t::sharpe_tv` / - `sortino_tv` are now `sharpe_monthly` / `sortino_monthly` — the same `double` - at the same offset behind a C11 anonymous union, so nothing an FFI consumer - links or reads moves; the old spelling is deprecated and removed at the next - `PF_ABI_VERSION`, and the serialized report key stays `sharpe_tv`. The - standalone `lifecycle_v1` enumerators `exit_legs::Domain::Coof` / - `MagnifierCoof` are now `FillRecalc` / `MagnifierFillRecalc` (the - fill-recalculation re-entry pass), with the old names kept as - value-identical aliases until `lifecycle_v2`. +- **Deprecated public spellings, removed for 1.0** + (`docs/adr/0001-kernel-adapter-boundary.md`, "Deprecated public spellings" — + a separate table, outside the residual section the gate parses, because none + of these names is a symbol or a literal in the archive). The C ABI's + `pf_equity_stats_t::sharpe_monthly` / `sortino_monthly` were also spelled + `sharpe_tv` / `sortino_tv` before 1.0, the same `double` at the same offset; + the old spelling is gone, nothing an FFI consumer links or reads moved, and + the serialized report key stays `sharpe_tv`. The standalone `lifecycle_v1` + enumerators `exit_legs::Domain::FillRecalc` / `MagnifierFillRecalc` (the + fill-recalculation re-entry pass) were also spelled `Coof` / `MagnifierCoof` + before 1.0, with the same values. `test_removed_public_spellings` holds that + each old spelling fails to compile. - **The ambient EMA seeding default.** `ta::EMA` seeds from its first finite input (`EmaSeeding::FirstValue`, the default) or from the simple average of its first `length` inputs (`EmaSeeding::SimpleAverage`, na for the diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 43368c4a..49ce1d5b 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -140,7 +140,7 @@ rather than defaulted. | `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:378 | `intrabar_kind` native_c_api.h:2331 | `tests/test_native_calc_timing.cpp` | The host owns the lower bars, as `lower_tf` native_run_spec.hpp:393 or `synthesized` native_run_spec.hpp:407. The C magnifier arguments of `run_backtest_full` pineforge.h:648 configure a *compiled Pine strategy*, never a native host. | | `fill_orders_on_standard_ohlc` | none — the kernel matches the bars you feed it | — | — | TradingView's switch exists because a Heikin-Ashi chart's candles are not the traded prices. A native host feeds the standard OHLC it wants matched, and derives any transformed series itself. | | `max_bars_back` | none — a native host owns its own history | — | `hello_kernel.cpp` | Pine infers a buffer length for every series. `pineforge::Series` series.hpp:94 is a fixed-capacity ring you size yourself, so nothing is inferred and nothing silently truncates. | -| `risk_free_rate` | none — the ratio is a report field with TradingView's fixed rate | `sharpe_monthly` pineforge.h:317 | `native_sized_report_strategy.cpp` | `pf_metrics_t::equity.sharpe_monthly` and `pf_metrics_t::equity.sortino_monthly` (`sortino_monthly` pineforge.h:329) use a fixed 2 %/yr, the TradingView default; nothing in the kernel takes a declared rate. `sharpe_tv` / `sortino_tv` are the deprecated spellings of the same two doubles (one anonymous union each, same offset), removed at the next `PF_ABI_VERSION`; the serialized report key stays `sharpe_tv`. A host that needs another rate computes it from `pf_report_t::equity_curve`. | +| `risk_free_rate` | none — the ratio is a report field with TradingView's fixed rate | `sharpe_monthly` pineforge.h:276 | `native_sized_report_strategy.cpp` | `pf_metrics_t::equity.sharpe_monthly` and `pf_metrics_t::equity.sortino_monthly` (`sortino_monthly` pineforge.h:282) use a fixed 2 %/yr, the TradingView default; nothing in the kernel takes a declared rate. Their pre-1.0 spellings `sharpe_tv` / `sortino_tv` were removed for 1.0; the serialized report keys stay `sharpe_tv` / `sortino_tv`. A host that needs another rate computes it from `pf_report_t::equity_curve`. | ### The feed-shape and presentation policies Pine has no word for diff --git a/docs/pages/report-schema.md b/docs/pages/report-schema.md index 3101c240..142ac180 100644 --- a/docs/pages/report-schema.md +++ b/docs/pages/report-schema.md @@ -216,14 +216,13 @@ by sqrt(12)) and `sharpe_bar` / `sortino_bar` (per-script-bar returns annualized by observed bar density) — plus `cagr`, `calmar`, `recovery_factor`, `time_in_market_pct`, and `open_pl`. -`sharpe_tv` / `sortino_tv` are the historical spelling of -`sharpe_monthly` / `sortino_monthly`: one `double` at one offset behind a -C11 anonymous union, deprecated in the C header and removed at the next -`PF_ABI_VERSION`. **The serialized keys do not change**: a report -dictionary still carries `sharpe_tv` and `sortino_tv`, and they are ruled -report-schema names — the historical spelling of `sharpe_monthly` / -`sortino_monthly` — so no consumer of this schema has to change. See -ADR-0001, "Deprecated public spellings". +The C fields behind `sharpe_tv` / `sortino_tv` are +`pf_equity_stats_t::sharpe_monthly` / `sortino_monthly`; the pre-1.0 C +spelling `sharpe_tv` / `sortino_tv` of those fields was removed for 1.0. +**The serialized keys do not change**: a report dictionary still carries +`sharpe_tv` and `sortino_tv`, ruled report-schema names, so no consumer of +this schema has to change (`scripts/test_report_schema_keys.py` pins the +`metrics.equity` keys). See ADR-0001, "Deprecated public spellings". **NaN convention:** any statistic whose denominator is empty or zero is `NaN`, never 0 or an infinity — e.g. `profit_factor` with zero gross diff --git a/include/pineforge/exit_leg_lifecycle.hpp b/include/pineforge/exit_leg_lifecycle.hpp index 4a1f79b5..9b0a5157 100644 --- a/include/pineforge/exit_leg_lifecycle.hpp +++ b/include/pineforge/exit_leg_lifecycle.hpp @@ -19,17 +19,12 @@ enum class Leg : uint8_t { Stop, Limit, Trail }; // The observation domain a frame comes from. `FillRecalc` is the // fill-recalculation re-entry pass -- the host re-runs its script after a fill // and observes the rest of the same bar; `MagnifierFillRecalc` is that pass on -// a magnified sub-bar. `Coof` / `MagnifierCoof` are the historical spellings -// (coof = calc-on-order-fills, the Pine adapter's name for the same pass); -// they are DEPRECATED aliases with identical values and are removed at -// lifecycle_v2. See ADR-0001 "Deprecated public spellings". +// a magnified sub-bar. Their pre-1.0 spellings `Coof` / `MagnifierCoof` (coof = +// calc-on-order-fills, the Pine adapter's name for the same pass) were removed +// for 1.0; the values (1 and 3) and the underlying type are unchanged. See +// ADR-0001 "Deprecated public spellings". enum class Domain : uint8_t { Ordinary, FillRecalc, Magnifier, MagnifierFillRecalc, RawTicks, - Coof [[deprecated("Coof is the historical spelling of FillRecalc; removed at lifecycle_v2")]] - = FillRecalc, - MagnifierCoof [[deprecated("MagnifierCoof is the historical spelling of " - "MagnifierFillRecalc; removed at lifecycle_v2")]] - = MagnifierFillRecalc, }; enum class Phase : uint8_t { Observation, AfterMargin }; enum class Fold : uint8_t { Prefix, Continue }; diff --git a/include/pineforge/pineforge.h b/include/pineforge/pineforge.h index fcb818fd..f5ac00b9 100644 --- a/include/pineforge/pineforge.h +++ b/include/pineforge/pineforge.h @@ -83,28 +83,6 @@ #define PF_API #endif -/* A public spelling kept only as a value-identical alias for compiled - * consumers (ADR-0001 "Deprecated public spellings"): a consumer that still - * names it compiles, with the compiler's deprecation diagnostic. */ -#if defined(__GNUC__) || defined(__clang__) - #define PF_DEPRECATED(msg) __attribute__((deprecated(msg))) -#elif defined(_MSC_VER) - #define PF_DEPRECATED(msg) __declspec(deprecated(msg)) -#else - #define PF_DEPRECATED(msg) -#endif - -/* An anonymous union member: C11 and C++ have them, and so does every - * compiler this runtime builds with, but strict C99 does not -- a C99 - * consumer compiling with -pedantic-errors would reject the declaration. - * GCC and Clang mark it __extension__, which exempts exactly that one - * declaration from -pedantic and nothing else in the consumer. */ -#if defined(__GNUC__) || defined(__clang__) - #define PF_ANONYMOUS_UNION __extension__ union -#else - #define PF_ANONYMOUS_UNION union -#endif - /** Monotonic ABI version of pf_report_t / pf_trade_t layout. Bumped * whenever a caller-visible struct grows. Consumers MUST verify * pf_abi_version() == PF_ABI_VERSION before calling run_backtest. @@ -306,32 +284,16 @@ typedef struct pf_equity_stats_s { * bucketing), risk-free 2%/yr (2/12 per month), annualized by sqrt(12). * Uses sample (N-1) stddev. NaN with <2 monthly returns or zero deviation. * - * `sharpe_tv` is the historical spelling of this same field. Both names - * are one `double` at one offset (an anonymous union of two members of - * the same type, #PF_ANONYMOUS_UNION so a strict C99 consumer compiles - * it too), so the struct's size and every field offset are unchanged and - * a caller compiled against either spelling reads the same storage. The old spelling is DEPRECATED and is removed at the next - * #PF_ABI_VERSION; see ADR-0001 "Deprecated public spellings". The - * serialized report key stays `sharpe_tv` (report-schema name). */ - PF_ANONYMOUS_UNION { - double sharpe_monthly; - PF_DEPRECATED("sharpe_tv is the historical spelling of sharpe_monthly; " - "removed at PF_ABI_VERSION 5") - double sharpe_tv; /**< Deprecated spelling of - * pf_equity_stats_s::sharpe_monthly. */ - }; + * Its pre-1.0 spelling `sharpe_tv` named this same double at this same + * offset (48) and was removed for 1.0; the serialized report key is still + * `sharpe_tv` (report-schema name). */ + double sharpe_monthly; /** Same resampling as sharpe_monthly; uses population downside deviation * vs the monthly risk-free. NaN with <2 monthly returns or zero deviation. * - * `sortino_tv` is the historical spelling of this same field, on the same - * terms as sharpe_monthly / sharpe_tv above. */ - PF_ANONYMOUS_UNION { - double sortino_monthly; - PF_DEPRECATED("sortino_tv is the historical spelling of sortino_monthly; " - "removed at PF_ABI_VERSION 5") - double sortino_tv; /**< Deprecated spelling of - * pf_equity_stats_s::sortino_monthly. */ - }; + * Its pre-1.0 spelling `sortino_tv` (offset 56) was removed the same way; + * the serialized report key is still `sortino_tv`. */ + double sortino_monthly; double sharpe_bar; /**< Per-script-bar returns, annualized by observed bar density * (bars per year = (len-1)/calendar span), NOT a fixed * calendar formula. Uses sample (N-1) stddev. diff --git a/scripts/check_doc_anchors.py b/scripts/check_doc_anchors.py index 7bb76433..9f314fcf 100644 --- a/scripts/check_doc_anchors.py +++ b/scripts/check_doc_anchors.py @@ -384,7 +384,7 @@ def symbol_of(span: str) -> tuple[str | None, str]: def qualifier_of(span: str) -> str | None: """The scope a qualified span claims: ``NativeRiskLimits`` of ``NativeRiskLimits::max_fills_per_day``, ``Domain`` of - ``exit_legs::Domain::Coof``; None for an unqualified span.""" + ``exit_legs::Domain::FillRecalc``; None for an unqualified span.""" text = span.strip() for cut in '({<[': index = text.find(cut) diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index 26748494..3f5771f9 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -207,6 +207,9 @@ def check_commands(source: Path) -> list[tuple]: # every mutation of the recorded battery the full sweep caught. ('corpus-parity-subset-cover-tests', [sys.executable, str(source / 'scripts/test_corpus_parity_subset_cover.py')]), + # Lane REL10: the JSON report keys outlive the removed C spellings. + ('report-schema-key-tests', + [sys.executable, str(source / 'scripts/test_report_schema_keys.py')]), ('design-inventory-tests', [sys.executable, str(source / 'scripts/test_check_design_inventory.py')]), ('design-inventory', diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index ed074326..b33100e3 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -130,7 +130,7 @@ # +3 F12 example_native_fee_reserve_strategy, example_native_fx_roll_strategy, # example_native_broker_hash_strategy # +3 F6 test_f6_dead_kernel_members, test_timeframe_trace_switch_once, -# test_deprecated_public_spellings +# test_removed_public_spellings (renamed by lane REL10) # +1 F3 test_native_bare_host_contracts # +1 F4 test_native_c_api_c99 # +1 F5 test_native_session_day_facts @@ -393,8 +393,12 @@ # edit of this file. A lane that adds rulings raises the floor with them: # 46 texts once the lane also ruled Eigen's `matrix.cols() == # matrix.rows()` assert text, which a debug or sanitizers archive carries. -ADR_RULED_IDENTIFIERS_MIN = 174 -ADR_RULED_TEXTS_MIN = 46 +# 170 identifiers and 40 texts once lane REL10 removed the four deprecated +# spellings for 1.0 (sharpe_tv, sortino_tv, Coof, MagnifierCoof): their names +# and their six deprecation and static_assert texts left the headers, the +# archive and the ADR's residual table together (INT26 pick of 38e8ad1e). +ADR_RULED_IDENTIFIERS_MIN = 170 +ADR_RULED_TEXTS_MIN = 40 # PR-only registration floors: the complete CTest populations of the three # excluded profiles at INT25, counted with ctest -N on the integrated tree -- # 653/653/662 at 91d65ad6 (INT24) plus wave G's six rows (C-SURFACE-1 +1, diff --git a/scripts/crossvalidate_metrics.py b/scripts/crossvalidate_metrics.py index 8c18689c..c1010dc1 100644 --- a/scripts/crossvalidate_metrics.py +++ b/scripts/crossvalidate_metrics.py @@ -32,7 +32,8 @@ - sharpe_bar = (mean(r)-rf_bar)/sample_sd(r, N-1)*sqrt(bpy), rf_bar = 0.02/bpy; - sortino_bar = (mean(r)-rf_bar)/pop_downside_dev(r vs rf_bar)*sqrt(bpy); - - sharpe_tv/sortino_tv: same construction over month-end-resampled + - sharpe_monthly/sortino_monthly (report keys sharpe_tv/sortino_tv): + the same construction over month-end-resampled equities (UTC calendar-month bucketing of bar-open times when the chart tz is empty), rf = 0.02/12, annualized by sqrt(12); - max_equity_drawdown: peak-to-trough walk over the curve (USD), pct @@ -355,22 +356,22 @@ def crossvalidate(strategy_dir: Path, ohlcv: Path, trim_end_ms: int | None, # --- TV monthly sharpe / sortino ---------------------------------------- np_sh_tv, np_so_tv = np_sharpe_sortino(m_ret, rf_month, math.sqrt(12.0)) - t = Table(f"sharpe_tv / sortino_tv (monthly, {len(m_ret)} returns, rf/mo={rf_month:.6f})") - t.add("sharpe_tv | numpy engine conv.", eng["sharpe_tv"], np_sh_tv) - t.add("sharpe_tv | empyrical(risk_free=rf/12, ann=12)", - eng["sharpe_tv"], ep.sharpe_ratio(m_ret_s, risk_free=rf_month, annualization=12)) - t.add("sharpe_tv | quantstats adapted (m-rf/12, rf=0)", - eng["sharpe_tv"], qs.stats.sharpe(m_ret_s - rf_month, rf=0.0, periods=12)) - t.add("sharpe_tv | quantstats native rf (geometric deann.)", - eng["sharpe_tv"], qs.stats.sharpe(m_ret_s, rf=RF_ANNUAL, periods=12), + t = Table(f"sharpe_monthly / sortino_monthly (monthly, {len(m_ret)} returns, rf/mo={rf_month:.6f})") + t.add("sharpe_monthly | numpy engine conv.", eng["sharpe_monthly"], np_sh_tv) + t.add("sharpe_monthly | empyrical(risk_free=rf/12, ann=12)", + eng["sharpe_monthly"], ep.sharpe_ratio(m_ret_s, risk_free=rf_month, annualization=12)) + t.add("sharpe_monthly | quantstats adapted (m-rf/12, rf=0)", + eng["sharpe_monthly"], qs.stats.sharpe(m_ret_s - rf_month, rf=0.0, periods=12)) + t.add("sharpe_monthly | quantstats native rf (geometric deann.)", + eng["sharpe_monthly"], qs.stats.sharpe(m_ret_s, rf=RF_ANNUAL, periods=12), known_convention_delta=True) - t.add("sortino_tv | numpy engine conv.", eng["sortino_tv"], np_so_tv) - t.add("sortino_tv | empyrical(required_return=rf/12, ann=12)", - eng["sortino_tv"], ep.sortino_ratio(m_ret_s, required_return=rf_month, annualization=12)) - t.add("sortino_tv | quantstats adapted (m-rf/12, rf=0)", - eng["sortino_tv"], qs.stats.sortino(m_ret_s - rf_month, rf=0.0, periods=12)) - t.add("sortino_tv | quantstats native rf (geometric deann.)", - eng["sortino_tv"], qs.stats.sortino(m_ret_s, rf=RF_ANNUAL, periods=12), + t.add("sortino_monthly | numpy engine conv.", eng["sortino_monthly"], np_so_tv) + t.add("sortino_monthly | empyrical(required_return=rf/12, ann=12)", + eng["sortino_monthly"], ep.sortino_ratio(m_ret_s, required_return=rf_month, annualization=12)) + t.add("sortino_monthly | quantstats adapted (m-rf/12, rf=0)", + eng["sortino_monthly"], qs.stats.sortino(m_ret_s - rf_month, rf=0.0, periods=12)) + t.add("sortino_monthly | quantstats native rf (geometric deann.)", + eng["sortino_monthly"], qs.stats.sortino(m_ret_s, rf=RF_ANNUAL, periods=12), known_convention_delta=True) tables.append(t) @@ -556,15 +557,15 @@ def engine_convention_checks(run: dict, cap: float) -> tuple[list, dict]: _lib_value(ep.sortino_ratio, r_s, required_return=rf_bar, annualization=bpy)), ("sortino_bar", "quantstats", eng["sortino_bar"], _lib_value(qs.stats.sortino, r_s - rf_bar, rf=0.0, periods=bpy)), - ("sharpe_tv", "numpy", eng["sharpe_tv"], np_sh_tv), - ("sharpe_tv", "empyrical", eng["sharpe_tv"], + ("sharpe_monthly", "numpy", eng["sharpe_monthly"], np_sh_tv), + ("sharpe_monthly", "empyrical", eng["sharpe_monthly"], _lib_value(ep.sharpe_ratio, m_ret_s, risk_free=rf_month, annualization=12)), - ("sharpe_tv", "quantstats", eng["sharpe_tv"], + ("sharpe_monthly", "quantstats", eng["sharpe_monthly"], _lib_value(qs.stats.sharpe, m_ret_s - rf_month, rf=0.0, periods=12)), - ("sortino_tv", "numpy", eng["sortino_tv"], np_so_tv), - ("sortino_tv", "empyrical", eng["sortino_tv"], + ("sortino_monthly", "numpy", eng["sortino_monthly"], np_so_tv), + ("sortino_monthly", "empyrical", eng["sortino_monthly"], _lib_value(ep.sortino_ratio, m_ret_s, required_return=rf_month, annualization=12)), - ("sortino_tv", "quantstats", eng["sortino_tv"], + ("sortino_monthly", "quantstats", eng["sortino_monthly"], _lib_value(qs.stats.sortino, m_ret_s - rf_month, rf=0.0, periods=12)), ("cagr", "numpy", eng["cagr"], np_cagr), ("calmar", "numpy", eng["calmar"], np_calmar), diff --git a/scripts/run_strategy.py b/scripts/run_strategy.py index ce8a2219..fca14d79 100644 --- a/scripts/run_strategy.py +++ b/scripts/run_strategy.py @@ -725,11 +725,9 @@ class EquityStatsC(ctypes.Structure): ("max_equity_drawdown", ctypes.c_double), ("max_equity_drawdown_pct", ctypes.c_double), ("max_equity_runup", ctypes.c_double), ("max_equity_runup_pct", ctypes.c_double), ("buy_hold_return", ctypes.c_double), ("buy_hold_return_pct", ctypes.c_double), - # Historical member names of the C fields sharpe_monthly / - # sortino_monthly. ctypes matches by offset, never by the C field's - # name, and these strings are the serialized report keys, so the mirror - # keeps them (ADR-0001, "Deprecated public spellings"). - ("sharpe_tv", ctypes.c_double), ("sortino_tv", ctypes.c_double), + # The C field names. The JSON report keys of these two stay + # sharpe_tv / sortino_tv (ADR-0001, "Deprecated public spellings"). + ("sharpe_monthly", ctypes.c_double), ("sortino_monthly", ctypes.c_double), ("sharpe_bar", ctypes.c_double), ("sortino_bar", ctypes.c_double), ("cagr", ctypes.c_double), ("calmar", ctypes.c_double), ("recovery_factor", ctypes.c_double), ("time_in_market_pct", ctypes.c_double), diff --git a/scripts/test_report_schema_keys.py b/scripts/test_report_schema_keys.py new file mode 100644 index 00000000..ac981837 --- /dev/null +++ b/scripts/test_report_schema_keys.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""The JSON report keys of pf_equity_stats_t outlive its 1.0 field spellings. + +Lane REL10 removed the pre-1.0 C spellings ``sharpe_tv`` / ``sortino_tv``; +the fields are ``sharpe_monthly`` / ``sortino_monthly``. The report schema is a +wire format and keeps its keys (ADR-0001, "Deprecated public spellings"): the +ctypes mirror in docker/run_json.py names the C fields, and ``_stats_dict`` +writes those two under their report keys. These tests pin the equity key list +a report carried before the rename, the value behind each renamed key, and the +mirror's layout. No compiled engine is needed. +""" +from __future__ import annotations + +import ctypes +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "docker")) + +import run_json # noqa: E402 + +# metrics.equity's keys, in order, as docker/run_json.py wrote them at +# 1a0e7ea1, the commit before the rename. +EQUITY_KEYS = [ + "max_equity_drawdown", "max_equity_drawdown_pct", + "max_equity_runup", "max_equity_runup_pct", + "buy_hold_return", "buy_hold_return_pct", + "sharpe_tv", "sortino_tv", + "sharpe_bar", "sortino_bar", + "cagr", "calmar", + "recovery_factor", "time_in_market_pct", + "open_pl", +] + + +class _Report: + """The fields build_report_dict reads, as run_json_diagnostics_test.py + fakes them: no trades, no equity curve, a real zero MetricsC.""" + trades_len = 0 + equity_curve = None + net_profit = 0.0 + input_bars_processed = 0 + script_bars_processed = 0 + magnifier_sub_bars_total = 0 + magnifier_sample_ticks_total = 0 + bar_magnifier_enabled = 0 + + def __init__(self) -> None: + self.metrics = run_json.MetricsC() + + +class ReportSchemaKeyTests(unittest.TestCase): + def test_equity_keys_are_the_pre_rename_keys(self) -> None: + self.assertEqual(list(run_json._stats_dict(run_json.EquityStatsC())), EQUITY_KEYS) + + def test_renamed_fields_carry_their_values_under_the_report_keys(self) -> None: + stats = run_json.EquityStatsC() + stats.sharpe_monthly = 1.25 + stats.sortino_monthly = -0.5 + out = run_json._stats_dict(stats) + self.assertEqual(out["sharpe_tv"], 1.25) + self.assertEqual(out["sortino_tv"], -0.5) + self.assertNotIn("sharpe_monthly", out) + self.assertNotIn("sortino_monthly", out) + + def test_trade_stats_keep_their_field_names(self) -> None: + names = [name for name, _ in run_json.TradeStatsC._fields_] + self.assertEqual(list(run_json._stats_dict(run_json.TradeStatsC())), names) + + def test_mirror_layout_matches_the_c_header(self) -> None: + # src/c_abi.cpp pins the same numbers with static_assert. + self.assertEqual(ctypes.sizeof(run_json.EquityStatsC), 120) + self.assertEqual(run_json.EquityStatsC.sharpe_monthly.offset, 48) + self.assertEqual(run_json.EquityStatsC.sortino_monthly.offset, 56) + self.assertEqual(run_json.EquityStatsC.sharpe_bar.offset, 64) + self.assertEqual(ctypes.sizeof(run_json.MetricsC), 768) + self.assertEqual(run_json.MetricsC.equity.offset, 648) + + def test_report_dict_writes_the_report_keys(self) -> None: + report = _Report() + report.metrics.equity.sharpe_monthly = 2.0 + rep = run_json.build_report_dict( + report, ohlcv_path="x.csv", n_bars=0, first_ts=0, last_ts=0, + elapsed=0.0, applied_inputs={}, applied_overrides={}, applied_runtime={}) + equity = rep["metrics"]["equity"] + self.assertEqual(list(equity), EQUITY_KEYS) + self.assertEqual(equity["sharpe_tv"], 2.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/c_abi.cpp b/src/c_abi.cpp index b32e5d55..e591dc27 100644 --- a/src/c_abi.cpp +++ b/src/c_abi.cpp @@ -239,31 +239,20 @@ static_assert(offsetof(pf_report_t, broker_state_hash) == offsetof(pineforge::Re static_assert(offsetof(pf_report_t, broker_state_hash_len) == offsetof(pineforge::ReportC, broker_state_hash_len), "pf_report_t::broker_state_hash_len offset mismatch"); -/* ── Equity-stats deprecated-spelling parity (R5 gap lane P2c) ──── */ -/* sharpe_monthly / sortino_monthly are the generic spelling of the fields - * this ABI shipped as sharpe_tv / sortino_tv. Each pair is ONE double behind a - * C11 anonymous union of two same-typed members, so the old spelling stays - * valid and no offset, no size and no value moves. These assertions pin the - * numbers measured on the commit before the alias (1974e87e, arm64 macOS and - * the LP64 C ABI generally): sizeof 120, the pair at 48 and 56, and - * pf_metrics_t::equity at 648. The old spelling is removed at the next - * PF_ABI_VERSION; until then both names must keep answering one offset. */ +/* ── Equity-stats layout (R5 gap lane P2c; removal for 1.0, lane REL10) ── */ +/* sharpe_monthly / sortino_monthly are plain doubles again: their pre-1.0 + * spellings sharpe_tv / sortino_tv, which shared that storage through an + * anonymous union, were removed for 1.0. These assertions pin the numbers + * measured on 1974e87e, the commit before the union (arm64 macOS and the + * LP64 C ABI generally): sizeof 120, the pair at 48 and 56, and + * pf_metrics_t::equity at 648. Neither the union nor its removal moved an + * offset or a size. */ static_assert(sizeof(pf_equity_stats_t) == 120, - "pf_equity_stats_t size moved; the deprecated-spelling union must not grow it"); + "pf_equity_stats_t size moved"); static_assert(offsetof(pf_equity_stats_t, sharpe_monthly) == 48, "pf_equity_stats_t::sharpe_monthly offset moved"); static_assert(offsetof(pf_equity_stats_t, sortino_monthly) == 56, "pf_equity_stats_t::sortino_monthly offset moved"); -/* The two assertions below name the deprecated spellings on purpose (they are - * what pins each alias to its generic storage), so they compile without the - * deprecation diagnostic every other use of those names draws. */ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -static_assert(offsetof(pf_equity_stats_t, sharpe_tv) == offsetof(pf_equity_stats_t, sharpe_monthly), - "the deprecated sharpe_tv spelling must name the sharpe_monthly storage"); -static_assert(offsetof(pf_equity_stats_t, sortino_tv) == offsetof(pf_equity_stats_t, sortino_monthly), - "the deprecated sortino_tv spelling must name the sortino_monthly storage"); -#pragma GCC diagnostic pop static_assert(offsetof(pf_equity_stats_t, sharpe_bar) == 64, "pf_equity_stats_t::sharpe_bar offset moved"); static_assert(offsetof(pf_equity_stats_t, open_pl) == 112, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e13cc48f..0fab8ecb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2179,29 +2179,20 @@ if(PINEFORGE_ENABLE_COVERAGE) endif() pineforge_add_twin_test(test_c_abi test_c_abi) -# R5 lane F6: the P2c historical spellings -- pf_equity_stats_t::sharpe_tv / -# sortino_tv and exit_legs::Domain::Coof / MagnifierCoof -- are -# compiler-deprecated aliases (ADR-0001 "Deprecated public spellings"). These -# TUs name them on purpose: test_c_abi exercises the C alias, and the two -# twins keep their frozen base CHECK texts verbatim. They build without the -# diagnostic; test_deprecated_public_spellings holds that every other use -# draws it, that the generic spellings compile clean, and that the aliases -# still compile. -foreach(_pf_deprecated_spelling_test - test_c_abi test_metrics_l4d test_exit_lifecycle_clock_l4c) - if(TARGET ${_pf_deprecated_spelling_test}) - target_compile_options(${_pf_deprecated_spelling_test} - PRIVATE -Wno-deprecated-declarations) - endif() -endforeach() -add_test(NAME test_deprecated_public_spellings +# Lane REL10: the four spellings ADR-0001 "Deprecated public spellings" +# scheduled for removal -- pf_equity_stats_t::sharpe_tv / sortino_tv and +# exit_legs::Domain::Coof / MagnifierCoof -- are gone at 1.0. The row compiles +# their 1.0 spellings clean (-Werror) in strict C99 and in C++17, and each +# removed spelling alone, which must fail with the unknown-member diagnostic +# that names it (R5 lane F6 registered this row as the deprecation check). +add_test(NAME test_removed_public_spellings COMMAND ${CMAKE_COMMAND} -DPF_CC=${CMAKE_C_COMPILER} -DPF_CXX=${CMAKE_CXX_COMPILER} "-DPF_INCLUDES=${PROJECT_SOURCE_DIR}/include|${PROJECT_BINARY_DIR}/include" -DPF_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} - -DPF_WORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/deprecated_spellings - -P ${CMAKE_CURRENT_SOURCE_DIR}/deprecated_spellings.cmake) + -DPF_WORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/removed_spellings + -P ${CMAKE_CURRENT_SOURCE_DIR}/removed_spellings.cmake) # R5 L13: the C-level native host API. One ctest row, two translation units. # test_native_c_api.c is compiled by the C compiler, so any C++ syntax leaking @@ -2241,20 +2232,16 @@ target_compile_options(test_native_c_api_int23_header PRIVATE -UNDEBUG) pineforge_add_twin_test(test_native_c_api_int23_header test_native_c_api_int23_header) # R5 lane F4: the public C headers as a strict C99 consumer compiles them -- -# ADR-0001's P2c portability note. The one C11 construct they hold, the -# anonymous union behind sharpe_tv / sortino_tv, is PF_ANONYMOUS_UNION; with -# extensions off and -pedantic-errors, any other C11-only construct in +# extensions off and -pedantic-errors, so any C11-only construct in # or is a build failure. -# The row executes the alias and one native C run. +# The one C11 construct they held, the anonymous union behind P2c's +# sharpe_tv / sortino_tv aliases, left with those spellings at 1.0 (lane +# REL10). The row checks the equity offsets and executes one native C run. add_executable(test_native_c_api_c99 test_native_c_api_c99.c) set_target_properties(test_native_c_api_c99 PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON C_EXTENSIONS OFF LINKER_LANGUAGE CXX) target_link_libraries(test_native_c_api_c99 PRIVATE pineforge) -# It reads P2c's historical spellings (sharpe_tv, sortino_tv) on purpose, to -# hold that the aliases share storage, so like test_c_abi it builds without -# their deprecation diagnostic. -target_compile_options(test_native_c_api_c99 PRIVATE -UNDEBUG -pedantic-errors -Wall -Wextra - -Wno-deprecated-declarations) +target_compile_options(test_native_c_api_c99 PRIVATE -UNDEBUG -pedantic-errors -Wall -Wextra) if(PINEFORGE_ENABLE_COVERAGE) target_compile_options(test_native_c_api_c99 PRIVATE ${_pf_cov_test_flags}) endif() diff --git a/tests/deprecated_spellings.cmake b/tests/deprecated_spellings.cmake deleted file mode 100644 index 5689f6be..00000000 --- a/tests/deprecated_spellings.cmake +++ /dev/null @@ -1,119 +0,0 @@ -# R5 lane F6 item 4: the P2c historical spellings are compiler-deprecated. -# -# ADR-0001 "Deprecated public spellings" keeps four old names as -# value-identical aliases until their ABI's next epoch: -# pf_equity_stats_t::sharpe_tv / sortino_tv (public C ABI, PF_ABI_VERSION 5) -# and exit_legs::Domain::Coof / MagnifierCoof (lifecycle_v2). Until this -# lane the deprecation was documentation only: a consumer compiled the old -# names silently. This row holds the three facts a consumer relies on: -# -# 1. the generic spellings compile clean under -# -Werror=deprecated-declarations, in C (C11) and in C++ (C++17); -# 2. each historical spelling draws the compiler's deprecation -# diagnostic, so under -Werror=deprecated-declarations the same probe -# fails, naming it; -# 3. with -Wno-deprecated-declarations the historical spellings still -# compile: an alias, never a break (the twin-parity-frozen suites that -# keep their base CHECK texts build exactly that way). -# -# cmake -DPF_CC= -DPF_CXX= -DPF_INCLUDES= -# -DPF_SOURCE_DIR= -DPF_WORK_DIR= -P deprecated_spellings.cmake - -cmake_minimum_required(VERSION 3.16) - -foreach(required PF_CC PF_CXX PF_INCLUDES PF_SOURCE_DIR PF_WORK_DIR) - if(NOT DEFINED ${required} OR "${${required}}" STREQUAL "") - message(FATAL_ERROR "deprecated_spellings.cmake: pass -D${required}=...") - endif() -endforeach() -file(MAKE_DIRECTORY "${PF_WORK_DIR}") -# `|`-separated: a `;` would not survive add_test's argument list. -string(REPLACE "|" ";" include_dirs "${PF_INCLUDES}") -set(include_flags) -foreach(dir IN LISTS include_dirs) - list(APPEND include_flags "-I${dir}") -endforeach() - -set(failures 0) -function(compile_probe label compiler standard source extra_flags out_status out_text) - execute_process( - COMMAND "${compiler}" "${standard}" -fsyntax-only ${include_flags} - ${extra_flags} "${PF_SOURCE_DIR}/${source}" - WORKING_DIRECTORY "${PF_WORK_DIR}" - RESULT_VARIABLE status - OUTPUT_VARIABLE text - ERROR_VARIABLE text - TIMEOUT 120) - set(${out_status} "${status}" PARENT_SCOPE) - set(${out_text} "${text}" PARENT_SCOPE) -endfunction() - -foreach(language C CXX) - if(language STREQUAL "C") - set(compiler "${PF_CC}") - set(standard "-std=c11") - set(source deprecated_spellings_probe.c) - set(names sharpe_tv sortino_tv) - else() - set(compiler "${PF_CXX}") - set(standard "-std=c++17") - set(source deprecated_spellings_probe.cpp) - set(names Coof MagnifierCoof sharpe_tv sortino_tv) - endif() - - compile_probe(generic "${compiler}" "${standard}" "${source}" - "-Werror=deprecated-declarations" status text) - if(NOT status STREQUAL "0") - message("${text}") - message(SEND_ERROR "${language}: the generic spellings do not compile clean " - "under -Werror=deprecated-declarations (${status})") - math(EXPR failures "${failures} + 1") - else() - message(STATUS "${language}: generic spellings compile clean") - endif() - - compile_probe(historical "${compiler}" "${standard}" "${source}" - "-Werror=deprecated-declarations;-DPF_PROBE_HISTORICAL" status text) - if(status STREQUAL "0") - message(SEND_ERROR "${language}: the historical spellings (${names}) compiled " - "under -Werror=deprecated-declarations with no deprecation " - "diagnostic") - math(EXPR failures "${failures} + 1") - else() - string(REPLACE "\n" ";" lines "${text}") - foreach(name IN LISTS names) - set(found FALSE) - foreach(line IN LISTS lines) - if(line MATCHES "(^|[^A-Za-z0-9_])${name}([^A-Za-z0-9_]|$)" - AND line MATCHES "deprecated") - set(found TRUE) - break() - endif() - endforeach() - if(NOT found) - message("${text}") - message(SEND_ERROR "${language}: no deprecation diagnostic names ${name}") - math(EXPR failures "${failures} + 1") - else() - message(STATUS "${language}: ${name} is deprecated") - endif() - endforeach() - endif() - - compile_probe(escaped "${compiler}" "${standard}" "${source}" - "-Wno-deprecated-declarations;-DPF_PROBE_HISTORICAL" status text) - if(NOT status STREQUAL "0") - message("${text}") - message(SEND_ERROR "${language}: the historical spellings no longer compile " - "with -Wno-deprecated-declarations (${status}): an alias " - "was broken") - math(EXPR failures "${failures} + 1") - else() - message(STATUS "${language}: historical spellings still compile as aliases") - endif() -endforeach() - -if(failures GREATER 0) - message(FATAL_ERROR "deprecated public spellings: ${failures} failure(s)") -endif() -message(STATUS "deprecated public spellings: generic clean, historical deprecated, aliases intact") diff --git a/tests/deprecated_spellings_probe.c b/tests/deprecated_spellings_probe.c deleted file mode 100644 index 961eddbe..00000000 --- a/tests/deprecated_spellings_probe.c +++ /dev/null @@ -1,12 +0,0 @@ -/* R5 lane F6 item 4: a pure-C consumer of the P2c alias in pf_equity_stats_t. - * tests/deprecated_spellings.cmake compiles it -fsyntax-only three ways; see - * that script. PF_PROBE_HISTORICAL selects the deprecated spelling. */ -#include - -double deprecated_spellings_probe_c(const pf_equity_stats_t* stats) { -#ifdef PF_PROBE_HISTORICAL - return stats->sharpe_tv + stats->sortino_tv; -#else - return stats->sharpe_monthly + stats->sortino_monthly; -#endif -} diff --git a/tests/deprecated_spellings_probe.cpp b/tests/deprecated_spellings_probe.cpp deleted file mode 100644 index ba922cdb..00000000 --- a/tests/deprecated_spellings_probe.cpp +++ /dev/null @@ -1,18 +0,0 @@ -// R5 lane F6 item 4: a C++ consumer of the P2c aliases -- the two -// exit_legs::Domain enumerators and the pf_equity_stats_t fields. -// tests/deprecated_spellings.cmake compiles it -fsyntax-only three ways; see -// that script. PF_PROBE_HISTORICAL selects the deprecated spellings. -#include -#include - -int deprecated_spellings_probe_cpp(const pf_equity_stats_t& stats) { - using pineforge::exit_legs::Domain; -#ifdef PF_PROBE_HISTORICAL - return static_cast(Domain::Coof) + static_cast(Domain::MagnifierCoof) - + static_cast(stats.sharpe_tv + stats.sortino_tv); -#else - return static_cast(Domain::FillRecalc) - + static_cast(Domain::MagnifierFillRecalc) - + static_cast(stats.sharpe_monthly + stats.sortino_monthly); -#endif -} diff --git a/tests/removed_spellings.cmake b/tests/removed_spellings.cmake new file mode 100644 index 00000000..0efa232b --- /dev/null +++ b/tests/removed_spellings.cmake @@ -0,0 +1,101 @@ +# Lane REL10: the four public spellings ADR-0001 "Deprecated public spellings" +# scheduled for removal are gone at 1.0 -- pf_equity_stats_t::sharpe_tv / +# sortino_tv (the public C header) and pineforge::exit_legs::Domain::Coof / +# MagnifierCoof (the standalone lifecycle_v1 C++ header). This row holds: +# +# 1. the 1.0 spellings (sharpe_monthly, sortino_monthly, FillRecalc, +# MagnifierFillRecalc) compile clean under -Werror, in strict C99 +# (-pedantic-errors) and in C++17; +# 2. each removed spelling, compiled alone, fails with the compiler's +# unknown-member diagnostic that names it. Deprecation warnings are +# suppressed for those compiles, so a spelling that was still declared, +# deprecated or not, would compile and fail this row. +# +# cmake -DPF_CC= -DPF_CXX= -DPF_INCLUDES= +# -DPF_SOURCE_DIR= -DPF_WORK_DIR= -P removed_spellings.cmake + +cmake_minimum_required(VERSION 3.16) + +foreach(required PF_CC PF_CXX PF_INCLUDES PF_SOURCE_DIR PF_WORK_DIR) + if(NOT DEFINED ${required} OR "${${required}}" STREQUAL "") + message(FATAL_ERROR "removed_spellings.cmake: pass -D${required}=...") + endif() +endforeach() +file(MAKE_DIRECTORY "${PF_WORK_DIR}") +# `|`-separated: a `;` would not survive add_test's argument list. +string(REPLACE "|" ";" include_dirs "${PF_INCLUDES}") +set(include_flags) +foreach(dir IN LISTS include_dirs) + list(APPEND include_flags "-I${dir}") +endforeach() + +function(compile_probe compiler source out_status out_text) + execute_process( + COMMAND "${compiler}" ${ARGN} -fsyntax-only ${include_flags} + "${PF_SOURCE_DIR}/${source}" + WORKING_DIRECTORY "${PF_WORK_DIR}" + RESULT_VARIABLE status + OUTPUT_VARIABLE text + ERROR_VARIABLE text + TIMEOUT 120) + set(${out_status} "${status}" PARENT_SCOPE) + set(${out_text} "${text}" PARENT_SCOPE) +endfunction() + +set(failures 0) +foreach(language C CXX) + if(language STREQUAL "C") + set(compiler "${PF_CC}") + set(flags -std=c99 -pedantic-errors) + set(source removed_spellings_probe.c) + set(names sharpe_tv sortino_tv) + else() + set(compiler "${PF_CXX}") + set(flags -std=c++17) + set(source removed_spellings_probe.cpp) + set(names Coof MagnifierCoof sharpe_tv sortino_tv) + endif() + + compile_probe("${compiler}" "${source}" status text ${flags} -Werror) + if(NOT status STREQUAL "0") + message("${text}") + message(SEND_ERROR "${language}: the 1.0 spellings do not compile clean " + "under -Werror (${status})") + math(EXPR failures "${failures} + 1") + else() + message(STATUS "${language}: the 1.0 spellings compile clean") + endif() + + foreach(name IN LISTS names) + compile_probe("${compiler}" "${source}" status text ${flags} + -Wno-deprecated-declarations "-DPF_PROBE_${name}") + set(found FALSE) + if(NOT status STREQUAL "0") + string(REPLACE "\n" ";" lines "${text}") + foreach(line IN LISTS lines) + # Clang: "no member named 'X' in ..."; GCC: "... has no member + # named 'X'" or "'X' is not a member of ...", its quotes + # typographic in a UTF-8 locale. + if(line MATCHES "(no member named|is not a member of)" + AND line MATCHES "(^|[^A-Za-z0-9_])${name}([^A-Za-z0-9_]|$)") + set(found TRUE) + break() + endif() + endforeach() + endif() + if(NOT found) + message("${text}") + message(SEND_ERROR "${language}: the removed spelling ${name} did not fail " + "with an unknown-member diagnostic naming it (${status})") + math(EXPR failures "${failures} + 1") + else() + message(STATUS "${language}: ${name} is gone (unknown member)") + endif() + endforeach() +endforeach() + +if(failures GREATER 0) + message(FATAL_ERROR "removed public spellings: ${failures} failure(s)") +endif() +message(STATUS "removed public spellings: the 1.0 spellings compile clean, " + "all four removed spellings are unknown") diff --git a/tests/removed_spellings_probe.c b/tests/removed_spellings_probe.c new file mode 100644 index 00000000..4c5e43c2 --- /dev/null +++ b/tests/removed_spellings_probe.c @@ -0,0 +1,17 @@ +/* Lane REL10: the C probe tests/removed_spellings.cmake compiles + * -fsyntax-only. With no PF_PROBE_* macro it names the 1.0 spellings of + * pf_equity_stats_t's two monthly ratios; PF_PROBE_ names one removed + * pre-1.0 spelling alone. */ +#include + +double removed_spellings_probe_c(const pf_equity_stats_t* stats); + +double removed_spellings_probe_c(const pf_equity_stats_t* stats) { +#if defined(PF_PROBE_sharpe_tv) + return stats->sharpe_tv; +#elif defined(PF_PROBE_sortino_tv) + return stats->sortino_tv; +#else + return stats->sharpe_monthly + stats->sortino_monthly; +#endif +} diff --git a/tests/removed_spellings_probe.cpp b/tests/removed_spellings_probe.cpp new file mode 100644 index 00000000..1ebe7f37 --- /dev/null +++ b/tests/removed_spellings_probe.cpp @@ -0,0 +1,23 @@ +// Lane REL10: the C++ probe tests/removed_spellings.cmake compiles +// -fsyntax-only. With no PF_PROBE_* macro it names the 1.0 spellings -- the +// two exit_legs::Domain enumerators and the pf_equity_stats_t fields; +// PF_PROBE_ names one removed pre-1.0 spelling alone. +#include +#include + +int removed_spellings_probe_cpp(const pf_equity_stats_t& stats) { + using pineforge::exit_legs::Domain; +#if defined(PF_PROBE_Coof) + return static_cast(Domain::Coof); +#elif defined(PF_PROBE_MagnifierCoof) + return static_cast(Domain::MagnifierCoof); +#elif defined(PF_PROBE_sharpe_tv) + return static_cast(stats.sharpe_tv); +#elif defined(PF_PROBE_sortino_tv) + return static_cast(stats.sortino_tv); +#else + return static_cast(Domain::FillRecalc) + + static_cast(Domain::MagnifierFillRecalc) + + static_cast(stats.sharpe_monthly + stats.sortino_monthly); +#endif +} diff --git a/tests/test_c_abi.c b/tests/test_c_abi.c index 5aa4881b..6cf7511e 100644 --- a/tests/test_c_abi.c +++ b/tests/test_c_abi.c @@ -46,38 +46,25 @@ int main(void) { memset(&m, 0, sizeof(m)); memset(&p, 0, sizeof(p)); m.all.num_trades = 42; - m.equity.sharpe_tv = 1.5; + m.equity.sharpe_monthly = 1.5; p.time_ms = 1700000000000LL; CHECK(m.all.num_trades == 42, "m.all.num_trades roundtrip"); - CHECK(m.equity.sharpe_tv == 1.5, "m.equity.sharpe_tv roundtrip"); + CHECK(m.equity.sharpe_monthly == 1.5, "m.equity.sharpe_monthly roundtrip"); CHECK(p.time_ms == 1700000000000LL, "p.time_ms roundtrip"); - /* R5 gap lane P2c: sharpe_monthly / sortino_monthly are the generic - * spelling of the fields this ABI shipped as sharpe_tv / sortino_tv. - * The old names are DEPRECATED aliases, removed at the next - * PF_ABI_VERSION (ADR-0001, "Deprecated public spellings"). Both - * spellings must name ONE double at ONE offset, in both directions, - * with no growth of the struct. Checked from C because that is the - * language of the contract. */ - CHECK(m.equity.sharpe_monthly == 1.5, - "m.equity.sharpe_monthly reads what sharpe_tv wrote"); + /* sharpe_monthly / sortino_monthly are plain doubles since their + * pre-1.0 spellings sharpe_tv / sortino_tv, P2c's aliases of the same + * storage, were removed for 1.0 (lane REL10; ADR-0001, "Deprecated + * public spellings"). Each still reads what it wrote, and the + * offsets and the size measured on 1974e87e, the commit before the + * alias, are unmoved. Checked from C because that is the language of + * the contract. */ m.equity.sharpe_monthly = -0.25; - CHECK(m.equity.sharpe_tv == -0.25, - "m.equity.sharpe_tv reads what sharpe_monthly wrote"); + CHECK(m.equity.sharpe_monthly == -0.25, + "m.equity.sharpe_monthly roundtrip"); m.equity.sortino_monthly = 3.75; - CHECK(m.equity.sortino_tv == 3.75, - "m.equity.sortino_tv reads what sortino_monthly wrote"); - m.equity.sortino_tv = -1.125; - CHECK(m.equity.sortino_monthly == -1.125, - "m.equity.sortino_monthly reads what sortino_tv wrote"); - CHECK(offsetof(pf_equity_stats_t, sharpe_monthly) - == offsetof(pf_equity_stats_t, sharpe_tv), - "sharpe_monthly / sharpe_tv share one offset"); - CHECK(offsetof(pf_equity_stats_t, sortino_monthly) - == offsetof(pf_equity_stats_t, sortino_tv), - "sortino_monthly / sortino_tv share one offset"); - /* The offsets and the size measured on 1974e87e, the commit before - * the alias: the union must not move or grow anything. */ + CHECK(m.equity.sortino_monthly == 3.75, + "m.equity.sortino_monthly roundtrip"); CHECK(offsetof(pf_equity_stats_t, sharpe_monthly) == 48, "pf_equity_stats_t::sharpe_monthly is still at offset 48"); CHECK(offsetof(pf_equity_stats_t, sortino_monthly) == 56, diff --git a/tests/test_exit_lifecycle_availability.cpp b/tests/test_exit_lifecycle_availability.cpp index ac35efaa..2d13690a 100644 --- a/tests/test_exit_lifecycle_availability.cpp +++ b/tests/test_exit_lifecycle_availability.cpp @@ -29,7 +29,7 @@ class Book:public pineforge::source::PineStrategyHost{ (leg==Leg::Stop||both)?(buy_?95:105):na,na,leg==Leg::Trail?1:na,leg==Leg::Trail?(buy_?110:90):na);} void action(Operation op,const std::string& id="X"){ auto& o=get(id);if(o.legs.last_action())exit_leg_event_seq_=std::max(exit_leg_event_seq_,o.legs.last_action()->cause.event); - Frame f{++exit_leg_event_seq_,bar_index_,coof_?Domain::Coof:Domain::Ordinary,Phase::Observation}; + Frame f{++exit_leg_event_seq_,bar_index_,coof_?Domain::FillRecalc:Domain::Ordinary,Phase::Observation}; Action a{o.legs.target(),o.legs.revision(),f,std::move(op)};CHECK(o.legs.apply(o.legs.target(),a)==Result::Applied); } double metric(){Bar bar{100,110,90,100,1,0};return internal::exit_order_earliest_path_metric_no_trail(bar,true,get(),position_side_,false,100,position_cycle_seq_,bar_index_);} @@ -83,7 +83,7 @@ class Chart:public pineforge::source::PineStrategyHost{ stop_?selected:(sibling_ready_?(buy_?9.90:10.26):(buy_?5:20))); auto& o=pending_orders_.back();o.leg_activation.bind({position_cycle_seq_,2,2}); if(o.legs.last_action())exit_leg_event_seq_=std::max(exit_leg_event_seq_,o.legs.last_action()->cause.event); - Frame f{++exit_leg_event_seq_,bar_index_,Domain::Coof,Phase::Observation};Leg leg=stop_?Leg::Stop:Leg::Limit; + Frame f{++exit_leg_event_seq_,bar_index_,Domain::FillRecalc,Phase::Observation};Leg leg=stop_?Leg::Stop:Leg::Limit; Operation op=suspend_?Operation{Suspend{{leg},{},{},{}}}:Operation{Cancel{{leg}}}; Action a{o.legs.target(),o.legs.revision(),f,op};CHECK(o.legs.apply(o.legs.target(),a)==Result::Applied); } diff --git a/tests/test_exit_lifecycle_clock.cpp b/tests/test_exit_lifecycle_clock.cpp index ded302dd..6875e524 100644 --- a/tests/test_exit_lifecycle_clock.cpp +++ b/tests/test_exit_lifecycle_clock.cpp @@ -29,8 +29,8 @@ Lifecycle window(Domain domain) { return x; } void observation_windows() { - for (Domain domain : {Domain::Ordinary, Domain::Coof, Domain::Magnifier, - Domain::MagnifierCoof, Domain::RawTicks}) { + for (Domain domain : {Domain::Ordinary, Domain::FillRecalc, Domain::Magnifier, + Domain::MagnifierFillRecalc, Domain::RawTicks}) { for (Fold fold : {Fold::Prefix, Fold::Continue}) { auto x = window(domain); const auto before = facts(x); for (int64_t bar : {9, 10}) for (Phase phase : {Phase::Observation, Phase::AfterMargin}) { @@ -75,8 +75,8 @@ Lifecycle staged(Domain domain) { CHECK(x.apply(x.target(), create) == Result::Applied); return x; } void completion_clocks() { - for (Domain domain : {Domain::Ordinary, Domain::Coof, Domain::Magnifier, - Domain::MagnifierCoof, Domain::RawTicks}) { + for (Domain domain : {Domain::Ordinary, Domain::FillRecalc, Domain::Magnifier, + Domain::MagnifierFillRecalc, Domain::RawTicks}) { auto x = staged(domain); const auto before = facts(x); const Frame occurrence{11, 10, domain, Phase::AfterMargin}; for (Frame receipt : {Frame{12, 9, domain, Phase::AfterMargin}, @@ -97,15 +97,15 @@ void completion_clocks() { } auto routed = staged(Domain::Ordinary); const auto before = facts(routed); auto inconsistent = action(routed, {12, 9, Domain::Ordinary, Phase::AfterMargin}, - CompleteBarrier{{11, 1, Domain::Coof, Phase::AfterMargin}, routed.release_barrier()}); + CompleteBarrier{{11, 1, Domain::FillRecalc, Phase::AfterMargin}, routed.release_barrier()}); CHECK(routed.apply(routed.target(), inconsistent) == Result::InvalidAction); CHECK(facts(routed) == before); - auto future_cross = action(routed, {12, 1, Domain::Coof, Phase::AfterMargin}, + auto future_cross = action(routed, {12, 1, Domain::FillRecalc, Phase::AfterMargin}, CompleteBarrier{{13, 11, Domain::Ordinary, Phase::AfterMargin}, routed.release_barrier()}); CHECK(routed.apply(routed.target(), future_cross) == Result::InvalidAction); CHECK(facts(routed) == before); - auto selected = action(routed, {12, 1, Domain::Coof, Phase::AfterMargin}, - CompleteBarrier{{11, 1, Domain::Coof, Phase::AfterMargin}, routed.release_barrier()}); + auto selected = action(routed, {12, 1, Domain::FillRecalc, Phase::AfterMargin}, + CompleteBarrier{{11, 1, Domain::FillRecalc, Phase::AfterMargin}, routed.release_barrier()}); CHECK(routed.apply(routed.target(), selected) == Result::Applied); CHECK(!routed.pending_replacement()); } diff --git a/tests/test_exit_lifecycle_clock_l4c.cpp b/tests/test_exit_lifecycle_clock_l4c.cpp index 6133a94d..139b908b 100644 --- a/tests/test_exit_lifecycle_clock_l4c.cpp +++ b/tests/test_exit_lifecycle_clock_l4c.cpp @@ -33,8 +33,8 @@ Lifecycle window(Domain domain) { return x; } void observation_windows() { - for (Domain domain : {Domain::Ordinary, Domain::Coof, Domain::Magnifier, - Domain::MagnifierCoof, Domain::RawTicks}) { + for (Domain domain : {Domain::Ordinary, Domain::FillRecalc, Domain::Magnifier, + Domain::MagnifierFillRecalc, Domain::RawTicks}) { for (Fold fold : {Fold::Prefix, Fold::Continue}) { auto x = window(domain); const auto before = facts(x); for (int64_t bar : {9, 10}) for (Phase phase : {Phase::Observation, Phase::AfterMargin}) { @@ -79,8 +79,8 @@ Lifecycle staged(Domain domain) { CHECK(x.apply(x.target(), create) == Result::Applied); return x; } void completion_clocks() { - for (Domain domain : {Domain::Ordinary, Domain::Coof, Domain::Magnifier, - Domain::MagnifierCoof, Domain::RawTicks}) { + for (Domain domain : {Domain::Ordinary, Domain::FillRecalc, Domain::Magnifier, + Domain::MagnifierFillRecalc, Domain::RawTicks}) { auto x = staged(domain); const auto before = facts(x); const Frame occurrence{11, 10, domain, Phase::AfterMargin}; for (Frame receipt : {Frame{12, 9, domain, Phase::AfterMargin}, @@ -101,15 +101,15 @@ void completion_clocks() { } auto routed = staged(Domain::Ordinary); const auto before = facts(routed); auto inconsistent = action(routed, {12, 9, Domain::Ordinary, Phase::AfterMargin}, - CompleteBarrier{{11, 1, Domain::Coof, Phase::AfterMargin}, routed.release_barrier()}); + CompleteBarrier{{11, 1, Domain::FillRecalc, Phase::AfterMargin}, routed.release_barrier()}); CHECK(routed.apply(routed.target(), inconsistent) == Result::InvalidAction); CHECK(facts(routed) == before); - auto future_cross = action(routed, {12, 1, Domain::Coof, Phase::AfterMargin}, + auto future_cross = action(routed, {12, 1, Domain::FillRecalc, Phase::AfterMargin}, CompleteBarrier{{13, 11, Domain::Ordinary, Phase::AfterMargin}, routed.release_barrier()}); CHECK(routed.apply(routed.target(), future_cross) == Result::InvalidAction); CHECK(facts(routed) == before); - auto selected = action(routed, {12, 1, Domain::Coof, Phase::AfterMargin}, - CompleteBarrier{{11, 1, Domain::Coof, Phase::AfterMargin}, routed.release_barrier()}); + auto selected = action(routed, {12, 1, Domain::FillRecalc, Phase::AfterMargin}, + CompleteBarrier{{11, 1, Domain::FillRecalc, Phase::AfterMargin}, routed.release_barrier()}); CHECK(routed.apply(routed.target(), selected) == Result::Applied); CHECK(!routed.pending_replacement()); } diff --git a/tests/test_metrics.cpp b/tests/test_metrics.cpp index 9c5082a0..4696b9d3 100644 --- a/tests/test_metrics.cpp +++ b/tests/test_metrics.cpp @@ -269,7 +269,7 @@ static pf_equity_point_t pt(int64_t ms, double eq) { static const int64_t kJan = 1706702400000LL, kFeb = 1709208000000LL, kMar = 1711886400000LL, kApr = 1714478400000LL; -static void test_equity_stats_sharpe_sortino_tv() { +static void test_equity_stats_sharpe_sortino_monthly() { std::printf("equity stats: TV monthly sharpe/sortino\n"); // equities 1000 -> 1100 -> 990 -> 1089 : monthly returns +10%, -10%, +10% pf_equity_point_t c[4] = { pt(kJan,1000), pt(kFeb,1100), pt(kMar,990), pt(kApr,1089) }; @@ -283,8 +283,8 @@ static void test_equity_stats_sharpe_sortino_tv() { // sortino numerator same; population downside dev vs rf: // d = min(0, -0.1 - rf)^2 / 3 => dd = |(-61/600)| / sqrt(3) // sortino = (mean - rf) / dd * sqrt(12) = 114/61 - CHECK(std::fabs(e.sharpe_tv - 0.95) < 1e-9); // 19/20 - CHECK(std::fabs(e.sortino_tv - 1.8688524590163935) < 1e-9); // 114/61 + CHECK(std::fabs(e.sharpe_monthly - 0.95) < 1e-9); // 19/20 + CHECK(std::fabs(e.sortino_monthly - 1.8688524590163935) < 1e-9); // 114/61 CHECK(std::fabs(e.buy_hold_return - 100.0) < 1e-12); // 1000*(110/100-1) CHECK(std::fabs(e.buy_hold_return_pct - 10.0) < 1e-12); CHECK(std::fabs(e.time_in_market_pct - 50.0) < 1e-12); // 2/4 @@ -306,7 +306,7 @@ static void test_equity_stats_drawdown_walk() { CHECK(std::fabs(e.max_equity_runup_pct - 200.0 / 9.0) < 1e-9); CHECK(std::fabs(e.recovery_factor - 100.0 / 300.0) < 1e-12); CHECK(!std::isnan(e.cagr)); - CHECK(std::isnan(e.sharpe_tv)); // single month bucket -> <2 returns + CHECK(std::isnan(e.sharpe_monthly)); // single month bucket -> <2 returns } static void test_equity_stats_edges() { @@ -314,13 +314,13 @@ static void test_equity_stats_edges() { pf_equity_point_t flat[3] = { pt(kJan,1000), pt(kFeb,1000), pt(kMar,1000) }; pf_equity_stats_t f = pineforge::metrics::compute_equity_stats( flat, 3, 1000.0, "", 100.0, 100.0, 0, 0.0); - CHECK(std::isnan(f.sharpe_tv)); // zero deviation + CHECK(std::isnan(f.sharpe_monthly)); // zero deviation CHECK(std::isnan(f.calmar)); // zero drawdown CHECK(std::isnan(f.recovery_factor)); CHECK(f.max_equity_drawdown == 0.0); pf_equity_stats_t z = pineforge::metrics::compute_equity_stats( nullptr, 0, 1000.0, "", kNaN_test(), kNaN_test(), 0, 0.0); - CHECK(std::isnan(z.sharpe_tv)); + CHECK(std::isnan(z.sharpe_monthly)); CHECK(std::isnan(z.cagr)); CHECK(std::isnan(z.buy_hold_return)); CHECK(z.max_equity_drawdown == 0.0); @@ -471,8 +471,8 @@ static void test_equity_stats_per_bar_oracle() { pf_equity_stats_t e = pineforge::metrics::compute_equity_stats( c, 5, 1000.0, "", /*first_open=*/100.0, /*last_close=*/100.0, /*bars_in_market=*/0, /*net_profit=*/19.99799); - // All 5 points in same UTC month -> single bucket -> sharpe_tv NaN. - CHECK(std::isnan(e.sharpe_tv)); + // All 5 points in same UTC month -> single bucket -> sharpe_monthly NaN. + CHECK(std::isnan(e.sharpe_monthly)); // Per-bar values from python oracle above. CHECK(std::fabs(e.sharpe_bar - 9.451108474837675) < 1e-9); CHECK(std::fabs(e.sortino_bar - 18.79927771509577) < 1e-9); @@ -514,14 +514,14 @@ static void test_equity_stats_non_utc_bucketing() { // UTC: first point in Feb -> 2 buckets (Feb, Mar) -> 1 return -> NaN. pf_equity_stats_t utc = pineforge::metrics::compute_equity_stats( c, 3, 1000.0, "", 100.0, 100.0, 0, -10.0); - CHECK(std::isnan(utc.sharpe_tv)); + CHECK(std::isnan(utc.sharpe_monthly)); // NY: first point in Jan -> 3 buckets (Jan, Feb, Mar) -> 2 returns -> finite. pf_equity_stats_t ny = pineforge::metrics::compute_equity_stats( c, 3, 1000.0, "America/New_York", 100.0, 100.0, 0, -10.0); - CHECK(!std::isnan(ny.sharpe_tv)); - CHECK(std::fabs(ny.sharpe_tv - (-0.04082482904638629)) < 1e-9); - CHECK(std::fabs(ny.sortino_tv - (-0.08031113910764517)) < 1e-9); + CHECK(!std::isnan(ny.sharpe_monthly)); + CHECK(std::fabs(ny.sharpe_monthly - (-0.04082482904638629)) < 1e-9); + CHECK(std::fabs(ny.sortino_monthly - (-0.08031113910764517)) < 1e-9); } // ---------- fill_report metrics integration (Task 6) ----------------------- @@ -569,7 +569,7 @@ static void test_report_empty_run() { s.fill_report(&rep); CHECK(rep.equity_curve == nullptr); CHECK(rep.equity_curve_len == 0); - CHECK(std::isnan(rep.metrics.equity.sharpe_tv)); + CHECK(std::isnan(rep.metrics.equity.sharpe_monthly)); CHECK(rep.metrics.all.num_trades == 0); BacktestEngine::free_report(&rep); } @@ -582,7 +582,7 @@ int main() { test_trade_stats_all(); test_trade_stats_filters_and_nan(); test_trade_stats_largest_pct_independent(); - test_equity_stats_sharpe_sortino_tv(); + test_equity_stats_sharpe_sortino_monthly(); test_equity_stats_drawdown_walk(); test_equity_stats_edges(); test_flat_strategy_bars_in_market(); diff --git a/tests/test_metrics_l4d.cpp b/tests/test_metrics_l4d.cpp index f987c040..7e21d12e 100644 --- a/tests/test_metrics_l4d.cpp +++ b/tests/test_metrics_l4d.cpp @@ -282,7 +282,7 @@ static pf_equity_point_t pt(int64_t ms, double eq) { static const int64_t kJan = 1706702400000LL, kFeb = 1709208000000LL, kMar = 1711886400000LL, kApr = 1714478400000LL; -static void test_equity_stats_sharpe_sortino_tv() { +static void test_equity_stats_sharpe_sortino_monthly() { std::printf("equity stats: TV monthly sharpe/sortino\n"); // equities 1000 -> 1100 -> 990 -> 1089 : monthly returns +10%, -10%, +10% pf_equity_point_t c[4] = { pt(kJan,1000), pt(kFeb,1100), pt(kMar,990), pt(kApr,1089) }; @@ -296,8 +296,8 @@ static void test_equity_stats_sharpe_sortino_tv() { // sortino numerator same; population downside dev vs rf: // d = min(0, -0.1 - rf)^2 / 3 => dd = |(-61/600)| / sqrt(3) // sortino = (mean - rf) / dd * sqrt(12) = 114/61 - CHECK(std::fabs(e.sharpe_tv - 0.95) < 1e-9); // 19/20 - CHECK(std::fabs(e.sortino_tv - 1.8688524590163935) < 1e-9); // 114/61 + CHECK(std::fabs(e.sharpe_monthly - 0.95) < 1e-9); // 19/20 + CHECK(std::fabs(e.sortino_monthly - 1.8688524590163935) < 1e-9); // 114/61 CHECK(std::fabs(e.buy_hold_return - 100.0) < 1e-12); // 1000*(110/100-1) CHECK(std::fabs(e.buy_hold_return_pct - 10.0) < 1e-12); CHECK(std::fabs(e.time_in_market_pct - 50.0) < 1e-12); // 2/4 @@ -319,7 +319,7 @@ static void test_equity_stats_drawdown_walk() { CHECK(std::fabs(e.max_equity_runup_pct - 200.0 / 9.0) < 1e-9); CHECK(std::fabs(e.recovery_factor - 100.0 / 300.0) < 1e-12); CHECK(!std::isnan(e.cagr)); - CHECK(std::isnan(e.sharpe_tv)); // single month bucket -> <2 returns + CHECK(std::isnan(e.sharpe_monthly)); // single month bucket -> <2 returns } static void test_equity_stats_edges() { @@ -327,13 +327,13 @@ static void test_equity_stats_edges() { pf_equity_point_t flat[3] = { pt(kJan,1000), pt(kFeb,1000), pt(kMar,1000) }; pf_equity_stats_t f = pineforge::metrics::compute_equity_stats( flat, 3, 1000.0, "", 100.0, 100.0, 0, 0.0); - CHECK(std::isnan(f.sharpe_tv)); // zero deviation + CHECK(std::isnan(f.sharpe_monthly)); // zero deviation CHECK(std::isnan(f.calmar)); // zero drawdown CHECK(std::isnan(f.recovery_factor)); CHECK(f.max_equity_drawdown == 0.0); pf_equity_stats_t z = pineforge::metrics::compute_equity_stats( nullptr, 0, 1000.0, "", kNaN_test(), kNaN_test(), 0, 0.0); - CHECK(std::isnan(z.sharpe_tv)); + CHECK(std::isnan(z.sharpe_monthly)); CHECK(std::isnan(z.cagr)); CHECK(std::isnan(z.buy_hold_return)); CHECK(z.max_equity_drawdown == 0.0); @@ -484,8 +484,8 @@ static void test_equity_stats_per_bar_oracle() { pf_equity_stats_t e = pineforge::metrics::compute_equity_stats( c, 5, 1000.0, "", /*first_open=*/100.0, /*last_close=*/100.0, /*bars_in_market=*/0, /*net_profit=*/19.99799); - // All 5 points in same UTC month -> single bucket -> sharpe_tv NaN. - CHECK(std::isnan(e.sharpe_tv)); + // All 5 points in same UTC month -> single bucket -> sharpe_monthly NaN. + CHECK(std::isnan(e.sharpe_monthly)); // Per-bar values from python oracle above. CHECK(std::fabs(e.sharpe_bar - 9.451108474837675) < 1e-9); CHECK(std::fabs(e.sortino_bar - 18.79927771509577) < 1e-9); @@ -527,14 +527,14 @@ static void test_equity_stats_non_utc_bucketing() { // UTC: first point in Feb -> 2 buckets (Feb, Mar) -> 1 return -> NaN. pf_equity_stats_t utc = pineforge::metrics::compute_equity_stats( c, 3, 1000.0, "", 100.0, 100.0, 0, -10.0); - CHECK(std::isnan(utc.sharpe_tv)); + CHECK(std::isnan(utc.sharpe_monthly)); // NY: first point in Jan -> 3 buckets (Jan, Feb, Mar) -> 2 returns -> finite. pf_equity_stats_t ny = pineforge::metrics::compute_equity_stats( c, 3, 1000.0, "America/New_York", 100.0, 100.0, 0, -10.0); - CHECK(!std::isnan(ny.sharpe_tv)); - CHECK(std::fabs(ny.sharpe_tv - (-0.04082482904638629)) < 1e-9); - CHECK(std::fabs(ny.sortino_tv - (-0.08031113910764517)) < 1e-9); + CHECK(!std::isnan(ny.sharpe_monthly)); + CHECK(std::fabs(ny.sharpe_monthly - (-0.04082482904638629)) < 1e-9); + CHECK(std::fabs(ny.sortino_monthly - (-0.08031113910764517)) < 1e-9); } // ---------- fill_report metrics integration (Task 6) ----------------------- @@ -582,7 +582,7 @@ static void test_report_empty_run() { s.fill_report(&rep); CHECK(rep.equity_curve == nullptr); CHECK(rep.equity_curve_len == 0); - CHECK(std::isnan(rep.metrics.equity.sharpe_tv)); + CHECK(std::isnan(rep.metrics.equity.sharpe_monthly)); CHECK(rep.metrics.all.num_trades == 0); BacktestEngine::free_report(&rep); } @@ -595,7 +595,7 @@ int main() { test_trade_stats_all(); test_trade_stats_filters_and_nan(); test_trade_stats_largest_pct_independent(); - test_equity_stats_sharpe_sortino_tv(); + test_equity_stats_sharpe_sortino_monthly(); test_equity_stats_drawdown_walk(); test_equity_stats_edges(); test_flat_strategy_bars_in_market(); diff --git a/tests/test_native_c_api_c99.c b/tests/test_native_c_api_c99.c index 1b9e1e7b..d32556d5 100644 --- a/tests/test_native_c_api_c99.c +++ b/tests/test_native_c_api_c99.c @@ -2,16 +2,13 @@ * test_native_c_api_c99.c -- the public C headers as a strict C99 consumer * sees them (R5 lane F4, item 9). * - * ADR-0001's P2c ruling keeps `sharpe_tv` / `sortino_tv` as deprecated - * spellings of `sharpe_monthly` / `sortino_monthly`: one double behind an - * anonymous union, which is a C11 construct. Its portability note promised a - * strict C99 consumer a warning; under -pedantic-errors it was an error - * (audit AUDIT3 §2.2 P2c). tests/CMakeLists.txt compiles this translation unit - * as C99 with extensions off and -pedantic-errors, so any C11-only construct - * in or fails the build. - * Its body executes what such a caller relies on: the alias reads one storage - * both ways, and a native C host configures through the typed enumerations, - * runs, and reads its words back by their C names. + * tests/CMakeLists.txt compiles this translation unit as C99 with extensions + * off and -pedantic-errors, so any C11-only construct in the public headers + * fails the build. The one they held, the anonymous union behind P2c's + * sharpe_tv / sortino_tv aliases, left with those spellings at 1.0 (lane + * REL10). Its body checks the equity offsets those spellings shared, and a + * native C host configures through the typed enumerations, runs, and reads + * its words back by their C names. * * No assert(): the Release gate defines NDEBUG and would no-op every row. */ @@ -65,19 +62,16 @@ static int c99_on_bar(void* user, const pf_bar_t* bar, const pf_native_decision_ return strategy_native_submit_v1(host->handle, &request, NULL, NULL) == PF_NATIVE_OK ? 0 : 1; } -static void check_deprecated_alias(void) { +static void check_equity_layout(void) { pf_equity_stats_t stats; - CHECK(offsetof(pf_equity_stats_t, sharpe_tv) == offsetof(pf_equity_stats_t, sharpe_monthly), - "sharpe_tv is not sharpe_monthly's storage"); - CHECK(offsetof(pf_equity_stats_t, sortino_tv) - == offsetof(pf_equity_stats_t, sortino_monthly), - "sortino_tv is not sortino_monthly's storage"); + CHECK(offsetof(pf_equity_stats_t, sharpe_monthly) == 48, "sharpe_monthly offset"); + CHECK(offsetof(pf_equity_stats_t, sortino_monthly) == 56, "sortino_monthly offset"); + CHECK(sizeof(pf_equity_stats_t) == 120, "equity stats size"); memset(&stats, 0, sizeof(stats)); stats.sharpe_monthly = 1.25; - stats.sortino_tv = -0.5; - CHECK(stats.sharpe_tv == 1.25, "a write through sharpe_monthly does not read as sharpe_tv"); - CHECK(stats.sortino_monthly == -0.5, - "a write through sortino_tv does not read as sortino_monthly"); + stats.sortino_monthly = -0.5; + CHECK(stats.sharpe_monthly == 1.25, "sharpe_monthly roundtrip"); + CHECK(stats.sortino_monthly == -0.5, "sortino_monthly roundtrip"); } static void check_native_run(void) { @@ -170,7 +164,7 @@ static void check_native_run(void) { } int main(void) { - check_deprecated_alias(); + check_equity_layout(); check_native_run(); printf("test_native_c_api_c99: %s\n", failures == 0 ? "ok" : "FAILED"); return failures == 0 ? 0 : 1; diff --git a/tutorial/run.py b/tutorial/run.py index 2bd6c66f..bacaaa89 100755 --- a/tutorial/run.py +++ b/tutorial/run.py @@ -61,10 +61,9 @@ class EquityStatsC(ctypes.Structure): # pf_equity_stats_t ("max_equity_runup_pct", ctypes.c_double), ("buy_hold_return", ctypes.c_double), ("buy_hold_return_pct", ctypes.c_double), - # Historical member names of the C fields sharpe_monthly / - # sortino_monthly; ctypes matches by offset, and these are also - # the serialized report keys (ADR-0001). - ("sharpe_tv", ctypes.c_double), ("sortino_tv", ctypes.c_double), + # The C field names. The JSON report keys of these two stay + # sharpe_tv / sortino_tv (ADR-0001, "Deprecated public spellings"). + ("sharpe_monthly", ctypes.c_double), ("sortino_monthly", ctypes.c_double), ("sharpe_bar", ctypes.c_double), ("sortino_bar", ctypes.c_double), ("cagr", ctypes.c_double), ("calmar", ctypes.c_double), ("recovery_factor", ctypes.c_double), From 4af4a9adfd2e120954b4a50c9f372ec1f1537e07 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:04:27 +0800 Subject: [PATCH 20/83] Build: a release-candidate VERSION configures, builds and installs, and every surface names it (lane REL10, item 3) VERSION may be X.Y.Z-rc.N (N >= 1). CMake's project(VERSION), the package's PineForge_VERSION, PINEFORGE_VERSION_MAJOR / _MINOR / _PATCH / _STRING and pf_version_get() stay the numeric X.Y.Z; PINEFORGE_VERSION_FULL, pf_version_string() and the package's new PineForge_VERSION_FULL carry the full version, -rc.N included. AUTO keeps git describe first (it already resolved a vX.Y.Z-rc.N tag); FILE and AUTO's no-describe fallback now keep the file's -rc.N instead of dropping it, and any other suffix fails configure. version.h.in keeps its six placeholders: codegen's test harness renders exactly those. The installed-package smoke consumer prints pf_version_string() and exits 1 unless the package, the installed header and the linked library name one version. scripts/ci_verify.py compares its print with VERSION, so the numeric print it replaced would have failed every profile on an -rc.N VERSION. test_cmake_version_source adds the candidate's archive, tag and past-the-tag cases, the final release after its candidate, and the refused prerelease spellings. No pkg-config file is added: integration-cmake.md says the CMake package is the one that ships. That page now pins 1.x: the package is SameMajorVersion, so a 0.x minimum does not find a 1.x install. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit dd6e9d843e24156e49be35ac4883f11ea2668116) INT26 pick note: clean pick. Two sentences are rewritten rather than kept: lane F2's statement of what the generated exposes for 0.14.0 (2fcbb52) now states the full and numeric versions, and lane FPC's smoke-consumer sentence (4c9404c) keeps its -ffp-contract=off check inside the rewritten smoke paragraph. --- cmake/PineForgeConfig.cmake.in | 5 ++ cmake/PineForgeVersion.cmake | 22 ++++++--- cmake/smoke_consumer/CMakeLists.txt | 16 ++++++ cmake/smoke_consumer/main.c | 31 +++++++++++- docs/ci.md | 8 ++- docs/pages/abi-stability.md | 25 +++++++--- docs/pages/install.md | 14 ++++-- docs/pages/integration-cmake.md | 25 ++++++---- include/pineforge/pineforge.h | 12 +++-- include/pineforge/version.h.in | 9 ++-- scripts/test_cmake_version_source.py | 73 ++++++++++++++++++++++++++++ 11 files changed, 202 insertions(+), 38 deletions(-) diff --git a/cmake/PineForgeConfig.cmake.in b/cmake/PineForgeConfig.cmake.in index eb13e027..96f8c4e1 100644 --- a/cmake/PineForgeConfig.cmake.in +++ b/cmake/PineForgeConfig.cmake.in @@ -1,5 +1,10 @@ @PACKAGE_INIT@ +# find_package compares MAJOR.MINOR.PATCH only (PineForge_VERSION). The full +# descriptor keeps a release candidate's -rc.N: the value of +# 's PINEFORGE_VERSION_FULL and of pf_version_string(). +set(PineForge_VERSION_FULL "@PINEFORGE_VERSION_FULL@") + include(CMakeFindDependencyMacro) find_dependency(Eigen3 3.3 REQUIRED) diff --git a/cmake/PineForgeVersion.cmake b/cmake/PineForgeVersion.cmake index ae61b158..9980aa43 100644 --- a/cmake/PineForgeVersion.cmake +++ b/cmake/PineForgeVersion.cmake @@ -3,7 +3,8 @@ # PINEFORGE_VERSION_SOURCE (CACHE STRING, AUTO|FILE): # AUTO (default) — historical behavior exactly: prefer # `git describe --tags --match 'v*' --abbrev=7 --dirty` for MMP/FULL -# when it matches MAJOR.MINOR.PATCH after stripping a leading 'v'; +# when it begins with MAJOR.MINOR.PATCH after stripping a leading 'v' +# (so a prerelease tag vX.Y.Z-rc.N gives MMP X.Y.Z and FULL X.Y.Z-rc.N); # otherwise use the VERSION file. Git SHA/dirty are observed the same way. # FILE — MMP and FULL always come from VERSION (stable release identity). # Git SHA/dirty are still observed as separate fields; checkout depth or @@ -17,22 +18,27 @@ # PINEFORGE_VERSION_FULL AUTO: describe-or-VERSION; FILE: VERSION # PINEFORGE_VERSION_GIT_SHA short sha or "unknown" # PINEFORGE_VERSION_DIRTY ON/OFF +# +# VERSION holds MAJOR.MINOR.PATCH or, for a release candidate, +# MAJOR.MINOR.PATCH-rc.N (N >= 1): MMP drops the -rc.N, FULL keeps it. set(PINEFORGE_VERSION_SOURCE "AUTO" CACHE STRING "Version identity source: AUTO (git describe preferred) or FILE (VERSION MMP/FULL)") set_property(CACHE PINEFORGE_VERSION_SOURCE PROPERTY STRINGS AUTO FILE) -function(_pineforge_read_version_file _out_mmp) +function(_pineforge_read_version_file _out_mmp _out_full) set(_vfile "${CMAKE_CURRENT_SOURCE_DIR}/VERSION") if(NOT EXISTS "${_vfile}") message(FATAL_ERROR "VERSION file missing at ${_vfile}") endif() file(READ "${_vfile}" _raw) string(STRIP "${_raw}" _raw) - if(NOT _raw MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+$") - message(FATAL_ERROR "VERSION file must be MAJOR.MINOR.PATCH (got '${_raw}')") + if(NOT _raw MATCHES "^([0-9]+\\.[0-9]+\\.[0-9]+)(-rc\\.[1-9][0-9]*)?$") + message(FATAL_ERROR + "VERSION file must be MAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH-rc.N (got '${_raw}')") endif() - set(${_out_mmp} "${_raw}" PARENT_SCOPE) + set(${_out_mmp} "${CMAKE_MATCH_1}" PARENT_SCOPE) + set(${_out_full} "${_raw}" PARENT_SCOPE) endfunction() function(pineforge_resolve_version) @@ -42,7 +48,7 @@ function(pineforge_resolve_version) "PINEFORGE_VERSION_SOURCE must be AUTO or FILE (got '${PINEFORGE_VERSION_SOURCE}')") endif() - _pineforge_read_version_file(_file_mmp) + _pineforge_read_version_file(_file_mmp _file_full) set(_git_full "") set(_git_sha "unknown") @@ -81,14 +87,14 @@ function(pineforge_resolve_version) if(PINEFORGE_VERSION_SOURCE STREQUAL "FILE") set(_mmp "${_file_mmp}") - set(_full "${_file_mmp}") + set(_full "${_file_full}") else() set(_mmp "${_file_mmp}") if(_git_full MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)") set(_mmp "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}") endif() if(NOT _git_full) - set(_git_full "${_mmp}") + set(_git_full "${_file_full}") endif() set(_full "${_git_full}") endif() diff --git a/cmake/smoke_consumer/CMakeLists.txt b/cmake/smoke_consumer/CMakeLists.txt index c9a175e2..6823484a 100644 --- a/cmake/smoke_consumer/CMakeLists.txt +++ b/cmake/smoke_consumer/CMakeLists.txt @@ -15,5 +15,21 @@ foreach(_pf_target PineForge::pineforge PineForge::kernel) endif() endforeach() +# The package's full descriptor keeps a release candidate's -rc.N, which +# find_package's PineForge_VERSION (MAJOR.MINOR.PATCH) cannot carry; it must +# begin with that MAJOR.MINOR.PATCH, and main.c holds it equal to the installed +# header's and the linked library's. +if(NOT DEFINED PineForge_VERSION_FULL) + message(FATAL_ERROR "PineForgeConfig.cmake does not set PineForge_VERSION_FULL") +endif() +string(LENGTH "${PineForge_VERSION}" _pf_mmp_length) +string(SUBSTRING "${PineForge_VERSION_FULL}" 0 ${_pf_mmp_length} _pf_full_head) +if(NOT _pf_full_head STREQUAL "${PineForge_VERSION}") + message(FATAL_ERROR "PineForge_VERSION_FULL (${PineForge_VERSION_FULL}) does not " + "begin with PineForge_VERSION (${PineForge_VERSION})") +endif() + add_executable(smoke_version main.c) target_link_libraries(smoke_version PRIVATE PineForge::pineforge) +target_compile_definitions(smoke_version PRIVATE + "PF_SMOKE_PACKAGE_VERSION_FULL=\"${PineForge_VERSION_FULL}\"") diff --git a/cmake/smoke_consumer/main.c b/cmake/smoke_consumer/main.c index 5b5cd824..cd2bb501 100644 --- a/cmake/smoke_consumer/main.c +++ b/cmake/smoke_consumer/main.c @@ -1,5 +1,10 @@ #include #include +#include + +#ifndef PF_SMOKE_PACKAGE_VERSION_FULL +#error "build through cmake/smoke_consumer/CMakeLists.txt, which passes the package's PineForge_VERSION_FULL" +#endif /* 1 when this TU fused x * x - p into one multiply-add. x * x - p is then the * rounding error of x * x (2^-60 here); rounded twice, as TradingView's runtime @@ -23,7 +28,31 @@ int main(void) "PineForge's -ffp-contract=off did not reach this TU\n"); return 1; } + /* One version, three places: the CMake package (PineForge_VERSION_FULL), + * the installed (PINEFORGE_VERSION_FULL) and the + * linked library (pf_version_string()), a release candidate's -rc.N + * included; pf_version_get() is its MAJOR.MINOR.PATCH. */ + const char* full = pf_version_string(); pf_version_t v = pf_version_get(); - printf("%d.%d.%d\n", (int)v.major, (int)v.minor, (int)v.patch); + char mmp[64]; + size_t mmp_length; + if (!full || strcmp(full, PINEFORGE_VERSION_FULL) != 0 + || strcmp(full, PF_SMOKE_PACKAGE_VERSION_FULL) != 0) { + fprintf(stderr, "smoke consumer: library %s, header %s and package %s " + "name different versions\n", + full ? full : "(null)", PINEFORGE_VERSION_FULL, + PF_SMOKE_PACKAGE_VERSION_FULL); + return 1; + } + snprintf(mmp, sizeof(mmp), "%d.%d.%d", (int)v.major, (int)v.minor, (int)v.patch); + mmp_length = strlen(mmp); + if (v.major != PINEFORGE_VERSION_MAJOR || v.minor != PINEFORGE_VERSION_MINOR + || v.patch != PINEFORGE_VERSION_PATCH || strncmp(full, mmp, mmp_length) != 0 + || (full[mmp_length] != '\0' && full[mmp_length] != '-')) { + fprintf(stderr, "smoke consumer: pf_version_get() %s is not the " + "MAJOR.MINOR.PATCH of %s\n", mmp, full); + return 1; + } + printf("%s\n", full); return 0; } diff --git a/docs/ci.md b/docs/ci.md index 0219e8c3..7e857c0b 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -142,8 +142,12 @@ checksum, workflow configuration, compiler/package environment and install path. Verification explicitly uses `PINEFORGE_VERSION_SOURCE=FILE`: package MMP/FULL identity comes from `VERSION`, while Git revision/dirty observations remain separate. Git tags and checkout depth cannot change the smoke-test expectation. -Ordinary CMake builds retain `AUTO`, the existing git-describe-first behavior. -No release tag or VERSION value is rewritten by verification. +The installed-package smoke test prints `pf_version_string()`, which must equal +`VERSION` exactly, a release candidate's `-rc.N` included, and it exits 1 when +the package's `PineForge_VERSION_FULL`, the installed `version.h` and the +library disagree (`cmake/smoke_consumer`). Ordinary CMake builds retain `AUTO`, +the existing git-describe-first behavior. No release tag or VERSION value is +rewritten by verification. The verifier fetches the pinned ABI commits `e60e571` (R2), `0e18690` (selected settlement, before exact reversal), `c3ed455` (native host v13), diff --git a/docs/pages/abi-stability.md b/docs/pages/abi-stability.md index d936a8bb..357ac30b 100644 --- a/docs/pages/abi-stability.md +++ b/docs/pages/abi-stability.md @@ -352,18 +352,31 @@ lift the missing surface into the public ABI. ## Version macros -The generated `` exposes, for this tree's `VERSION` -(0.14.0): +The generated `` exposes, for a `VERSION` of +`1.0.0-rc.1` built from a tarball or at that release candidate's tag: ```c -#define PINEFORGE_VERSION_MAJOR 0 -#define PINEFORGE_VERSION_MINOR 14 +#define PINEFORGE_VERSION_MAJOR 1 +#define PINEFORGE_VERSION_MINOR 0 #define PINEFORGE_VERSION_PATCH 0 -#define PINEFORGE_VERSION_STRING "0.14.0" -#define PINEFORGE_VERSION_FULL "0.14.0" /* or "0.14.0-3-gabc1234-dirty" */ +#define PINEFORGE_VERSION_STRING "1.0.0" +#define PINEFORGE_VERSION_FULL "1.0.0-rc.1" /* or "1.0.0-rc.1-3-gabc1234-dirty" */ #define PINEFORGE_GIT_SHA "" ``` +`PINEFORGE_VERSION_STRING` is MAJOR.MINOR.PATCH, as are CMake's +`project(VERSION)`, the package's `PineForge_VERSION` and #pf_version_get; a +release candidate's `-rc.N` lives in `PINEFORGE_VERSION_FULL`, the package's +`PineForge_VERSION_FULL` and #pf_version_string. A final release reads the +same with no `-rc.N`: `FULL` is `"1.0.0"`. The trailing `-N-gSHA[-dirty]` is +`git describe`'s, for a git checkout past its tag; a tarball build, or one +configured with `-DPINEFORGE_VERSION_SOURCE=FILE`, carries the `VERSION` file +exactly. `scripts/test_cmake_version_source.py` (CTest +`test_cmake_version_source`) holds the resolution, from `VERSION` and from a +tag; the installed-package smoke test that `scripts/ci_verify.py` runs +(`cmake/smoke_consumer`) holds that the package, the installed header and the +linked library name one full version, and that it is `VERSION` exactly. + Use these for compile-time gating of features added in later minors: ```c diff --git a/docs/pages/install.md b/docs/pages/install.md index eceda940..15c81742 100644 --- a/docs/pages/install.md +++ b/docs/pages/install.md @@ -81,10 +81,14 @@ A self-contained smoke test ships under `cmake/smoke_consumer/`: ```bash cmake -S cmake/smoke_consumer -B build-smoke cmake --build build-smoke -./build-smoke/smoke_version # prints the runtime version +./build-smoke/smoke_version # prints the runtime version string ``` -If this prints the version you installed (`0.14.0` for this tree's `VERSION`) -you're done. The configure step also checks that the package hands its -consumers `-ffp-contract=off`, and the program exits 1 instead if its own -multiply-add was fused. +If this prints the version you installed you're done: `pf_version_string()`, +which is the `VERSION` file exactly (a release candidate's `-rc.N` included) +for a tarball build or one configured with `-DPINEFORGE_VERSION_SOURCE=FILE`, +and `git describe`'s descriptor for a build from a git checkout. The program +exits 1 instead if the CMake package's `PineForge_VERSION_FULL`, the installed +`pineforge/version.h` and the linked library name different versions, or if +its own multiply-add was fused; the configure step also checks that the +package hands its consumers `-ffp-contract=off`. diff --git a/docs/pages/integration-cmake.md b/docs/pages/integration-cmake.md index 74230549..c592556d 100644 --- a/docs/pages/integration-cmake.md +++ b/docs/pages/integration-cmake.md @@ -11,7 +11,7 @@ projects pull it in with one `find_package` call. cmake_minimum_required(VERSION 3.16) project(my_strategy_runner LANGUAGES C) -find_package(PineForge 0.1 REQUIRED) +find_package(PineForge 1.0 REQUIRED) add_executable(runner runner.c) target_link_libraries(runner PRIVATE PineForge::pineforge) @@ -42,14 +42,21 @@ Or set `CMAKE_PREFIX_PATH=/opt/pineforge`. ## Version selection ```cmake -find_package(PineForge 0.14 REQUIRED) # 0.14.0 or any later 0.x.y -find_package(PineForge 0.14.0 EXACT REQUIRED) # pinned to this tree's VERSION +find_package(PineForge 1.0 REQUIRED) # 1.0.0 or any later 1.x.y +find_package(PineForge 1.0.0 EXACT REQUIRED) # exactly 1.0.0 ``` -The package config is `SameMajorVersion`, and within a major version -PineForge guarantees C ABI back-compat — see -[ABI stability](@ref abi_stability) — so a minimum `0.x` (any compatible -later 0.x.y) is the recommended pin. +The package config is `SameMajorVersion`: a minimum pins its major version, so +`find_package(PineForge 0.14 REQUIRED)` does not find a 1.x install, and +`1.0` does not find a 0.x one. Within a major version PineForge guarantees C +ABI back-compat — see [ABI stability](@ref abi_stability) — so a minimum `1.x` +(any compatible later 1.x.y) is the recommended pin. + +`find_package` compares MAJOR.MINOR.PATCH only. A release candidate installs +as `PineForge_VERSION` `1.0.0` with `PineForge_VERSION_FULL` `1.0.0-rc.1`, so +`find_package(PineForge 1.0.0 EXACT)` accepts `1.0.0-rc.1` as well; a project +that must tell a candidate from its release compares `PineForge_VERSION_FULL`, +the value `pf_version_string()` returns. ## Linking from a hand-written Makefile @@ -116,8 +123,8 @@ have it. ```cmake find_package(PineForge REQUIRED) -message(STATUS "PineForge ${PineForge_VERSION} from ${PineForge_DIR}") +message(STATUS "PineForge ${PineForge_VERSION_FULL} from ${PineForge_DIR}") ``` -If `PineForge_VERSION` doesn't match what you installed, your +If `PineForge_VERSION_FULL` doesn't match what you installed, your `CMAKE_PREFIX_PATH` or `PineForge_DIR` is pointing at a different copy. diff --git a/include/pineforge/pineforge.h b/include/pineforge/pineforge.h index f5ac00b9..8970976c 100644 --- a/include/pineforge/pineforge.h +++ b/include/pineforge/pineforge.h @@ -1432,11 +1432,15 @@ PF_API pf_version_t pf_version_get(void); /** @return Monotonic ABI version (see #PF_ABI_VERSION). */ PF_API int pf_abi_version(void); -/** Full git-derived version descriptor. +/** Full version descriptor, the `PINEFORGE_VERSION_FULL` of the generated + * `pineforge/version.h`. * - * Returns `"MAJOR.MINOR.PATCH[-N-gSHA[-dirty]]"` for git checkouts, or - * plain `"MAJOR.MINOR.PATCH"` for tarball builds. The pointer is to a - * static string with program lifetime; do not free. */ + * Returns `"MAJOR.MINOR.PATCH[-rc.N][-N-gSHA[-dirty]]"` for a tagged git + * checkout, or exactly the `VERSION` file (`"MAJOR.MINOR.PATCH"`, or + * `"MAJOR.MINOR.PATCH-rc.N"` for a release candidate) for a tarball build + * or one configured with `PINEFORGE_VERSION_SOURCE=FILE`. #pf_version_get + * carries only the numeric MAJOR.MINOR.PATCH. The pointer is to a static + * string with program lifetime; do not free. */ PF_API const char* pf_version_string(void); /** @} */ /* end of pf_version */ diff --git a/include/pineforge/version.h.in b/include/pineforge/version.h.in index 27118bd8..c2214877 100644 --- a/include/pineforge/version.h.in +++ b/include/pineforge/version.h.in @@ -9,11 +9,14 @@ #define PINEFORGE_VERSION_MINOR @PINEFORGE_VERSION_MINOR@ #define PINEFORGE_VERSION_PATCH @PINEFORGE_VERSION_PATCH@ -/* "MAJOR.MINOR.PATCH" — for ABI / find_package(PineForge X.Y) checks. */ +/* "MAJOR.MINOR.PATCH" — for ABI / find_package(PineForge X.Y) checks. A + * release candidate's -rc.N is not part of it: see PINEFORGE_VERSION_FULL. */ #define PINEFORGE_VERSION_STRING "@PINEFORGE_VERSION_MMP@" -/* "MAJOR.MINOR.PATCH[-N-gSHA[-dirty]]" — full git-derived descriptor. - * Identical to PINEFORGE_VERSION_STRING for tarball / non-git builds. */ +/* "MAJOR.MINOR.PATCH[-rc.N][-N-gSHA[-dirty]]" — the full descriptor: git + * describe's in a tagged git checkout; exactly the VERSION file, -rc.N + * included, for a tarball / non-git build or one configured with + * PINEFORGE_VERSION_SOURCE=FILE. */ #define PINEFORGE_VERSION_FULL "@PINEFORGE_VERSION_FULL@" /* Short git sha (7 chars) or "unknown" if built outside a git checkout. */ diff --git a/scripts/test_cmake_version_source.py b/scripts/test_cmake_version_source.py index 34f86466..2fb968b8 100644 --- a/scripts/test_cmake_version_source.py +++ b/scripts/test_cmake_version_source.py @@ -268,6 +268,79 @@ def test_invalid_mode_fails_clearly(self) -> None: self.assertIn("got '" + mode + "'", log) self.assertNotIn("Configuring done", log) + # Lane REL10: a release candidate's VERSION and tag. project(VERSION) takes + # MAJOR.MINOR.PATCH only (the probe calls it with MMP, so an -rc.N that + # leaked into MMP would fail configure); FULL keeps the -rc.N. + def assert_candidate(self, values: dict[str, str], full: str) -> None: + self.assertEqual(values["PINEFORGE_VERSION_MAJOR"], "1") + self.assertEqual(values["PINEFORGE_VERSION_MINOR"], "0") + self.assertEqual(values["PINEFORGE_VERSION_PATCH"], "0") + self.assertEqual(values["PINEFORGE_VERSION_MMP"], "1.0.0") + self.assertEqual(values["PINEFORGE_VERSION_FULL"], full) + + def test_release_candidate_archive_no_git(self) -> None: + source = self.base / "rc-archive" + self.write_tree(source, "1.0.0-rc.1") + self.assertFalse((source / ".git").exists()) + for label, defs in (("rc-archive-auto", ()), + ("rc-archive-file", ("-DPINEFORGE_VERSION_SOURCE=FILE",))): + with self.subTest(label=label): + values = self.probe(source, label, *defs) + self.assert_candidate(values, "1.0.0-rc.1") + self.assertEqual(values["PINEFORGE_VERSION_GIT_SHA"], "unknown") + + def test_release_candidate_tag(self) -> None: + source = self.base / "rc-tag" + self.write_tree(source, "1.0.0-rc.1") + self.init_repo(source) + self.git(source, "tag", "-a", "v1.0.0-rc.1", "-m", "Release v1.0.0-rc.1") + self.assertEqual(self.expected_describe(source), "1.0.0-rc.1") + for label, mode in (("rc-tag-auto", "AUTO"), ("rc-tag-file", "FILE")): + with self.subTest(mode=mode): + values = self.probe(source, label, "-DPINEFORGE_VERSION_SOURCE=" + mode) + self.assert_candidate(values, "1.0.0-rc.1") + self.assertEqual(values["PINEFORGE_VERSION_DIRTY"], "OFF") + source.joinpath("extra.txt").write_text("after the candidate\n", encoding="utf-8") + self.git(source, "add", "extra.txt") + self.git(source, "commit", "-m", "ahead of the candidate") + ahead = self.expected_describe(source) + self.assertTrue(ahead.startswith("1.0.0-rc.1-1-g"), ahead) + auto = self.probe(source, "rc-ahead-auto", "-DPINEFORGE_VERSION_SOURCE=AUTO") + file_mode = self.probe(source, "rc-ahead-file", "-DPINEFORGE_VERSION_SOURCE=FILE") + self.assert_candidate(auto, ahead) + self.assert_candidate(file_mode, "1.0.0-rc.1") + + def test_final_release_after_candidate(self) -> None: + source = self.base / "rc-final" + self.write_tree(source, "1.0.0-rc.1") + self.init_repo(source) + self.git(source, "tag", "-a", "v1.0.0-rc.1", "-m", "Release v1.0.0-rc.1") + source.joinpath("VERSION").write_text("1.0.0\n", encoding="utf-8") + self.git(source, "add", "VERSION") + self.git(source, "commit", "-m", "chore(release): v1.0.0") + self.git(source, "tag", "-a", "v1.0.0", "-m", "Release v1.0.0") + self.assertEqual(self.expected_describe(source), "1.0.0") + for label, mode in (("final-auto", "AUTO"), ("final-file", "FILE")): + with self.subTest(mode=mode): + values = self.probe(source, label, "-DPINEFORGE_VERSION_SOURCE=" + mode) + self.assert_candidate(values, "1.0.0") + + def test_other_prerelease_spellings_fail_clearly(self) -> None: + for index, version in enumerate(( + "1.0.0-rc.0", "1.0.0-rc", "1.0.0-rc1", "1.0.0-beta.1", + "1.0.0-rc.1.1", "1.0.0+build.1", "v1.0.0-rc.1", "1.0")): + with self.subTest(version=version): + source = self.base / ("bad-version-" + str(index)) + self.write_tree(source, version) + for mode in ("AUTO", "FILE"): + log = self.configure_fail( + source, "bad-version-" + str(index) + "-" + mode, + "-DPINEFORGE_VERSION_SOURCE=" + mode) + flat = " ".join(log.split()) # CMake wraps long messages + self.assertIn( + "VERSION file must be MAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH-rc.N", flat) + self.assertIn("(got '" + version + "')", flat) + def test_file_preserves_dirty_without_retagging_full(self) -> None: source = self.base / "dirty" self.write_tree(source, "3.2.1") From 2b826f9f0c277941087626925e6afdeb1cdff208 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:04:28 +0800 Subject: [PATCH 21/83] Release: release.yml takes an exact override, a dry run and release candidates; its version arithmetic is a tested script (lane REL10, item 4) scripts/release_version.py `next` computes current / next / tag / prerelease / previous from VERSION and the tags, and refuses anything but X.Y.Z or X.Y.Z-rc.N, a version that does not sort above VERSION, an existing tag, and a bump from a release candidate (its successor is always an exact override). `previous`, the release-notes base, orders tags by semver precedence: the YAML's `git tag --sort=-v:refname` put vX.Y.Z-rc.N above vX.Y.Z, so v1.0.1's notes would have started at a candidate. `check-install` verifies a staged install's version.h and package files before a tarball is packed. scripts/test_release_version.py holds all of it as a ci_preflight stage. release.yml: - `override` (exact version) and `dry_run` inputs beside `bump`; `override` reaches only the script, through env. - A release candidate is a GitHub prerelease, and the hub dispatch carries client_payload {version: vX.Y.Z[-rc.N], prerelease: true|false, run_id}. - The prebuilt job configures with PINEFORGE_VERSION_SOURCE=FILE and verifies the staged version; the tarball keeps its pineforge-vX.Y.Z[-rc.N]- name. - A dry run computes the version, builds, installs, verifies and packs every tarball (kept as workflow artifacts), and commits, tags, releases, uploads and dispatches nothing. - The App-token commit / tag / push step is unchanged; a dry run skips it. - The workflow publishes no container image, and its header no longer says it does: the GHCR image is the hub's. ci_preflight's workflow-lint stage now lints release.yml too (the prebuilt configure's intentional word split carries its shellcheck annotation). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 2bd8e2f3fb9307571b57167d20f892a25f53cd42) INT26 pick note: ci_preflight.py conflicted beside H-MEASURE's subset-cover stage; both stages stay and this commit's comment covers its two stages. --- .github/workflows/release.yml | 190 ++++++++++++++++++-------- scripts/ci_preflight.py | 8 +- scripts/release_version.py | 229 ++++++++++++++++++++++++++++++++ scripts/test_release_version.py | 198 +++++++++++++++++++++++++++ 4 files changed, 566 insertions(+), 59 deletions(-) create mode 100644 scripts/release_version.py create mode 100644 scripts/test_release_version.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f77e7571..0553593f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,18 +1,32 @@ name: Release -# Bump version, tag, build & push the Docker image to GHCR, and cut a -# GitHub Release with auto-generated notes — all from a single -# workflow_dispatch click. The choice of bump (major/minor/patch) is the -# only required input. +# Bump VERSION, tag, cut a GitHub Release with generated notes, attach the +# prebuilt static-lib tarballs, and notify the pineforge-release hub — all +# from a single workflow_dispatch click. This workflow publishes no container +# image: the GHCR image is the hub's, built from these tarballs. # -# Trigger: GitHub UI → Actions → Release → "Run workflow" → pick bump. +# Trigger: GitHub UI → Actions → Release → "Run workflow" → pick bump, or +# name the exact version in `override` (a release candidate is +# X.Y.Z-rc.N, then the final release is override X.Y.Z). Tick `dry_run` to +# rehearse: it computes the version, builds, installs, verifies and packs +# every tarball (kept as workflow artifacts), and commits, tags, releases, +# uploads and dispatches nothing. +# +# A release candidate (X.Y.Z-rc.N) is created as a GitHub prerelease, and +# the hub dispatch carries it: client_payload = {version: "vX.Y.Z-rc.N", +# prerelease: true, run_id}; a final release sends prerelease: false. The +# engine and codegen release one version number from 1.0.0; the hub pairs +# them (docs/pages/public-contract.md). +# +# The version arithmetic is scripts/release_version.py (unit tests: +# scripts/test_release_version.py, a ci_preflight stage), not YAML. # # Security notes: -# - The only user input is `bump`, a `choice` enum constrained to -# {patch,minor,major} — no free-text reaches a shell. Even so, it is -# piped through an `env:` block (BUMP) and matched in a `case`. +# - `bump` is a `choice` enum. `override` is free text: it reaches only +# scripts/release_version.py, through an `env:` block, which refuses +# anything but X.Y.Z or X.Y.Z-rc.N before any later step reads it. # - All other interpolations are GitHub-controlled context (repo, actor, -# sha, secrets) or values we computed ourselves from the VERSION file. +# sha, secrets) or values the script computed from VERSION and the tags. on: workflow_dispatch: @@ -26,6 +40,16 @@ on: - patch - minor - major + override: + description: 'Exact version, X.Y.Z or X.Y.Z-rc.N (for example 1.0.0-rc.1); wins over bump' + required: false + type: string + default: '' + dry_run: + description: 'Rehearse: build and verify the tarballs; commit, tag, release, upload and dispatch nothing' + required: false + type: boolean + default: false permissions: contents: write @@ -35,8 +59,9 @@ jobs: runs-on: ubuntu-latest outputs: - tag: ${{ steps.bump.outputs.tag }} - version: ${{ steps.bump.outputs.next }} + tag: ${{ steps.bump.outputs.tag }} + version: ${{ steps.bump.outputs.next }} + prerelease: ${{ steps.bump.outputs.prerelease }} steps: # Org GitHub App token (no PAT — never expires). Authorizes the version @@ -64,32 +89,20 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + # current / next / tag / prerelease / previous (the release-notes base), + # validated; see scripts/release_version.py. - name: Compute next version id: bump env: - BUMP: ${{ inputs.bump }} + BUMP: ${{ inputs.bump }} + OVERRIDE: ${{ inputs.override }} run: | set -euo pipefail - current="$(tr -d '[:space:]' < VERSION)" - if ! [[ "$current" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - echo "VERSION file malformed: '$current'" >&2 - exit 1 - fi - maj="${BASH_REMATCH[1]}" - min="${BASH_REMATCH[2]}" - pat="${BASH_REMATCH[3]}" - case "$BUMP" in - major) maj=$((maj + 1)); min=0; pat=0 ;; - minor) min=$((min + 1)); pat=0 ;; - patch) pat=$((pat + 1)) ;; - *) echo "unknown bump: $BUMP" >&2; exit 1 ;; - esac - next="${maj}.${min}.${pat}" - tag="v${next}" - echo "current=$current" >> "$GITHUB_OUTPUT" - echo "next=$next" >> "$GITHUB_OUTPUT" - echo "tag=$tag" >> "$GITHUB_OUTPUT" - echo "Bumping ${current} -> ${next} (${BUMP})" + git tag --list 'v*' > "${RUNNER_TEMP}/tags.txt" + python3 scripts/release_version.py next \ + --bump "${BUMP}" --override "${OVERRIDE}" \ + --tags-file "${RUNNER_TEMP}/tags.txt" \ + --github-output "${GITHUB_OUTPUT}" - name: Refuse if tag already exists env: @@ -101,6 +114,7 @@ jobs: fi - name: Write bumped VERSION, commit, tag, push + if: ${{ !inputs.dry_run }} env: NEXT: ${{ steps.bump.outputs.next }} TAG: ${{ steps.bump.outputs.tag }} @@ -113,25 +127,16 @@ jobs: git push origin HEAD git push origin "${TAG}" - - name: Resolve previous tag for changelog range - id: prev - env: - TAG: ${{ steps.bump.outputs.tag }} - run: | - set -euo pipefail - prev="$(git tag --list 'v*' --sort=-v:refname | grep -v "^${TAG}$" | head -n1 || true)" - echo "prev=${prev}" >> "$GITHUB_OUTPUT" - if [[ -n "$prev" ]]; then - echo "Changelog range: ${prev}..${TAG}" - else - echo "No prior tag -- full history will be used" - fi - + # The range starts at scripts/release_version.py's `previous`: the last + # tag below this release in semver order (git's v:refname sort puts + # X.Y.Z-rc.N above X.Y.Z), and the last final release for a final one. + # A dry run has no tag yet and ends the range at the dispatched commit. - name: Generate release notes id: notes env: - TAG: ${{ steps.bump.outputs.tag }} - PREV: ${{ steps.prev.outputs.prev }} + TAG: ${{ steps.bump.outputs.tag }} + PREV: ${{ steps.bump.outputs.previous }} + RANGE_END: ${{ inputs.dry_run && github.sha || steps.bump.outputs.tag }} run: | set -euo pipefail { @@ -140,11 +145,11 @@ jobs: if [[ -n "$PREV" ]]; then echo "### Changes since ${PREV}" echo - git log --pretty=format:'- %s (%h)' "${PREV}..${TAG}" + git log --pretty=format:'- %s (%h)' "${PREV}..${RANGE_END}" else echo "### Initial release" echo - git log --pretty=format:'- %s (%h)' "${TAG}" + git log --pretty=format:'- %s (%h)' "${RANGE_END}" fi echo echo @@ -155,16 +160,47 @@ jobs: } > RELEASE_NOTES.md echo "path=RELEASE_NOTES.md" >> "$GITHUB_OUTPUT" + # A release candidate is a GitHub prerelease, which GitHub never marks + # "Latest". - name: Create GitHub Release + if: ${{ !inputs.dry_run }} env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ steps.bump.outputs.tag }} - NOTES: ${{ steps.notes.outputs.path }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.bump.outputs.tag }} + NOTES: ${{ steps.notes.outputs.path }} + PRERELEASE: ${{ steps.bump.outputs.prerelease }} run: | + set -euo pipefail + prerelease=() + if [[ "${PRERELEASE}" == "true" ]]; then prerelease=(--prerelease); fi gh release create "${TAG}" \ --title "${TAG}" \ --notes-file "${NOTES}" \ - --verify-tag + --verify-tag \ + "${prerelease[@]}" + + - name: Dry run summary + if: ${{ inputs.dry_run }} + env: + CURRENT: ${{ steps.bump.outputs.current }} + NEXT: ${{ steps.bump.outputs.next }} + TAG: ${{ steps.bump.outputs.tag }} + PRERELEASE: ${{ steps.bump.outputs.prerelease }} + PREV: ${{ steps.bump.outputs.previous }} + NOTES: ${{ steps.notes.outputs.path }} + RUN_ID: ${{ github.run_id }} + run: | + set -euo pipefail + { + echo "## Dry run: ${CURRENT} -> ${NEXT}" + echo + echo "- would commit VERSION=${NEXT} and push the tag ${TAG}" + echo "- would create the GitHub release ${TAG} (prerelease: ${PRERELEASE}), notes since ${PREV:-the first commit}" + echo "- would dispatch engine-release to pineforge-release with client_payload {\"version\": \"${TAG}\", \"prerelease\": ${PRERELEASE}, \"run_id\": ${RUN_ID}}" + echo + cat "${NOTES}" + } >> "${GITHUB_STEP_SUMMARY}" + cat "${GITHUB_STEP_SUMMARY}" # Prebuilt static-lib + headers tarballs, one per (os, arch). Attached # to the GitHub Release created above. Tarball layout: @@ -199,11 +235,18 @@ jobs: runs-on: ${{ matrix.runner }} steps: + # A dry run has no tag yet: it builds the dispatched commit. - name: Checkout tag uses: actions/checkout@v4 with: - ref: ${{ needs.release.outputs.tag }} - fetch-depth: 0 # version helper wants tags for `git describe` + ref: ${{ inputs.dry_run && github.sha || needs.release.outputs.tag }} + fetch-depth: 0 # the version helper still observes the git sha + + - name: Write the dry run's VERSION + if: ${{ inputs.dry_run }} + env: + NEXT: ${{ needs.release.outputs.version }} + run: printf '%s\n' "${NEXT}" > VERSION - name: Install Eigen3 (Linux) if: runner.os == 'Linux' @@ -226,12 +269,17 @@ jobs: set -euo pipefail stage="stage/pineforge-${TAG}-${TRIPLE}" echo "STAGE_DIR=${stage}" >> "$GITHUB_ENV" + # FILE: the version identity is the VERSION file exactly, -rc.N + # included, however the checkout's tags describe it; a dry run's + # VERSION has no tag at all. # CMAKE_EXTRA is unquoted so multi-flag strings (e.g. mac fat # archive flag) split into separate argv entries. + # shellcheck disable=SC2086 cmake -B build \ -DCMAKE_BUILD_TYPE=Release \ -DPINEFORGE_BUILD_TESTS=OFF \ -DPINEFORGE_BUILD_TUTORIAL=OFF \ + -DPINEFORGE_VERSION_SOURCE=FILE \ -DCMAKE_INSTALL_PREFIX="${PWD}/${stage}" \ ${CMAKE_EXTRA} @@ -241,6 +289,15 @@ jobs: - name: Install to staging run: cmake --install build + # The generated version.h and the CMake package files must carry the + # full version, -rc.N included, and its numeric X.Y.Z. + - name: Verify the staged version + env: + NEXT: ${{ needs.release.outputs.version }} + run: | + python3 scripts/release_version.py check-install \ + --prefix "${STAGE_DIR}" --expect "${NEXT}" + - name: Bundle docs into tarball root run: | set -euo pipefail @@ -265,6 +322,7 @@ jobs: echo "sha256=${tarball}.sha256" >> "$GITHUB_OUTPUT" - name: Upload to release + if: ${{ !inputs.dry_run }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ needs.release.outputs.tag }} @@ -273,6 +331,16 @@ jobs: run: | gh release upload "${TAG}" "${TARBALL}" "${SHASUM}" --clobber + - name: Keep the dry run's tarball as a workflow artifact + if: ${{ inputs.dry_run }} + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.pack.outputs.tarball }} + path: | + ${{ steps.pack.outputs.tarball }} + ${{ steps.pack.outputs.sha256 }} + retention-days: 7 + # Notify the pineforge-release hub AFTER the tarballs are uploaded — the hub # builds FROM the engine static-lib tarball, so it must exist first. The hub # bumps its ENGINE_VERSION pin, rebuilds the combined image, and fans out to @@ -280,6 +348,7 @@ jobs: # never rebuilds. Credential is the org GitHub App (no PAT). notify-hub: needs: [release, prebuilt] + if: ${{ !inputs.dry_run }} runs-on: ubuntu-latest steps: - name: Mint org App token (hub dispatch) @@ -291,15 +360,22 @@ jobs: owner: pineforge-4pass repositories: pineforge-release + # client_payload = {version: vX.Y.Z[-rc.N], prerelease: true|false, + # run_id}. -F sends prerelease as a JSON boolean and run_id as a + # number. The hub never publishes a prerelease to its stable or + # `latest` channels and pairs the engine with codegen of the same + # version (lane REL10-HUB). - name: Dispatch engine-release -> pineforge-release env: GH_TOKEN: ${{ steps.app.outputs.token }} ENGINE_TAG: ${{ needs.release.outputs.tag }} + PRERELEASE: ${{ needs.release.outputs.prerelease }} RUN_ID: ${{ github.run_id }} run: | set -euo pipefail gh api repos/pineforge-4pass/pineforge-release/dispatches \ -f event_type=engine-release \ -F "client_payload[version]=${ENGINE_TAG}" \ + -F "client_payload[prerelease]=${PRERELEASE}" \ -F "client_payload[run_id]=${RUN_ID}" - echo "dispatched engine-release -> pineforge-release (${ENGINE_TAG})" + echo "dispatched engine-release -> pineforge-release (${ENGINE_TAG}, prerelease=${PRERELEASE})" diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index 3f5771f9..60e66096 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -175,7 +175,8 @@ def check_commands(source: Path) -> list[tuple]: str(source / '.github/workflows/native-live.yml'), str(source / '.github/workflows/corpus-parity.yml'), str(source / '.github/workflows/docs.yml'), - str(source / '.github/workflows/promote-baseline.yml')]), + str(source / '.github/workflows/promote-baseline.yml'), + str(source / '.github/workflows/release.yml')]), ('ci-workflow-contract', [sys.executable, str(source / 'scripts/ci_preflight.py'), '--check-ci-workflow']), ('docs-workflow-contract', [sys.executable, str(source / 'scripts/ci_preflight.py'), @@ -207,9 +208,12 @@ def check_commands(source: Path) -> list[tuple]: # every mutation of the recorded battery the full sweep caught. ('corpus-parity-subset-cover-tests', [sys.executable, str(source / 'scripts/test_corpus_parity_subset_cover.py')]), - # Lane REL10: the JSON report keys outlive the removed C spellings. + # Lane REL10: the JSON report keys outlive the removed C spellings, and + # release.yml's version arithmetic is a tested script. ('report-schema-key-tests', [sys.executable, str(source / 'scripts/test_report_schema_keys.py')]), + ('release-version-tests', + [sys.executable, str(source / 'scripts/test_release_version.py')]), ('design-inventory-tests', [sys.executable, str(source / 'scripts/test_check_design_inventory.py')]), ('design-inventory', diff --git a/scripts/release_version.py b/scripts/release_version.py new file mode 100644 index 00000000..54e32f67 --- /dev/null +++ b/scripts/release_version.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +"""The engine release's version arithmetic, for .github/workflows/release.yml. + +The workflow is the only writer of VERSION; it calls this script instead of +computing versions in YAML (lane REL10). + +``next`` validates VERSION and the requested release, and prints the typed +outputs the later jobs read:: + + python3 scripts/release_version.py next --bump patch|minor|major \\ + [--override X.Y.Z[-rc.N]] [--version-file VERSION] \\ + [--tags-file FILE] [--github-output FILE] + + current=0.14.0 + next=1.0.0-rc.1 + tag=v1.0.0-rc.1 + prerelease=true + previous=v0.13.1 + +A version is ``X.Y.Z`` or a release candidate ``X.Y.Z-rc.N`` (N >= 1, no +leading zeros anywhere): the one prerelease spelling the release hub pairs +exactly with codegen's. ``--override`` names the next version exactly and wins +over ``--bump``; a current release candidate has no bump, so its successor +(the next candidate, or the final release) is always an override. The next +version must sort above the current one and its tag must not exist yet. +``previous`` is the release-notes base: the highest existing tag below the +next version, or, for a final release, the highest final one, so that a final +release's notes cover its candidates too. Tags are ordered by semver +precedence, where ``X.Y.Z-rc.N`` sorts below ``X.Y.Z``; git's ``v:refname`` +sort puts it above. + +``check-install`` verifies a staged install before a tarball is packed: the +generated ``pineforge/version.h`` and the CMake package files must carry the +full version (``-rc.N`` included) and its numeric ``X.Y.Z``:: + + python3 scripts/release_version.py check-install --prefix DIR --expect X.Y.Z[-rc.N] +""" +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path +from typing import Iterable, NamedTuple + +_NUMBER = r"(0|[1-9][0-9]*)" +VERSION_RE = re.compile(rf"{_NUMBER}\.{_NUMBER}\.{_NUMBER}(?:-rc\.([1-9][0-9]*))?") + + +class Version(NamedTuple): + major: int + minor: int + patch: int + rc: int | None + + @property + def prerelease(self) -> bool: + return self.rc is not None + + @property + def numeric(self) -> str: + return f"{self.major}.{self.minor}.{self.patch}" + + def key(self) -> tuple[int, int, int, int, int]: + # Semver precedence: X.Y.Z-rc.N < X.Y.Z, and rc.N orders by N. + final = 1 if self.rc is None else 0 + return (self.major, self.minor, self.patch, final, self.rc or 0) + + def __str__(self) -> str: + return self.numeric + ("" if self.rc is None else f"-rc.{self.rc}") + + +class ReleaseError(ValueError): + """A request the release must refuse; the message names what to pass.""" + + +def parse_version(raw: str) -> Version: + match = VERSION_RE.fullmatch(raw) + if not match: + raise ReleaseError(f"invalid version {raw!r}: expected X.Y.Z or X.Y.Z-rc.N " + "(N >= 1, no leading zeros, no 'v')") + major, minor, patch = (int(match.group(i)) for i in (1, 2, 3)) + rc = int(match.group(4)) if match.group(4) else None + return Version(major, minor, patch, rc) + + +def parse_tags(tags: Iterable[str]) -> list[Version]: + """The ``vX.Y.Z[-rc.N]`` tags among ``tags``; anything else is ignored.""" + found = [] + for tag in tags: + tag = tag.strip() + if tag.startswith("v"): + try: + found.append(parse_version(tag[1:])) + except ReleaseError: + pass + return found + + +def next_version(current: str, bump: str, override: str = "") -> Version: + now = parse_version(current) + if override: + new = parse_version(override) + elif bump not in ("patch", "minor", "major"): + raise ReleaseError(f"invalid bump {bump!r}: expected patch, minor or major") + elif now.prerelease: + raise ReleaseError( + f"VERSION is the release candidate {now}: pass override, the next " + f"candidate (for example {now.numeric}-rc.{now.rc + 1}) or the final " + f"release {now.numeric}") + elif bump == "patch": + new = Version(now.major, now.minor, now.patch + 1, None) + elif bump == "minor": + new = Version(now.major, now.minor + 1, 0, None) + else: + new = Version(now.major + 1, 0, 0, None) + if new.key() <= now.key(): + raise ReleaseError(f"release {new} does not sort above VERSION {now}") + return new + + +def previous_tag(new: Version, tags: Iterable[Version]) -> str: + """The release-notes base for ``new``: see the module docstring.""" + below = [t for t in tags if t.key() < new.key() and (new.prerelease or not t.prerelease)] + return f"v{max(below, key=Version.key)}" if below else "" + + +def plan(current: str, bump: str, override: str, tags: Iterable[str]) -> dict[str, str]: + new = next_version(current, bump, override) + known = parse_tags(tags) + if any(t == new for t in known): + raise ReleaseError(f"tag v{new} already exists") + return { + "current": current, + "next": str(new), + "tag": f"v{new}", + "prerelease": "true" if new.prerelease else "false", + "previous": previous_tag(new, known), + } + + +def _define(text: str, name: str) -> str | None: + match = re.search(rf"^#define {name}\s+(.+?)\s*$", text, re.MULTILINE) + return match.group(1) if match else None + + +def check_install(prefix: Path, expect: str) -> list[str]: + """What a staged install gets wrong about ``expect``; empty when nothing.""" + version = parse_version(expect) + problems = [] + header = prefix / "include" / "pineforge" / "version.h" + config_dir = next(iter(sorted(prefix.glob("lib*/cmake/PineForge"))), None) + if not header.is_file(): + problems.append(f"{header} is missing") + else: + text = header.read_text(encoding="utf-8") + wanted = { + "PINEFORGE_VERSION_MAJOR": str(version.major), + "PINEFORGE_VERSION_MINOR": str(version.minor), + "PINEFORGE_VERSION_PATCH": str(version.patch), + "PINEFORGE_VERSION_STRING": f'"{version.numeric}"', + "PINEFORGE_VERSION_FULL": f'"{version}"', + } + for name, value in wanted.items(): + found = _define(text, name) + if found != value: + problems.append(f"{header}: {name} is {found!r}, expected {value!r}") + if config_dir is None: + problems.append(f"{prefix}/lib*/cmake/PineForge is missing") + else: + config = config_dir / "PineForgeConfig.cmake" + config_version = config_dir / "PineForgeConfigVersion.cmake" + if not config.is_file(): + problems.append(f"{config} is missing") + elif f'set(PineForge_VERSION_FULL "{version}")' not in config.read_text(encoding="utf-8"): + problems.append(f"{config} does not set PineForge_VERSION_FULL to {version}") + if not config_version.is_file(): + problems.append(f"{config_version} is missing") + elif f'set(PACKAGE_VERSION "{version.numeric}")' not in config_version.read_text( + encoding="utf-8"): + problems.append(f"{config_version} does not set PACKAGE_VERSION to {version.numeric}") + return problems + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + commands = parser.add_subparsers(dest="command", required=True) + step = commands.add_parser("next", help="compute and validate the next release") + step.add_argument("--bump", required=True, choices=("patch", "minor", "major")) + step.add_argument("--override", default="", + help="the exact next version, X.Y.Z or X.Y.Z-rc.N; wins over --bump") + step.add_argument("--version-file", type=Path, default=Path("VERSION")) + step.add_argument("--tags-file", type=Path, + help="`git tag --list 'v*'` output; refuses an existing tag and " + "sets previous") + step.add_argument("--github-output", type=Path, help="append key=value lines here") + check = commands.add_parser("check-install", help="verify a staged install's version") + check.add_argument("--prefix", type=Path, required=True) + check.add_argument("--expect", required=True) + args = parser.parse_args(argv) + + try: + if args.command == "check-install": + problems = check_install(args.prefix, args.expect) + for problem in problems: + print(f"release_version: {problem}", file=sys.stderr) + if problems: + return 1 + print(f"release_version: {args.prefix} carries {args.expect}") + return 0 + current = args.version_file.read_text(encoding="utf-8").strip() + tags = (args.tags_file.read_text(encoding="utf-8").splitlines() + if args.tags_file else []) + fields = plan(current, args.bump, args.override, tags) + except ReleaseError as error: + print(f"release_version: {error}", file=sys.stderr) + return 1 + for key, value in fields.items(): + print(f"{key}={value}") + if args.github_output: + with args.github_output.open("a", encoding="utf-8") as output: + for key, value in fields.items(): + output.write(f"{key}={value}\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/test_release_version.py b/scripts/test_release_version.py new file mode 100644 index 00000000..411c43bf --- /dev/null +++ b/scripts/test_release_version.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Unit tests for scripts/release_version.py, the release workflow's version +arithmetic (lane REL10). A ci_preflight stage runs them.""" +from __future__ import annotations + +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from release_version import ( # noqa: E402 + ReleaseError, check_install, next_version, parse_tags, parse_version, plan, + previous_tag) + +SCRIPT = Path(__file__).with_name("release_version.py") + +# The engine's tags as of lane REL10 (no v0.14.0: VERSION went 0.13.1 -> +# 0.14.0 without a release), plus the candidates and finals of a 1.0 cycle. +TAGS = ["v0.10.14", "v0.11.0", "v0.12.3", "v0.13.0", "v0.13.1"] + + +class ParseTests(unittest.TestCase): + def test_accepts_finals_and_release_candidates(self) -> None: + self.assertEqual(str(parse_version("0.14.0")), "0.14.0") + self.assertEqual(str(parse_version("1.0.0-rc.1")), "1.0.0-rc.1") + self.assertEqual(parse_version("1.0.0-rc.12").rc, 12) + self.assertEqual(parse_version("10.20.30").numeric, "10.20.30") + + def test_refuses_every_other_spelling(self) -> None: + for bad in ("v1.0.0", "01.0.0", "1.00.0", "1.0.0-rc.0", "1.0.0-rc.01", + "1.0.0-rc", "1.0.0rc1", "1.0.0-beta.1", "1.0.0-rc.1.1", + "1.0.0+build", "1.0", "1.0.0 ", "1.0.0\n", "1.0.0;true", + "$(id)", ""): + with self.subTest(bad=bad), self.assertRaises(ReleaseError): + parse_version(bad) + + def test_semver_precedence(self) -> None: + order = ["0.13.1", "0.14.0", "1.0.0-rc.1", "1.0.0-rc.2", "1.0.0-rc.10", + "1.0.0", "1.0.1-rc.1", "1.0.1", "1.1.0"] + keys = [parse_version(v).key() for v in order] + self.assertEqual(keys, sorted(keys)) + self.assertEqual(len(set(keys)), len(keys)) + + def test_tags_that_are_not_releases_are_ignored(self) -> None: + found = parse_tags(["v1.0.0", "1.0.0", "vnext", "v1.0", "v1.0.0-beta.1", " v0.13.1 "]) + self.assertEqual([str(v) for v in found], ["1.0.0", "0.13.1"]) + + +class NextVersionTests(unittest.TestCase): + def test_bumps_a_final(self) -> None: + self.assertEqual(str(next_version("0.14.0", "patch")), "0.14.1") + self.assertEqual(str(next_version("0.14.0", "minor")), "0.15.0") + self.assertEqual(str(next_version("0.14.0", "major")), "1.0.0") + + def test_override_wins_over_bump(self) -> None: + self.assertEqual(str(next_version("0.14.0", "patch", "1.0.0-rc.1")), "1.0.0-rc.1") + self.assertEqual(str(next_version("1.0.0-rc.1", "patch", "1.0.0-rc.2")), "1.0.0-rc.2") + self.assertEqual(str(next_version("1.0.0-rc.2", "major", "1.0.0")), "1.0.0") + + def test_a_candidate_has_no_bump(self) -> None: + for bump in ("patch", "minor", "major"): + with self.subTest(bump=bump), self.assertRaisesRegex( + ReleaseError, r"release candidate 1\.0\.0-rc\.1: pass override"): + next_version("1.0.0-rc.1", bump) + + def test_refuses_what_does_not_sort_above(self) -> None: + for current, override in (("1.0.0", "1.0.0"), ("1.0.0", "1.0.0-rc.3"), + ("1.0.0-rc.2", "1.0.0-rc.1"), ("1.0.0-rc.1", "1.0.0-rc.1"), + ("0.14.0", "0.13.2")): + with self.subTest(current=current, override=override), \ + self.assertRaisesRegex(ReleaseError, "does not sort above"): + next_version(current, "patch", override) + + def test_refuses_a_malformed_current_or_bump(self) -> None: + with self.assertRaises(ReleaseError): + next_version("0.14", "patch") + with self.assertRaises(ReleaseError): + next_version("0.14.0", "invented") + + +class PreviousTagTests(unittest.TestCase): + def tags(self, *extra: str) -> list: + return parse_tags(TAGS + list(extra)) + + def test_first_candidate_starts_at_the_last_release(self) -> None: + self.assertEqual(previous_tag(parse_version("1.0.0-rc.1"), self.tags()), "v0.13.1") + + def test_a_later_candidate_starts_at_the_candidate_before(self) -> None: + self.assertEqual(previous_tag(parse_version("1.0.0-rc.2"), self.tags("v1.0.0-rc.1")), + "v1.0.0-rc.1") + + def test_a_final_starts_at_the_last_final(self) -> None: + tags = self.tags("v1.0.0-rc.1", "v1.0.0-rc.2") + self.assertEqual(previous_tag(parse_version("1.0.0"), tags), "v0.13.1") + + def test_candidates_of_a_release_sort_below_it(self) -> None: + # git's --sort=-v:refname would answer v1.0.0-rc.2 here. + tags = self.tags("v1.0.0-rc.1", "v1.0.0-rc.2", "v1.0.0") + self.assertEqual(previous_tag(parse_version("1.0.1"), tags), "v1.0.0") + self.assertEqual(previous_tag(parse_version("1.0.1-rc.1"), tags), "v1.0.0") + + def test_no_earlier_tag(self) -> None: + self.assertEqual(previous_tag(parse_version("0.1.0"), []), "") + + +class PlanTests(unittest.TestCase): + def test_the_release_candidate_plan(self) -> None: + self.assertEqual(plan("0.14.0", "patch", "1.0.0-rc.1", TAGS), { + "current": "0.14.0", "next": "1.0.0-rc.1", "tag": "v1.0.0-rc.1", + "prerelease": "true", "previous": "v0.13.1"}) + + def test_the_final_plan(self) -> None: + self.assertEqual(plan("1.0.0-rc.1", "patch", "1.0.0", TAGS + ["v1.0.0-rc.1"]), { + "current": "1.0.0-rc.1", "next": "1.0.0", "tag": "v1.0.0", + "prerelease": "false", "previous": "v0.13.1"}) + + def test_refuses_an_existing_tag(self) -> None: + with self.assertRaisesRegex(ReleaseError, r"tag v1\.0\.0-rc\.1 already exists"): + plan("0.14.0", "patch", "1.0.0-rc.1", TAGS + ["v1.0.0-rc.1"]) + + +class CheckInstallTests(unittest.TestCase): + def stage(self, root: Path, header_full: str, config_full: str, package: str) -> Path: + header = root / "include" / "pineforge" / "version.h" + header.parent.mkdir(parents=True) + header.write_text( + "#define PINEFORGE_VERSION_MAJOR 1\n" + "#define PINEFORGE_VERSION_MINOR 0\n" + "#define PINEFORGE_VERSION_PATCH 0\n" + '#define PINEFORGE_VERSION_STRING "1.0.0"\n' + f'#define PINEFORGE_VERSION_FULL "{header_full}"\n' + '#define PINEFORGE_GIT_SHA "abc1234"\n', encoding="utf-8") + cmake = root / "lib" / "cmake" / "PineForge" + cmake.mkdir(parents=True) + (cmake / "PineForgeConfig.cmake").write_text( + f'set(PineForge_VERSION_FULL "{config_full}")\n', encoding="utf-8") + (cmake / "PineForgeConfigVersion.cmake").write_text( + f'set(PACKAGE_VERSION "{package}")\n', encoding="utf-8") + return root + + def test_a_candidate_install(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = self.stage(Path(directory), "1.0.0-rc.1", "1.0.0-rc.1", "1.0.0") + self.assertEqual(check_install(root, "1.0.0-rc.1"), []) + + def test_a_dropped_prerelease_is_named(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = self.stage(Path(directory), "1.0.0", "1.0.0", "1.0.0") + problems = check_install(root, "1.0.0-rc.1") + self.assertEqual(len(problems), 2, problems) + self.assertIn("PINEFORGE_VERSION_FULL", problems[0]) + self.assertIn("PineForge_VERSION_FULL", problems[1]) + + def test_a_git_descriptor_is_not_the_release(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = self.stage(Path(directory), "1.0.0-rc.1-1-gabc1234", "1.0.0-rc.1", "1.0.0") + self.assertEqual(len(check_install(root, "1.0.0-rc.1")), 1) + + def test_a_missing_install(self) -> None: + with tempfile.TemporaryDirectory() as directory: + self.assertEqual(len(check_install(Path(directory), "1.0.0")), 2) + + +class CommandLineTests(unittest.TestCase): + def run_next(self, base: Path, *argv: str) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, str(SCRIPT), "next", "--version-file", str(base / "VERSION"), + "--tags-file", str(base / "tags"), "--github-output", str(base / "output"), + *argv], capture_output=True, text=True, check=False) + + def test_writes_the_outputs_the_workflow_reads(self) -> None: + with tempfile.TemporaryDirectory() as directory: + base = Path(directory) + (base / "VERSION").write_text("0.14.0\n", encoding="utf-8") + (base / "tags").write_text("\n".join(TAGS) + "\n", encoding="utf-8") + result = self.run_next(base, "--bump", "patch", "--override", "1.0.0-rc.1") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual((base / "output").read_text(encoding="utf-8"), + "current=0.14.0\nnext=1.0.0-rc.1\ntag=v1.0.0-rc.1\n" + "prerelease=true\nprevious=v0.13.1\n") + self.assertEqual((base / "VERSION").read_text(encoding="utf-8"), "0.14.0\n") + + def test_a_refusal_exits_1_and_writes_nothing(self) -> None: + with tempfile.TemporaryDirectory() as directory: + base = Path(directory) + (base / "VERSION").write_text("1.0.0-rc.1\n", encoding="utf-8") + (base / "tags").write_text("v1.0.0-rc.1\n", encoding="utf-8") + result = self.run_next(base, "--bump", "minor") + self.assertEqual(result.returncode, 1) + self.assertIn("pass override", result.stderr) + self.assertFalse((base / "output").exists()) + + +if __name__ == "__main__": + unittest.main() From ed57b3b2e97c9ba6330b44f4fb846d51370973c5 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:04:28 +0800 Subject: [PATCH 22/83] Docs: the public contract for 1.0 (lane REL10, item 2) docs/pages/public-contract.md states what the version number covers from 1.0.0, mirroring codegen's docs/PUBLIC_CONTRACT.md: versions and where the full X.Y.Z-rc.N appears; the C ABI (PF_ABI_VERSION 4 for 1.x, frozen layouts, append-only functions and enumerators); the native C++ API (source-compatible within 1.x, not binary-compatible); the script ABI epoch engine_script_run_v19 and the stability of its state-hash values; the 1.0 C-surface boundary (the exclusions listed in the native-engine page's "Driving the kernel from C", where lane H-DOCGATES's 1.0 table lands; C-SURFACE-2 planned for 1.1.0); and the pairing rule -- engine and codegen share one version number, the same X.Y.Z prerelease included, and the hub enforces it. Each rule names the checker or CTest row that holds it on this tree, and the parts no checker holds (a C signature kept by name, a C enumerator value against the previous release, pf_report_t against a 1.0.0 baseline, C++ source compatibility against a 1.0.0 baseline, the stream fingerprint's value) are named as such. Linked from README, docs/README, the site index, the CMake integration page and CONTRIBUTING's new release step. pineforge.h's two hash comments no longer say a fingerprint may change between builds: its recipe is the epoch's. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 5f423b54fa8954c705fafde171f98ec0adf577f6) --- CONTRIBUTING.md | 9 ++ README.md | 6 +- docs/Doxyfile | 1 + docs/README.md | 3 +- docs/pages/index.md | 1 + docs/pages/integration-cmake.md | 4 +- docs/pages/public-contract.md | 193 ++++++++++++++++++++++++++++++++ include/pineforge/pineforge.h | 15 ++- 8 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 docs/pages/public-contract.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e208bf3..e764d5d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -323,3 +323,12 @@ License 2.0 (the same license as the rest of the project). See 4. **Benchmark AGPL** — optional `benchmarks/` tooling installs AGPL-covered PineTS. Default CI stays on ctest only so a minimal clone is not forced to pull AGPL into the library build. +5. **Cut the release** — dispatch `.github/workflows/release.yml` (Actions → + Release → Run workflow); rehearse with `dry_run` first. A dry run computes + the version with `scripts/release_version.py`, builds, installs, verifies + and packs every prebuilt tarball, and commits, tags, releases, uploads and + dispatches nothing. A release candidate is `override` `X.Y.Z-rc.N`: a + GitHub prerelease, dispatched to the hub with `prerelease: true`. The final + release is `override` `X.Y.Z` (a candidate has no `bump`). From 1.0.0 the + engine and codegen release one version number, prerelease included; see + the [public contract](docs/pages/public-contract.md). diff --git a/README.md b/README.md index b34644d4..0ec4389e 100644 --- a/README.md +++ b/README.md @@ -431,6 +431,10 @@ TradingView ties some day-boundary logic (intraday order caps, session rollovers ## Public C ABI +What the version number promises from 1.0.0 — for this C ABI, the native C++ +API, the script ABI epoch and the pairing with codegen — is the +[public contract](docs/pages/public-contract.md). + A built strategy `.so` exposes 66 compiled-strategy `PF_API` declarations (58 runtime implementations plus eight generated exports) plus 43 native-host declarations: 109 `PF_API` exports in total. `nm -gU` also shows libc++'s @@ -580,7 +584,7 @@ docs/ coverage map, Pine v6 audit, Doxygen site (cdocs.pinefor cmake/ PineForgeConfig.cmake.in + the find_package smoke consumer ``` -Documentation: [C ABI reference](https://cdocs.pineforge.dev) · [Getting started](https://cdocs.pineforge.dev/getting_started.html) · [MACD tutorial](https://cdocs.pineforge.dev/tutorial_macd.html) · [Streaming](https://cdocs.pineforge.dev/streaming.html) · [Metrics reference](https://cdocs.pineforge.dev/metrics.html) · [FFI from Python](https://cdocs.pineforge.dev/ffi_python.html) · [Rust](https://cdocs.pineforge.dev/examples_rust.html) · [CMake integration](https://cdocs.pineforge.dev/integration_cmake.html) · [ABI stability](https://cdocs.pineforge.dev/abi_stability.html) · [Coverage](https://cdocs.pineforge.dev/coverage.html). The site rebuilds on every push to `main`. +Documentation: [C ABI reference](https://cdocs.pineforge.dev) · [Getting started](https://cdocs.pineforge.dev/getting_started.html) · [MACD tutorial](https://cdocs.pineforge.dev/tutorial_macd.html) · [Streaming](https://cdocs.pineforge.dev/streaming.html) · [Metrics reference](https://cdocs.pineforge.dev/metrics.html) · [FFI from Python](https://cdocs.pineforge.dev/ffi_python.html) · [Rust](https://cdocs.pineforge.dev/examples_rust.html) · [CMake integration](https://cdocs.pineforge.dev/integration_cmake.html) · [ABI stability](https://cdocs.pineforge.dev/abi_stability.html) · [Public contract](https://cdocs.pineforge.dev/public_contract.html) · [Coverage](https://cdocs.pineforge.dev/coverage.html). The site rebuilds on every push to `main`. --- diff --git a/docs/Doxyfile b/docs/Doxyfile index 4b639ea3..fffb7f61 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -141,6 +141,7 @@ INPUT = groups.dox \ pages/examples-magnifier.md \ pages/examples-rust.md \ pages/abi-stability.md \ + pages/public-contract.md \ pages/coverage.md \ pages/exit-leg-activation.md \ pages/quantity-intent.md \ diff --git a/docs/README.md b/docs/README.md index 8f91e9f7..c3490078 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,8 +18,9 @@ docs/ ├── coverage.md # the canonical Pine v6 coverage map ├── pine_v6_audit_master.md, pine_v6_coverage_detail.md │ # the per-builtin inventory the coverage map summarises -├── pages/ # the narrative pages (31 files; see the site index) +├── pages/ # the narrative pages (32 files; see the site index) │ ├── index.md # @mainpage, and the index of every page +│ ├── public-contract.md # what the version number promises from 1.0.0 │ ├── native-engine.md # the native host reference │ ├── pine-to-native.md # the Pine -> native migration map │ ├── contributing-llm.md # contributor onboarding written for an agent diff --git a/docs/pages/index.md b/docs/pages/index.md index 83979c6f..285bfb1f 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -219,6 +219,7 @@ Build the generated strategy module with the repository recipe (or | [Report schema](@ref report_schema) | `pf_report_t` field by field, including the equity curve. | | [Trading metrics reference](@ref metrics) | Every `pf_metrics_t` field: units, NaN rules, validation status. | | [ABI stability](@ref abi_stability) | The append-only guarantee, and the internal C++ epochs behind it. | +| [Public contract for 1.0](@ref public_contract) | What the version number promises from 1.0.0: the C ABI, the native C++ API, the script ABI epoch, the C-surface boundary, and the pairing with codegen. | | [ABI v4 live surface](@ref live_surface) | The default-off live accessors: abort, realtime tail, broker-state hash, pending-order mirror. | | [FFI from Python](@ref ffi_python) | A ctypes mirror of every POD in `pineforge.h`. | diff --git a/docs/pages/integration-cmake.md b/docs/pages/integration-cmake.md index c592556d..5d5fa616 100644 --- a/docs/pages/integration-cmake.md +++ b/docs/pages/integration-cmake.md @@ -56,7 +56,9 @@ ABI back-compat — see [ABI stability](@ref abi_stability) — so a minimum `1. as `PineForge_VERSION` `1.0.0` with `PineForge_VERSION_FULL` `1.0.0-rc.1`, so `find_package(PineForge 1.0.0 EXACT)` accepts `1.0.0-rc.1` as well; a project that must tell a candidate from its release compares `PineForge_VERSION_FULL`, -the value `pf_version_string()` returns. +the value `pf_version_string()` returns. Generated strategy code pairs with +exactly that full version of the engine (see +[Public contract](@ref public_contract)). ## Linking from a hand-written Makefile diff --git a/docs/pages/public-contract.md b/docs/pages/public-contract.md new file mode 100644 index 00000000..fb690acb --- /dev/null +++ b/docs/pages/public-contract.md @@ -0,0 +1,193 @@ +# Public contract for 1.0 {#public_contract} + +@tableofcontents + +From 1.0.0 the engine's version number is a semantic version over the surfaces +this page lists, and over nothing else: a PATCH release fixes behaviour, a +MINOR release adds to a surface, and only a MAJOR release removes or changes +one. Each rule below names the checker or the CTest row that holds it on this +tree; where no checker holds part of a rule, the page says so. The codegen +states its half in its own `docs/PUBLIC_CONTRACT.md` (pineforge-codegen); the +two releases pair as the last section below says. + +## Versions + +- A release's version is `X.Y.Z`, or `X.Y.Z-rc.N` (N from 1) for a release + candidate; its tag is `vX.Y.Z` or `vX.Y.Z-rc.N`. `release.yml` computes it + with `scripts/release_version.py`, which refuses any other spelling, a + version that does not sort above `VERSION` in semver order (`X.Y.Z-rc.N` + below `X.Y.Z`) and a tag that exists (`scripts/test_release_version.py`, a + `ci_preflight` stage). +- A release candidate is a GitHub prerelease, and its dispatch to the release + hub carries `prerelease: true`: `.github/workflows/release.yml` reads both + from the script's `prerelease` output, which `scripts/test_release_version.py` + holds. The `workflow-lint` stage of `ci_preflight` lints the workflow's + syntax, expressions and shell, not its behaviour; a `dry_run` dispatch + rehearses it without publishing. +- CMake's `project(VERSION)`, the package's `PineForge_VERSION`, + `PINEFORGE_VERSION_MAJOR` / `_MINOR` / `_PATCH`, `PINEFORGE_VERSION_STRING` + and `pf_version_get()` carry the numeric `X.Y.Z`. The package's + `PineForge_VERSION_FULL`, `PINEFORGE_VERSION_FULL`, `pf_version_string()`, + the tag and the release tarball's name carry the full version, `-rc.N` + included (@ref abi_stability, "Version macros"). `test_cmake_version_source` + holds the resolution from `VERSION` and from a tag; the installed-package + smoke test `scripts/ci_verify.py` runs (`cmake/smoke_consumer`) holds that + the package, the installed header and the library name one full version, + `VERSION` exactly; and `release.yml` verifies every staged tarball with + `scripts/release_version.py check-install` before it packs it. + +## The C ABI + +The C ABI is `` and the native host API +`` it includes: the `PF_API` functions, the `pf_*` +structs and enumerations, and the `PF_*` constants. + +- **`PF_ABI_VERSION` is 4 for every 1.x release.** It versions the layouts a + caller allocates or strides over (`pf_report_t`, `pf_trade_t`, the metrics + structs), and a consumer checks `pf_abi_version() == PF_ABI_VERSION` before + it runs a strategy. `test_c_abi` checks that equality, `test_live_run_status` + pins the value 4, and `test_run_strategy_range_end` pins the Python + harness's `EXPECTED_PF_ABI` to it. +- **Functions are append-only.** No `PF_API` function is removed or renamed in + 1.x; new ones are added. `scripts/check_c_abi_runtime.py` (a source guard of + every `ci_verify` profile and of `ci_preflight`) pins the exact name sets + `EXPECTED_RUNTIME` and `EXPECTED_NATIVE_C_API` and the declaration counts, so + an unrecorded addition, removal or rename fails. It reads names only: a + signature change that keeps its name is caught only where a test calls the + function, the frozen v1 closure below among them. +- **Layouts are frozen.** A field of a published struct never moves, changes + type or disappears in 1.x. A size-prefixed native struct (`struct_size`, + `version`) grows only by an appended tail, and the runtime keeps accepting + every length it published (each `*_SIZE` constant). The `static_assert`s in + `src/c_abi.cpp` pin the size and field offsets of the structs the header + shares with the runtime — `pf_bar_t`, `pf_trade_tick_t`, `pf_trade_t`, + `pf_report_t`, `pf_security_diag_t`, `pf_trace_entry_t` — to their internal + C++ twins, and `pf_equity_stats_t` / `pf_metrics_t` to literal offsets and + sizes; `src/native_c_host.cpp` pins the published lengths of the native + structs that grew by tails; + `test_native_c_api_frozen_header` compiles the frozen `PF_NATIVE_API_VERSION` + 1 header closure (`tests/fixtures/native_c_api/v1`) and runs it against the + current runtime; `test_c_abi` and `test_native_c_api_c99` check the equity + offsets from C. These pins are against the internal twins and the frozen v1 + closure; no checker compares `pf_report_t` or `pf_trade_t` with a 1.0.0 + baseline. +- **Enumerators are append-only.** An enumerator keeps its value in 1.x; a new + one takes a new value. `scripts/check_native_c_api_surface.py` holds every + C enumeration of `native_c_api.h` and `pineforge.h` equal, value for value, + to its kernel twin (one named exclusion) or declares it C-only with a + reason, and + `src/c_abi.cpp`'s `static_assert`s pin the `pf_magnifier_distribution_t` + values. No checker compares a value with the previous release. + +## The C++ native API + +The native C++ API is the kernel's host surface: ``, +``, ``, +``, ``, +``, ``, +``, the header-only `` +and `` (@ref native_engine). + +- **Within 1.x it is source-compatible:** a host that compiles against 1.0.0's + native headers compiles against every later 1.x release. The top-level + examples (`examples/native`, 16 C++ and 2 C hosts) build and run as the + `example_*` CTest rows of the `release` and `kernel` profiles; + `scripts/check_native_include_independence.py` (the + `native-include-independence` stage of the `release`, `native` and `kernel` + profiles) compiles every native root header and every example against the + installed headers with the Pine source layer removed; + `scripts/check_native_c_api_surface.py` fails when a public + `NativeStrategyHost` member appears or disappears without its row in + `native_c_api.h`'s COVERAGE block; `scripts/check_native_cpp_versions.py` + pins every native type to its versioned inline namespace and the member + order of the structs it names. Each of them holds the current tree against + itself: no checker yet compiles a host written against 1.0.0's headers + against a later release, so across releases review holds the promise, until + a frozen 1.0.0 native header closure joins these rows the way + `tests/fixtures/native_c_api/v1` does for the C API. +- **It is not binary-compatible across releases.** Rebuild every C++ object + that includes an engine header — a native host, a generated strategy — + against the headers and the archive of the release it links. The C++ + layouts change inside an epoch; `engine.hpp` states the rule where the + script ABI epoch opens. + +## The script ABI epoch and state hashes + +- **The script ABI epoch is `engine_script_run_v19` for every 1.x release.** + It is the inline namespace of `BacktestEngine`, `NativeStrategyHost` and the + execution consumer, and `PINEFORGE_HAS_NATIVE_STRATEGY_HOST_V19` announces + it. `scripts/check_native_cpp_versions.py` and + `scripts/check_aggregate_cpp_versions.py` pin the one live epoch; the CTest + rows `test_script_cpp_abi`, `test_settlement_cpp_abi` and + `test_aggregate_cpp_versions_runtime` link callers built against the frozen + v18 headers (`host-fc7aad6`) and against the live ones, and require the + mismatched pairs to fail on the epoch-qualified + `BacktestEngine::broker_state_hash` symbol (`scripts/ci_verify.py` requires + their prepared receipts, so none of them skips there). +- **State-hash values are stable within the epoch.** The recipe of each state + hash — the broker-state hash (`strategy_broker_state_hash`, the per-bar + `pf_report_t::broker_state_hash` rows), the stream fingerprint + (`strategy_stream_state_hash`) and the native continuation hash — belongs to + the epoch, so from 1.0.0 every 1.x engine folds one state to one value, and + a new recipe is a new epoch and a major release. Values from builds before + 1.0.0 are not comparable. `scripts/check_aggregate_cpp_versions.py` and + `scripts/check_broker_state_hash_coverage.py` pin the domain tags + `pineforge-broker-state/v19` and `pineforge-source-adapter/v4` and the + stream fingerprint's version 19; witness rows pin values — + `test_native_host_hash_extension`, `test_native_lean_path`, + `test_native_match_hash_witness`, `test_native_continuation_view`, + `test_adapter_quiet_bar`, `test_publication_witness` among them. Two + limits: the native continuation hash folds the resolved timezone's + resource digest, so a tzdata release that rewrites a zone moves the value of + a run in that zone (`test_native_report_truth` states why it pins no raw + continuation value), and no row pins a `strategy_stream_state_hash` value + directly. + +## The 1.0 C-surface boundary + +The C surface is a documented subset of the native C++ API in 1.0, not its +twin. What a C host cannot reach, each exclusion with its reason, is listed in +@ref native_engine, section *Driving the kernel from C*. The `[--]` rows of +`native_c_api.h`'s COVERAGE block and the exclusions +`scripts/check_native_c_api_surface.py` declares hold that list: the checker +fails when a public `NativeStrategyHost` member has no row, or when a C +enumeration is neither twinned to its kernel enumeration nor declared C-only. +Four additions are planned for 1.1.0: a non-mutating execution preview, the +origin and label of an applied event, a closed-trade entry-comment accessor +and a replace-options word. Nothing in 1.0 promises C and C++ parity beyond +that list. + +## Pairing with codegen + +- **From 1.0.0 the engine and the codegen share one version number, and the + only supported pair is the same `X.Y.Z`, prerelease included:** engine + `v1.0.0-rc.1` with codegen `1.0.0-rc.1` (PyPI spells it `1.0.0rc1`), engine + `v1.0.0` with codegen `1.0.0`. Any other pair is unsupported: a release + candidate with its final release, two minors, or two builds that agree only + on `PF_ABI_VERSION`. Generated strategy code compiles against the engine's + internal C++ headers, which the C ABI does not cover and the script ABI + checks above version (@ref abi_stability, "What's *not* guaranteed"). + Regenerate the C++ and relink the strategy library on every pair change. +- **The release hub enforces it.** `release.yml` dispatches `engine-release` + to pineforge-release with `client_payload` `{version: "vX.Y.Z[-rc.N]", + prerelease: true|false, run_id}`; codegen dispatches its own version. The + hub builds and publishes an image only for an engine and a codegen of the + same version, and publishes a prerelease pair under its exact version, never + a stable or `latest` tag; that check lives in the pineforge-release + repository, not on this tree. + +## Compatibility boundary + +Not covered by the version number: + +- the C++ headers generated strategies compile against (`engine.hpp`, + `ta.hpp`, the Pine source layer under `include/pineforge/source/` and + `include/pineforge/compat/`), which the pairing rule covers instead; +- any symbol without `PF_API`, and the text of log lines; +- the Python harnesses under `scripts/`, `tutorial/` and `benchmarks/`. + +The JSON report keys of `docker/run_json.py` are the release image's schema +(@ref report_schema); `scripts/test_report_schema_keys.py` pins the +`metrics.equity` keys, which kept `sharpe_tv` / `sortino_tv` when the C +fields' pre-1.0 spellings were removed. What changed at 1.0 for a 0.x user is +in the repository's `CHANGELOG.md`. diff --git a/include/pineforge/pineforge.h b/include/pineforge/pineforge.h index 8970976c..55098ae5 100644 --- a/include/pineforge/pineforge.h +++ b/include/pineforge/pineforge.h @@ -775,8 +775,10 @@ PF_API void strategy_stream_order_actions_clear(pf_strategy_t s); * Excludes the consumable queue and arbitrary private strategy members. * This is a replay check, not a complete state snapshot or cryptographic hash. * Fresh replay must use deterministic strategy code, the same pinned engine - * build and configuration. Fingerprint representations may change between builds. - * Returns 0 for NULL. */ + * build and configuration. The fingerprint's recipe belongs to the script + * ABI epoch: from 1.0.0 it is fixed for every 1.x engine + * (engine_script_run_v19), and a new recipe is a new epoch; a value from a + * build before 1.0.0 is not comparable. Returns 0 for NULL. */ PF_API uint64_t strategy_stream_state_hash(pf_strategy_t s); /** Push one normalized realtime trade. Returns 0 on success, -1 on failure. */ @@ -995,10 +997,11 @@ PF_API void strategy_set_broker_state_hash_recording(pf_strategy_t s, int on); /** Return the broker-state hash of the FINAL state after the most recent * run() (see #strategy_set_broker_state_hash_recording's doc and * pf_report_t::broker_state_hash for the per-bar recording; this accessor - * works whether or not recording was enabled). Compare only within the same - * pinned engine build and configuration; this is not a serialized checkpoint. - * Returns 0 when @p s is - * NULL. */ + * works whether or not recording was enabled). Compare runs of one + * configuration: the recipe belongs to the script ABI epoch, fixed from 1.0.0 + * for every 1.x engine (engine_script_run_v19), so two values differ only + * when the states they fold differ. This is not a serialized checkpoint. + * Returns 0 when @p s is NULL. */ PF_API uint64_t strategy_broker_state_hash(pf_strategy_t s); /** Number of orders resting in the engine's pending-order book after the * most recent run() (ABI v4 live-runtime surface, task 7, spec 3.6): the From 81782d16ae77961240f35abcf2c1b91000ea5b6e Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:04:29 +0800 Subject: [PATCH 23/83] CHANGELOG.md: the 1.0.0 entry (lane REL10, item 5) What 1.0 means -- the kernel runs without any Pine adapter, and Pine is an adapter over it -- and what a 0.x user must act on since v0.13.1, the last tag (VERSION read 0.14.0 from 439bd520 on, never tagged): C ABI 4 and the grown pf_report_t, the renamed equity fields (JSON keys unchanged), the regenerate-and-relink rule of the v19 script ABI epoch and the codegen pairing, the new state hashes, the event-retention window default, -ffp-contract=off reaching consumers, the moved pine_float_compare.hpp, the 0.x find_package pins a SameMajorVersion package no longer satisfies, and prerelease versions; then the behaviour that moves results. Read from the tags, the merged PRs #232-#288 and the public headers' diff between v0.13.1 and 1a0e7ea1, with each commit and PR cited checked against the history. README points to it. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit bda36e1fe578221727afff65883823f92d0b83c5) --- CHANGELOG.md | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 +- 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..d0012db0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,110 @@ +# Changelog + +Notable changes to pineforge-engine. From 1.0.0 the version number follows +semantic versioning over the surfaces the +[public contract](docs/pages/public-contract.md) lists. Releases before 1.0.0 +are summarized in the README's *Releases* section and on the GitHub releases +page. + +## 1.0.0 + +Release candidates are tagged `v1.0.0-rc.N`. This entry covers every change +since v0.13.1, the last release: `VERSION` read 0.14.0 from 439bd520 on, and +0.14.0 was never tagged. The engine and pineforge-codegen ship 1.0.0 +together, and from 1.0.0 the only supported pair is the same version, +prerelease included. + +### What 1.0 means + +- **The kernel runs without any Pine adapter.** A C++ host + (`NativeStrategyHost`, `` and the native headers) + or a C host (``) drives the execution kernel that + generated PineScript strategies run on: batch and stream runs, the native + request vocabulary, margin and liquidation, risk limits, FX conversion, the + per-bar broker-state hash and the report. `-DPINEFORGE_BUILD_SOURCE_LAYER=OFF` + builds that kernel alone, `libpineforge_kernel.a` (`PineForge::kernel`, + #257), and `examples/native` holds 18 Pine-free hosts (16 C++, 2 C) that run + as CTest rows. +- **Pine is an adapter over that kernel.** The Pine source layer was cut out of + `BacktestEngine` (#253), and generated Pine execution was lowered onto the + native kernel, retiring the legacy loop (#254). Its host + (`PineStrategyHost`) and headers live under `include/pineforge/source/`. +- **The version number now promises something:** the C ABI, the native C++ + API, the script ABI epoch and its state-hash values, the 1.0 C-surface + boundary and the pairing with codegen, as the + [public contract](docs/pages/public-contract.md) states. + +### Breaking changes: what a 0.x user must act on + +- **C ABI version 4.** `PF_ABI_VERSION` is 4 (0.13.1: 3). `pf_report_t` + appends `broker_state_hash` and `broker_state_hash_len`, and it is + caller-allocated, so a mirror built for ABI 3 under-sizes it: update FFI + mirrors and check `pf_abi_version() == PF_ABI_VERSION` before a run + (439bd520, d9b80809). No `PF_API` function was removed or renamed; `pineforge.h` + declares 34 more and `native_c_api.h` adds the native host API. +- **Two C fields renamed.** `pf_equity_stats_t::sharpe_tv` / `sortino_tv` are + `sharpe_monthly` / `sortino_monthly`, at the same offsets (48 and 56); the + old spellings, kept as deprecated aliases during development, are removed + for 1.0. Rename them in C code and in ctypes or Rust mirrors. The JSON + report keys stay `sharpe_tv` / `sortino_tv`. Development builds' C++ + `exit_legs::Domain::Coof` / `MagnifierCoof` are likewise gone; the + enumerators are `FillRecalc` / `MagnifierFillRecalc`, with the same values. +- **Regenerate and relink every generated strategy.** The C++ a generated + strategy compiles against now carries the script ABI epoch + `engine_script_run_v19` (an inline namespace 0.13.1 did not have), so an + object built against 0.x headers does not link. Transpile with the + pineforge-codegen release of the same version (engine 1.0.0 with codegen + 1.0.0; `v1.0.0-rc.N` with codegen `1.0.0-rc.N`) and rebuild. `PF_ABI_VERSION` + equality alone does not make a pair. +- **State hashes are new, and v19's.** The broker-state hash, the per-bar hash + rows, the stream fingerprint and the native continuation hash did not exist + in 0.13.1; their values are the v19 epoch's (#282, #283, #284, #286), and a + value recorded by a development build before 1.0.0 is not comparable. From + 1.0.0 their recipe is fixed for all of 1.x. +- **Event retention defaults to a window.** `NativeRunSpec::event_retention` + defaults to `NativeEventRetention::Window`, which keeps the command journal + only until every reader has consumed it (#283). A C++ host that reads the + whole run's events after the run sets `Full` (or `Commands`). A C host that + does not send the retention word keeps `PF_NATIVE_EVENT_RETENTION_FULL`. +- **`-ffp-contract=off` reaches consumers.** The CMake package hands it to + every translation unit that links `PineForge::pineforge` or + `PineForge::kernel` (#282); 0.13.1 compiled the library alone with it. A + build outside CMake compiles generated strategy code with + `-ffp-contract=off` itself (docs/pages/integration-cmake.md). +- **One header moved.** `` is now + ``, a source-layer header + (e71e589c); new code includes ``. A + kernel-only build installs neither `include/pineforge/source/` nor + `include/pineforge/compat/`. +- **CMake pins move to 1.** The package's version file is `SameMajorVersion`, + so `find_package(PineForge 0.N ...)` does not find a 1.x install: pin + `find_package(PineForge 1.0 REQUIRED)` (docs/pages/integration-cmake.md). +- **Versions may carry a prerelease.** `VERSION` is `X.Y.Z` or `X.Y.Z-rc.N`. + `PINEFORGE_VERSION_STRING`, `PineForge_VERSION` and `pf_version_get()` stay + numeric; the full version is `PINEFORGE_VERSION_FULL`, the package's new + `PineForge_VERSION_FULL` and `pf_version_string()`, so + `find_package(PineForge 1.0.0 EXACT)` also accepts `1.0.0-rc.N`. The CMake + smoke consumer (`cmake/smoke_consumer`) prints `pf_version_string()`. + +### Behaviour that moves results against 0.13.1 + +These change a strategy's trades or report values; each commit states the +TradingView behaviour it matches. + +- `str.tostring` / `str.format` render numbers by the rule TradingView's tapes + pin (3872c46a, #287). +- Exact quantities: a Transact that crosses the book is charged exactly its + units, a close that spans lots and ends inside one exactly its request, and + a close whose binary64 sum reaches its request exactly at a lot closes that + lot whole (deb344b1, 99cf9252, 1e9e0761; #287). +- Indicators: `ta::ALMA` takes the floor input, `ta::KC` / `ta::KCW` take + use_true_range, KC's middle band is its source's EMA on every bar, + `ta::AnchoredVWAP` restarts on a caller-supplied anchor, and + `ta::PivotPointLevels` computes an anchored period's pivot levels + (f2ae16c7, de612473, 24c173ed, be41c1bb, 1eef78eb; #281); the six-argument + `ta::pivot_point_levels` overload computes every type (adb8f2b6, #287). +- Sessions: `session.isfirstbar` / `session.islastbar` are the kernel's + session-day facts, a session ends at the session day, and an aggregated + chart and a stream's realtime bar read `session.islastbar` as TradingView + does (c68fab7e, b531a8d9, 8b843499, be19463d). +- A margin call never revives an exit the script cancelled (4b00da92, #287). diff --git a/README.md b/README.md index 0ec4389e..b76a6c61 100644 --- a/README.md +++ b/README.md @@ -433,7 +433,8 @@ TradingView ties some day-boundary logic (intraday order caps, session rollovers What the version number promises from 1.0.0 — for this C ABI, the native C++ API, the script ABI epoch and the pairing with codegen — is the -[public contract](docs/pages/public-contract.md). +[public contract](docs/pages/public-contract.md); what a 0.x user must act on +is in [CHANGELOG.md](CHANGELOG.md). A built strategy `.so` exposes 66 compiled-strategy `PF_API` declarations (58 runtime implementations plus eight generated exports) plus 43 native-host @@ -590,6 +591,7 @@ Documentation: [C ABI reference](https://cdocs.pineforge.dev) · [Getting starte ## Releases +- **1.0.0** — the release notes are [CHANGELOG.md](CHANGELOG.md), and what 1.x promises is the [public contract](docs/pages/public-contract.md). - **Unreleased** (branch `live/abi-v4`) — ABI v4 live surface for `pineforge-live`: 24 new default-off exports (cooperative abort, realtime tail, probe-suppress tail logic, forced path order, a per-bar broker-state hash, the pending-order book as a generated POD mirror, closed-trade id/comment/close-cause, position and equity accessors). No flag changes a historical run: `scripts/live_flags_off_identity.py` (312 corpus probes, 0 differ vs the pre-v4 branch point), `scripts/live_flags_lane.py` (312 probes, 0 positives, 130 open-at-end trades subtracted), and `scripts/bar_identity_lane.py` (row 1: 222,295 bars compared, 2 explained open divergences, 0 else) all pass. 56 symbols. - **v0.13.0** (2026-09-05) — the parity campaign, rounds 1–11: TradingView's broker rules pinned with sensor exports and landed with replay tests — ten-significant-digit money, trailing-stop restarts, zero-offset trails, declined-reversal bracket legs, the surviving `strategy.close`, sparse `ta.atr`/`ta.tr`, pivot tick snap, same-bar entry/close transactions, early-close higher-timeframe buckets, 64-bit epoch arrays. Closed test 3,880/3,881; corpus 309/309. ABI v3, 32 symbols, 198 tests. - **v0.7 – v0.12** (June–August 2026) — native and auxiliary `request.security()` feeds, ABI v2 metrics + equity curve, streaming mode, range-end accounting. See [GitHub releases](https://github.com/pineforge-4pass/pineforge-engine/releases). From 87928a9519d1cd1125162a1a4d3309090eb52bed Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 04:49:21 +0800 Subject: [PATCH 24/83] Kernel: a group's receipts follow its drain's queue order, so a re-price that keeps its handle inside a group no longer stops the run (R5 lane K-OCA-KEEP) Ruling (ADR-0001 rule-2 row "the order of one cause's group-effect receipts"): a drain applies a group's effect in queue order, whatever the handles' numbers, and the receipts record that order. Generic, no knob. Root cause (K-ULP5's finding 1, HIGH, pre-existing). A member's fill reaches its live siblings through WorkingRequestCore::group_recipients, which walks the book in queue order -- ascending LiveRequest::priority. A re-price with ReplaceOptions::keep_handle (R5 lane V19-D) keeps the request's incarnation and takes a fresh priority, so it moves behind younger siblings: queue order and incarnation order part. The receipt store was bisected by (cause, recipient incarnation, effect), and prepare_group_effect / apply_group_effect refused any receipt that sorted before receipts_.back(). So the second recipient of one cause -- the re-priced request, drained behind a younger sibling -- was refused CoreFailure::InvalidCause and the consumer failed the run (Contract, discriminator 1) after the member's fill was booked, staged and direct, under Cancel and Reduce. K-ULP5's probe on 2a03c658: book [inc 2 prio 2][inc 1 prio 3], recipients 2 then 1, receipt (21,2) committed, (21,1) refused. Which order is right: V19-D's contract is that a keep_handle run records the plain replace's run -- every journal event, ordinal for ordinal. The plain successor is the newest request and is drained last, i.e. in queue order. Sorting the recipients by incarnation instead would drain the re-priced request first and swap the group events' ordinals against the plain run. So the walk's order stands and the receipt store follows it: - receipts_ stays append-only in commit order (the consumer folds each once, by index, at its commit), ordered by cause alone; - receipt_lookup bisects the cause and walks that cause's receipts (a drain of k recipients makes about k*k/2 comparisons); - the order check refuses only a cause older than the newest receipt's. A completed run's receipts were already in incarnation order within each cause, and the lookup answers such a store as before, so no completed run changes and no hash value moves. receipt_cmp is gone. The other walks (waiting_children, bound_close_handles, the parent-terminal and owner-applied drains) read no receipt order; the cohort rosters and the opening rows sort by handle, which a keep_handle re-price does not change. Tests: test_native_group_keep_handle (new, kernel-only): K-ULP5's probe under Cancel and Reduce, a retained trail, a carried binding -- each under the four option sets against the plain replace's journal; the order core's receipts in queue order and in reverse with replays, an older cause replayed after a newer cause's receipts, and the retained older-cause refusal; a seeded battery (3 seeds x 30 scripts x 4 option sets, staged and direct) re-pricing group members in random subsets and orders, whose misordered drains are required under Cancel, under Reduce and with deferred recipients. Fail-before on 2a03c658: 464 of 3,356 checks; the battery stops all 180 of its keep_handle runs (of 360), code 2, discriminator 1. After: 3,404 checks, 0 failures; 0 stops; about 1,000 drains per seed out of incarnation order each record the plain replace's run. The same battery at 200 scripts per seed (scratch): 0 of 2,400 runs stop; on 2a03c658 all 1,200 keep_handle runs do. No C row: the C replace takes no options. Floors: kernel 275 (276 registered), release 676. Docs: native_order.hpp (group_recipients, the receipts' order), the ADR row, native-engine.md's keep_handle paragraph; citations re-anchored from 2a03c658 (anchors 1678/1678). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit ad7a10f599810a594a61a1d05e1cfb3157c1d317) INT26 pick note: the kernel and header hunks applied without conflict. The floors add this commit's row to the merged values (KERNEL 281 -> 282, RELEASE 693 -> 694; the four floor citations name the merged lines). The ADR row sits after INT25's K-ULP5 absorption row, where this commit put it, and before H-DOCGATES' inventory rows (8deed8c9); the design pages merged on citation digits only. --- CONTRIBUTING.md | 4 +- docs/adr/0001-kernel-adapter-boundary.md | 1 + docs/design/native-feature-parity.md | 10 +- docs/pages/contributing-llm.md | 6 +- docs/pages/native-engine.md | 7 +- include/pineforge/native_order.hpp | 15 +- scripts/ci_verify.py | 10 +- src/native_order.cpp | 54 +- tests/CMakeLists.txt | 9 + tests/test_native_group_keep_handle.cpp | 1424 ++++++++++++++++++++++ 10 files changed, 1491 insertions(+), 49 deletions(-) create mode 100644 tests/test_native_group_keep_handle.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e764d5d5..ed10e3b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -152,8 +152,8 @@ What each gate refuses: ### The floors `ci_verify.py` counts the CTest rows that actually **ran** and fails below a -floor — `KERNEL_MIN_TESTS` ci_verify.py:249 and `RELEASE_MIN_TESTS` -ci_verify.py:375. +floor — `KERNEL_MIN_TESTS` ci_verify.py:252 and `RELEASE_MIN_TESTS` +ci_verify.py:381. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 2aae4fb2..3c03142f 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -533,6 +533,7 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | what the journal window made state — `RequestDefinition::root`, the order core's chain index (the issued incarnations and each replace successor's root) and `TrailTrack` / `TrailActive::activation_ordinal` (`include/pineforge/native_order.hpp`), and the FX-roll check's two driver-point instants (`NativeExecutionConsumer::driver_marks_`, `src/native_execution_consumer.hpp`) | the core, at the replace that creates a successor and at a trail's arm; the consumer at every walked driver point (synchronous current executions keep their driver log/readback and high water, but do not replace the FX predecessor) | **kernel state, folded into the continuation** (R5 lane V19-B). Rule 2 (a), mechanism: the facts a cohort command, `trail_state()` and the FX-roll check read about requests and points the journal no longer holds, kept as the kernel's own tables for every host. Rule 2 (b), knob: none. They fold where they exist -- a successor's root once at its replace, an arm ordinal where a trail armed, the two instants under a staged curve and a margin model -- which moved the pinned values of runs that replace, arm a trail or roll FX (re-pinned once, each marked); every answer is the one the journal scan gave. Pinned by `tests/test_native_journal_window.cpp` (kernel-only: the chain index against a journal oracle over randomized command streams, a trail's arm ordinal after its event retired) and `tests/test_native_event_retention.cpp` (a trail and a margin receipt read back after the window retired their events). | | the settlement's quantity refusals and their outcome -- `execution::Status::UnrepresentableQuantity` from `next_close_split`, `order_action::plan` and an opening beside a surviving book (`src/engine_execution.cpp`), which `NativeExecutionConsumer::consume_matched_request` answers as `MatchRejectReason::UnrepresentableQuantity` (`include/pineforge/native_order.hpp`; C `PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY`), as it answers the request core's `CoreFailure::NonrepresentableQuantity` from `check_execution` / `prepare_execution` (a fill the request's own units cannot absorb); the quantity grid's own quantities (`CommandContext::units_are_scope_boundary` for a `Reduce` of a FIFO boundary of its scope, measured at submit, what it settles held at its candidate to the grid, a boundary of the scope as it stands or at least its held total, and a `ScopeFraction` whose product is its gross scope's held total, unfloored -- each for a request that settles in one fill); and the opt-in `NativeRunSpec::quantity_tolerance` walk (`tolerant_close_split`) | any host, by the quantities it requests and, for the tolerance, in its spec (C: the tail of `pf_native_run_spec_ext_v1`); the Pine adapter requests its quantities through its own `1e-10` rules, emits no `ScopeFraction` and declares no tolerance (next section) | **a typed refusal of the request, the book's own quantities on the grid, and an opt-in tolerance** (R5 lane K-ULP4, the owner-level ruling of audit X1). Rule 2 (a), mechanism: binary64 bookkeeping is venue-neutral -- a host of any venue meets the same absorbed quantities, gets the same terminal typed outcome for the one request, and keeps its run; the grid admits the quantities a host chooses and the book's own sizes the same way for every venue; the tolerance is data, a spec value with no platform branch. Rule 2 (b), knob: the refusal is none -- it replaces a whole-run `SettlementFailure` (code 6, discriminator 5) no host could want, and a request that settles exactly settles as before, bit for bit; the tolerance is a knob, but not for a decision the run has already made another way: absent, the walk is exact and the spec digest is the one it had before the field (pinned from 91d65ad6), so no existing run faces a hash-visible choice. What moved: runs that stopped on such a request now complete with its refusal (the two point-budget rows of `tests/test_native_resting_acceptance.cpp` and `tests/test_native_resting_replay_contract.cpp` pinned the old run failure and now pin the refusal); on a quantity grid a whole-scope fraction and a boundary `Reduce` that were floored or `OffGrid` now settle -- 48 of AUDIT4's 3,091 previously clean battery scenarios, each one where one of the two rules fired, and no other. Pinned by `tests/test_native_unrepresentable_refusal.cpp` and `tests/test_native_quantity_tolerance.cpp` (kernel-only), `check_unrepresentable_quantity` of `tests/test_native_c_api.c`, and the genuine-* cases of `tests/test_native_partial_close_split.cpp` and `tests/test_native_exact_sum_close.cpp`. | | an OCA-Reduce sibling's deduction binary64 cannot take -- at most half an ulp of what it is taken from, so that fl(units - d) is the units again -- absorbed where the request core deducts a member's fill (`absorbed_deduction`, `pending_sum` and `absorbed_pending_delta` in `src/native_order.cpp`, read by `WorkingRequestCore::prepare_group_effect` / `apply_group_effect`, by `prepare_terms` / `apply_terms` through `effective_host_units`, and by `prepare_owner_applied` / `apply_owner_applied`), and recorded as a `ReservationReducedEvent` with `actual_deduction` 0, a `DeferredGroupAdjustmentEvent` with `deferred_delta` 0 that joins no chain, or a `TermsResolvedEvent` / `QuantityBoundEvent` with `effective_deduction` 0 beside a positive `pending_total` (`include/pineforge/native_order.hpp`; C: `closed_units` 0 on a `PF_NATIVE_EVENT_RESERVATION_REDUCED` or `PF_NATIVE_EVENT_DEFERRED_GROUP` row) | any host, by the OCA-Reduce groups it submits (C: `PF_NATIVE_GROUP_REDUCE`), and the Pine adapter by the `strategy.oca.reduce` groups it maps (`PineExecutionAdapter::group_for`, `src/source/pine_adapter.cpp`) | **absorbed as a no-op** (R5 lane K-ULP5, the owner-level ruling on K-ULP4's finding 2). Rule 2 (a), mechanism: binary64 bookkeeping is venue-neutral -- a deduction too small to move the sibling's remaining units (or its pending total) leaves them where binary64 subtraction puts them, unchanged, for a host of any venue, and the member's fill stands; a pending total below half an ulp of a later fill rounds into it, as the addition does. Rule 2 (b), knob: none -- it replaces a whole-run `SettlementFailure` (code 6, discriminator 7, after the fill was booked) that no host could want, a deduction binary64 can take is taken as before, bit for bit, and only a pending total that overflows binary64 still fails the run with that discriminator. What moved: runs that stopped on such a deduction now complete -- K-ULP4's p5b probe on the C++ and the C host, and the 914 of the 3,000 runs of the seeded OCA-Reduce battery that stopped on f1af50dc -- and the rows that pinned the old contract now pin the absorption (the G5 rows of `tests/test_native_resting_acceptance.cpp` and `tests/test_native_resting_replay_contract.cpp`, `checked_reservation_arithmetic` of `tests/test_native_order_resting_core.cpp`, A-T4d of `tests/test_native_execution_terms.cpp`, `numeric_absorption_and_chain_authentication` of `tests/test_native_order_terms_core.cpp`); the overflow rows still pin the failure. No completed run's deductions move -- each was one binary64 could take -- and `inspect_current_execution`, which threw `std::overflow_error` for a host-sized request whose pending total its units absorb, now answers it. Pinned by `tests/test_native_group_absorption.cpp` (kernel-only) and `check_reservation_absorbed` of `tests/test_native_c_api.c`. | +| the order of one cause's group-effect receipts -- the queue order `WorkingRequestCore::group_recipients` walks, which a `ReplaceOptions::keep_handle` re-price parts from incarnation order: `receipt_lookup` bisects the receipts by cause and walks that cause's, and `prepare_group_effect` / `apply_group_effect` refuse only a cause older than the newest receipt's (`src/native_order.cpp`; `WorkingRequestCore::group_effect_receipt`, `include/pineforge/native_order.hpp`) | any C++ host that re-prices a group member with `keep_handle` and then fills a sibling (`NativeStrategyHost::replace(handle, request, options)`; no C spelling, the C replace takes no options); the Pine adapter never sets `keep_handle` | **generic: a drain applies a group's effect in queue order, whatever the handles' numbers** (R5 lane K-OCA-KEEP, K-ULP5's finding 1). Rule 2 (a), mechanism: which live siblings a member's fill reaches, and in what order, is the book's own queue for a host of any venue, and the receipts are that drain's idempotence record, in the order it applied them; a request re-priced with its handle kept is reached where a plain replace's successor is, so the two runs record one timeline, as V19-D's contract already said. Rule 2 (b), knob: none -- it replaces a whole-run `Contract` failure (code 2, discriminator 1, `CoreFailure::InvalidCause`, after the member's fill was booked) that no host could want: the receipt check held one cause's receipts to incarnation order, and the re-priced member, drained behind a younger sibling, was refused; a cause older than the newest receipt's is still refused. What moved: runs that stopped on such a drain now complete -- K-ULP5's `probe_keep_handle_oca`, staged and direct, under `Cancel` and `Reduce`, and all 180 runs of the seeded battery under a keep_handle option set (of its 360), which stopped on 2a03c658 -- each recording the plain replace's run event for event. No completed run moves and no hash value moved: a completed run's receipts were already in incarnation order within each cause, and the lookup answers such a store as before (the K-ULP4 and K-ULP5 batteries' per-run hashes unchanged; corpus parity unchanged by construction, the Pine adapter never setting `keep_handle`). Pinned by `tests/test_native_group_keep_handle.cpp` (kernel-only). | | the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:1825` and `:1934`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:295`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | | the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1478`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | | the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1616`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index c83393bf..854f8e14 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -22,7 +22,7 @@ Most status differences are row granularity (one input folds two features into o | # | Ruling | |---|---| | R5-1 | **Classification.** Generic execution feature → kernel native API. TradingView quirk → adapter. Mixed feature → kernel gets the generic mechanism with a policy knob, the adapter selects the TV policy, the bare-host default is the plain generic behaviour. | -| R5-2 | **Brackets are not a new kernel object.** `WaitForApplied{RequestHandle parent}` (native_order.hpp:436-438) and `waiting_children(parent)` (native_order.hpp:1884, *fresh*) exist; L7 delivers a builder/toolkit over them plus relative anchors and the missing offsets. Size M. | +| R5-2 | **Brackets are not a new kernel object.** `WaitForApplied{RequestHandle parent}` (native_order.hpp:436-438) and `waiting_children(parent)` (native_order.hpp:1895, *fresh*) exist; L7 delivers a builder/toolkit over them plus relative anchors and the missing offsets. Size M. | | R5-3 | **Price grid is its own kernel lane (L8).** Quantity grid is native already; the price grid (booked fill on the tick, optional quantized trigger test) is generic and missing. TV's exact half-tick rule stays in the adapter on top. | | R5-4 | **Close reservations split.** Fractional reduce + reservation against sibling exits → kernel, inside the sizing lane. Callsite batching, two-call provenance, the POOC population predicate → adapter. | | R5-5 | **Calc-timing is v1.** Sub-bar hook + callback chronology contract; the adapter keeps its current path (zero drift). | @@ -56,7 +56,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL7 | OCA cancel / reduce groups (`group_for` pine_adapter.cpp:2746-2752) | **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:1092-1093 | 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:13004; lowering `submit_one` pine_adapter.cpp:9040-9052, `BindCohort` pine_adapter.cpp:9541-9547, `submit_leg` pine_adapter.cpp:9565-9589; 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:1884); 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. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:13004; lowering `submit_one` pine_adapter.cpp:9040-9052, `BindCohort` pine_adapter.cpp:9541-9547, `submit_leg` pine_adapter.cpp:9565-9589; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | | OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8645-8648; `materialize_relative_exits` pine_adapter.cpp:17322-17363, pine_adapter.hpp:1820-1832) | **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:5611-5645. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19083-19112) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10880-10904), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15631, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15865) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:651-653 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | @@ -120,7 +120,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16118) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7066-7070; `execute_current` legal native_host.hpp:1092 | 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:7009 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7060 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1061 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:863, `NativeInputContext` native_host.hpp:761-766 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16261, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6920, native_execution_consumer.cpp:6920-6950; commands are legal there `commands_allowed` native_execution_consumer.cpp:1398-1406; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5611-5645, `remaining_path_delivery` native_order.cpp:2069-2077. 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:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16261, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6920, native_execution_consumer.cpp:6920-6950; commands are legal there `commands_allowed` native_execution_consumer.cpp:1398-1406; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5611-5645, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:866 fires per accepted realtime print native_execution_consumer.cpp:7148-7152; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | | 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:7518-7635 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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:2448-2504) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | @@ -298,7 +298,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2933-3000) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2914-2981) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:45, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -1401,7 +1401,7 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1609-1610) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6982-6994) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1621-1622) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6982-6994) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). 2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5616-5620) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1659-1666) is calibrated against it. 3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6835-6842). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2557); valid in bar-open, applied and tick callbacks. diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 5688bef8..128270a4 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -117,8 +117,8 @@ finding to report, not a step to take. `scripts/check_doc_reverts.py`. 10. **A test row never silently disappears.** Each profile counts the rows that - *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:249 and - `RELEASE_MIN_TESTS` ci_verify.py:375. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:252 and + `RELEASE_MIN_TESTS` ci_verify.py:381. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -264,7 +264,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:249, `RELEASE_MIN_TESTS` ci_verify.py:375). It +(`KERNEL_MIN_TESTS` ci_verify.py:252, `RELEASE_MIN_TESTS` ci_verify.py:381). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 1fc6a0f3..fb6152f7 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -947,7 +947,12 @@ predecessor's handle, its place in the chain (no chain-root pair is folded again), its children and cohort members, and a trail's arm ordinal, and takes the number a plain successor would have taken as its priority -- queue ties still rank a re-priced request newest, and the next request is numbered as -after a plain replace. `keep_binding` carries a close's book binding: at its +after a plain replace. A group member's fill reaches it where the queue holds +it, behind every sibling accepted or re-priced before it, as it reaches a +plain successor: the group's events and receipts come in that order whatever +the handles' numbers (R5 lane K-OCA-KEEP; until then a re-priced member +drained behind a younger sibling failed the run with code 2, discriminator 1, +`CoreFailure::InvalidCause`). `keep_binding` carries a close's book binding: at its next point the successor binds, with no `CloseBoundEvent`, to exactly the book close the event would have installed when the position has not moved (taking the timeline ordinal the event would have taken, so every later ordinal is the diff --git a/include/pineforge/native_order.hpp b/include/pineforge/native_order.hpp index a5caf782..2afe71f3 100644 --- a/include/pineforge/native_order.hpp +++ b/include/pineforge/native_order.hpp @@ -1736,7 +1736,12 @@ class WorkingRequestCore { uint64_t last_incarnation() const noexcept { return last_incarnation_; } /// The group-effect receipts, in commit order: the idempotence keys a later /// drain consults. Read-only; the consumer's state continuation folds each - /// once, when it commits. + /// once, when it commits. Commit order is cause order, and one cause's + /// receipts stand in the order they were applied, whatever the recipients' + /// incarnations: the consumer's drain applies them in the queue order + /// group_recipients answers, which is not incarnation order once a re-price + /// has kept its handle (R5 lane K-OCA-KEEP). A receipt for a cause older + /// than the newest one's is refused CoreFailure::InvalidCause. std::size_t group_effect_receipt_count() const noexcept { return receipts_.size(); } GroupEffectReceipt group_effect_receipt(std::size_t index) const; /// The incarnations this run issued -- every accepted request and every @@ -1880,6 +1885,12 @@ class WorkingRequestCore { bool* exhausted) noexcept; void reserve(std::size_t expected_events); + /// The live siblings an applied member's fill reaches -- its group, another + /// cohort, born before the fill; none for a fill with no effect, such as a + /// Cancel member's nonterminal one -- in queue order (live()): a request + /// re-priced with its handle kept stands behind every sibling accepted or + /// re-priced before it, where a plain replace's successor stands (R5 lane + /// K-OCA-KEEP). std::vector group_recipients(const EventId& applied) const; std::vector waiting_children(const RequestHandle& parent) const; bool has_waiting_children(const RequestHandle& parent) const noexcept; @@ -2217,6 +2228,8 @@ class WorkingRequestCore { GroupEffect effect = GroupEffect::Reduce; uint64_t outcome_ordinal = 0; }; + // In commit order: non-decreasing cause ordinal, one cause's in the order + // they were applied (receipt_lookup bisects the cause, then walks it). std::vector receipts_; std::uint64_t next_cohort_handle_ = 1; std::vector cohorts_; diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index b33100e3..588e92a3 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -246,7 +246,10 @@ # registered, 278 run: the WebSocket row still skips on a system libcurl. # 279 = those 278 plus R5 lane H-DOCGATES' test_doc_reverts (the doc-revert # gate's self-test, a Python row every profile registers; INT26 pick, summed). -KERNEL_MIN_TESTS = 281 +# 282 = those 281 plus the one source-free TU of R5 lane K-OCA-KEEP +# (test_native_group_keep_handle; INT26 pick, summed). It registers in release +# too. +KERNEL_MIN_TESTS = 282 # Release-row floor, the same gate for the default profile. Before lane P7 # only the kernel profile had one, so a row that left release alone (a # source-bound TU dropped from TEST_SOURCES, a deleted twin or ABI row) left a @@ -372,7 +375,10 @@ # No release row skips, so 690 registered is 690 run. # 691 = those 690 plus R5 lane H-DOCGATES' test_doc_reverts (see # KERNEL_MIN_TESTS; INT26 pick, summed). -RELEASE_MIN_TESTS = 693 +# 694 = those 693 plus the one R5 lane K-OCA-KEEP row KERNEL_MIN_TESTS lists +# above; the C replace takes no options, so the lane adds no C row (INT26 +# pick, summed). +RELEASE_MIN_TESTS = 694 # R5 lane H-DOCGATES (AUDIT4-opus perf N-7) raises both floors by one more: # test_native_runtime_ambient_lifo, the runtime blocks' LIFO death row, is # source-free, so it registers and runs in the kernel profile (279 -> 280) and diff --git a/src/native_order.cpp b/src/native_order.cpp index 888d2d60..ac44081a 100644 --- a/src/native_order.cpp +++ b/src/native_order.cpp @@ -490,19 +490,6 @@ const ExecutionAppliedEvent* as_applied(const CommandEvent& event) noexcept { return std::get_if(&event); } -int receipt_cmp(uint64_t oa, uint64_t ia, GroupEffect ea, uint64_t ob, uint64_t ib, - GroupEffect eb) noexcept { - if (oa < ob) return -1; - if (oa > ob) return 1; - if (ia < ib) return -1; - if (ia > ib) return 1; - const auto a = static_cast(ea); - const auto b = static_cast(eb); - if (a < b) return -1; - if (a > b) return 1; - return 0; -} - // A trail offset is a finite price distance. Zero is the "ride the best" // spelling: the level is the best itself and only a move strictly past it // exits. Negative and nonfinite offsets stay rejected. @@ -1403,17 +1390,22 @@ GroupEffectReceipt WorkingRequestCore::group_effect_receipt(std::size_t index) c return GroupEffectReceipt{key.cause, key.recipient, key.effect, key.outcome_ordinal}; } +// The receipts are in cause order, and one cause's stand in the order they +// were applied -- the consumer's drain applies them in the queue order +// group_recipients walks, which is not incarnation order once a re-price has +// kept its handle (R5 lane K-OCA-KEEP). So the cause is bisected and its +// receipts are walked: a drain of k recipients makes about k*k/2 comparisons. WorkingRequestCore::ReceiptLookup WorkingRequestCore::receipt_lookup( const EventId& cause, const RequestHandle& recipient, GroupEffect effect, uint64_t* outcome) const { - ReceiptKey needle{cause, recipient, effect, 0}; - const auto it = std::lower_bound( - receipts_.begin(), receipts_.end(), needle, - [](const ReceiptKey& a, const ReceiptKey& b) { - return receipt_cmp(a.cause.ordinal, a.recipient.incarnation, a.effect, - b.cause.ordinal, b.recipient.incarnation, b.effect) - < 0; - }); + auto it = std::lower_bound(receipts_.begin(), receipts_.end(), cause.ordinal, + [](const ReceiptKey& key, uint64_t ordinal) { + return key.cause.ordinal < ordinal; + }); + while (it != receipts_.end() && it->cause.ordinal == cause.ordinal + && (it->recipient.incarnation != recipient.incarnation || it->effect != effect)) { + ++it; + } if (it == receipts_.end()) return ReceiptLookup::Absent; if (it->cause != cause || it->recipient != recipient || it->effect != effect) { return ReceiptLookup::Absent; @@ -4918,13 +4910,10 @@ Preparation WorkingRequestCore::prepare_group_effect( if (lookup == ReceiptLookup::Conflict) { return PreparationError{CoreFailure::ConflictingReceipt, applied, recipient}; } - if (!receipts_.empty()) { - const auto& last = receipts_.back(); - if (receipt_cmp(applied.ordinal, recipient.incarnation, member->effect, last.cause.ordinal, - last.recipient.incarnation, last.effect) - < 0) { - return PreparationError{CoreFailure::InvalidCause, applied, recipient}; - } + // Receipts commit in cause order; one cause's in the order they are + // applied, whatever the recipients' incarnations (receipt_lookup). + if (!receipts_.empty() && applied.ordinal < receipts_.back().cause.ordinal) { + return PreparationError{CoreFailure::InvalidCause, applied, recipient}; } std::size_t live_index = 0; if (classify(recipient, &live_index) != TargetKind::Live) { @@ -5081,13 +5070,8 @@ Preparation WorkingRequestCore::apply_group_effect( if (lookup == ReceiptLookup::Conflict) { return PreparationError{CoreFailure::ConflictingReceipt, applied, recipient}; } - if (!receipts_.empty()) { - const auto& last = receipts_.back(); - if (receipt_cmp(applied.ordinal, recipient.incarnation, member->effect, last.cause.ordinal, - last.recipient.incarnation, last.effect) - < 0) { - return PreparationError{CoreFailure::InvalidCause, applied, recipient}; - } + if (!receipts_.empty() && applied.ordinal < receipts_.back().cause.ordinal) { + return PreparationError{CoreFailure::InvalidCause, applied, recipient}; } std::size_t live_index = 0; if (classify(recipient, &live_index) != TargetKind::Live) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0fab8ecb..e13ba7f7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1192,6 +1192,15 @@ list(APPEND TEST_SOURCES test_native_quantity_tolerance) # and a seeded battery of OCA-Reduce groups at ulp scale, staged and direct. # Source-free, so it also runs in the kernel-only profile. list(APPEND TEST_SOURCES test_native_group_absorption) +# R5 lane K-OCA-KEEP: a re-price that keeps its handle inside a group, then a +# sibling's fill, drains the group in queue order and the run goes on (it +# stopped with code 2 / discriminator 1 on 2a03c658): K-ULP5's keep_handle +# probe under Cancel and Reduce, a retained trail and a carried binding, the +# order core's receipts in queue order and in reverse, and a seeded battery of +# groups re-priced in random subsets and orders under every option set against +# the plain replace, staged and direct. Source-free, so it also runs in the +# kernel-only profile. +list(APPEND TEST_SOURCES test_native_group_keep_handle) # R5 lane D2-A (PB): the consumer's O(1) has_command_after against the two # journal searches the Pine adapter's quiet receipt read compared, for every # kind of cursor, at every callback of windowed randomized runs diff --git a/tests/test_native_group_keep_handle.cpp b/tests/test_native_group_keep_handle.cpp new file mode 100644 index 00000000..408197f2 --- /dev/null +++ b/tests/test_native_group_keep_handle.cpp @@ -0,0 +1,1424 @@ +// R5 lane K-OCA-KEEP: a re-price that keeps its handle inside a group, then a +// sibling's fill -- the group's effect reaches every sibling it names, and the +// run goes on, recording what the plain replace records. +// +// A group member's fill reaches every live sibling born before it +// (WorkingRequestCore::group_recipients) in queue order: the order the book +// holds them, ascending LiveRequest::priority. A re-price that keeps its +// handle (ReplaceOptions::keep_handle, R5 lane V19-D) keeps the request's +// incarnation and takes the number a plain successor would have taken as its +// priority, so it moves to the back of the queue, and queue order and +// incarnation order part. The group-effect receipts were kept in commit order +// and bisected by (cause, recipient incarnation, effect), so each new receipt +// had to sort after the newest one: the re-priced request, drained behind a +// younger sibling, sorted before that sibling's receipt and was refused +// CoreFailure::InvalidCause, and the run stopped after the member's fill was +// booked (lifecycle Failed, code 2, discriminator 1) -- staged and direct, +// under GroupEffect::Cancel and ::Reduce alike (K-ULP5, finding 1). The plain +// replace records that sibling as the newest request, last in the drain, and +// V19-D's contract is that the two runs record one timeline, so the queue +// order is the drain's and the receipts follow it: they are ordered by cause +// alone, one cause's receipts stand in the order its drain committed them, +// a lookup bisects the cause and walks its receipts, and a receipt for a +// cause older than the newest one's is still InvalidCause. +// +// probe-* K-ULP5's probe_keep_handle_oca: A and B rest in one group, +// A is re-priced with keep_handle (handle 1, now behind B) and +// a market member fills; under Reduce each sibling is +// deducted, under Cancel each is cancelled, B then A -- the +// plain replace's journal, ordinal for ordinal, under every +// option set +// trail-* a trail close re-priced with keep_handle and its running +// best retained (retain_trigger_state), then a sibling's fill +// bound-* a close bound to the book re-priced with keep_handle and +// keep_binding, then a sibling's fill +// core-* the order core itself: one cause's receipts in queue order +// and in reverse, each found again on a replay +// (AlreadyApplied) and read back in commit order; after a +// newer cause's receipts the older cause's are still found, +// and a recipient its drain never reached is refused; a +// receipt for a cause older than the newest one's is refused +// InvalidCause, and records nothing +// battery seeded groups of both effects -- entries, closes of every +// intent, bracket legs waiting for their entry, market +// members, shared cohorts -- re-priced in random subsets and +// random orders, from the bar and from the fill callback, and +// cancelled, under the four option sets (plain, keep_handle, +// keep_binding, both; a trail's best retained at random). +// Every run completes; the four record one run -- every fill, +// every journal event mapped to the host's slots with its +// ordinal (CloseBoundEvents aside, which keep_binding spares), +// every group-effect receipt, the queue order every bar, every +// answer, every trade but its entry incarnation; and the +// drains whose receipts came in other than incarnation order +// -- the ones refused before -- are counted, under each effect +// and for recipients whose units are deferred +// The consumer cases run on the request core's staged and direct paths, which +// must agree bit for bit, hashes included; the core rows drive each path on a +// core of its own. No C row: the C replace takes no options +// (strategy_native_replace_v1 / _ext_v1), so a C host re-prices with the plain +// replace only, whose successor is the newest request in both orders. +// +// Fail-before: compiled against 2a03c658 (K-ULP5) the TU builds and fails +// 464 of 3,356 checks: the 12 probe-*, trail-* and bound-* runs under a +// keep_handle option set stop with code 2, discriminator 1, cause the member's +// fill and recipient the re-priced request (the 12 plain and keep_binding runs +// complete); core-* refuses the re-priced recipient InvalidCause in queue +// order, staged and direct (the reverse order passes, and the older-cause +// replay fails at its first drain); and of the battery's 360 runs, every one +// of the 180 under a keep_handle option set stops with code 2, discriminator +// 1, while no plain drain leaves incarnation order. +// Source-free: the kernel-only profile registers the row. +#include "../src/native_execution_consumer.hpp" +#include "native_match_book_fixture.hpp" +#include "native_order_full_fold.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace pineforge; +namespace no = pineforge::native_order; +using k3_book::ticks; + +int checks = 0; +int failures = 0; +std::string scenario = "setup"; + +#define CHECK(condition) do { \ + ++checks; \ + if (!(condition)) { \ + ++failures; \ + std::printf("FAIL [%s] line %d: %s\n", scenario.c_str(), __LINE__, \ + #condition); \ + } \ +} while (0) + +// ---- The option sets ------------------------------------------------------------ + +enum class Mode : int { Replace = 0, KeepHandle = 1, KeepBinding = 2, KeepBoth = 3 }; +constexpr Mode kModes[] = {Mode::Replace, Mode::KeepHandle, Mode::KeepBinding, Mode::KeepBoth}; +const char* mode_name(Mode mode) { + switch (mode) { + case Mode::Replace: return "replace"; + case Mode::KeepHandle: return "keep_handle"; + case Mode::KeepBinding: return "keep_binding"; + case Mode::KeepBoth: return "keep_handle+keep_binding"; + } + return "?"; +} +bool keeps_handle(Mode mode) { return mode == Mode::KeepHandle || mode == Mode::KeepBoth; } +bool keeps_binding(Mode mode) { return mode == Mode::KeepBinding || mode == Mode::KeepBoth; } +const char* effect_name(no::GroupEffect effect) { + return effect == no::GroupEffect::Reduce ? "reduce" : "cancel"; +} + +// ---- A scripted host: each bar may run one callback ------------------------------- + +struct ScriptHost; +using Script = std::map>; + +struct ScriptHost final : public NativeStrategyHost { + ScriptHost(bool direct, Mode mode) : mode(mode) { + as_native_consumer(execution_consumer()).set_direct_mutation(direct); + } + void on_native_run_begin() override { bar = 0; } + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + const int b = bar++; + const auto it = script.find(b); + if (it != script.end()) it->second(*this); + } + const no::WorkingRequestCore& core() const { + return as_native_consumer(execution_consumer()).request_core(); + } + // Submits under `name`; answers whether the kernel accepted it. + bool sub(const std::string& name, no::Request r) { + r.label = name; + const auto res = submit(r); + if (res.status != no::SubmitStatus::Accepted || !res.handle) { + ++refused; + return false; + } + h[name] = *res.handle; + names[res.handle->incarnation] = name; + return true; + } + // Re-prices `name` under the run's option set; `retain` carries a trail's + // running best. + bool rep(const std::string& name, no::Request r, bool retain = false) { + r.label = name; + no::ReplaceOptions options; + options.retain_trigger_state = retain; + options.keep_handle = keeps_handle(mode); + options.keep_binding = keeps_binding(mode); + const auto res = replace(h[name], r, options); + if (res.status != no::ReplaceStatus::Replaced || !res.successor) { + ++refused; + return false; + } + if (*res.successor == h[name]) ++kept; + h[name] = *res.successor; + names[res.successor->incarnation] = name; + return true; + } + std::string name_of(const no::RequestHandle& handle) const { + const auto it = names.find(handle.incarnation); + return it == names.end() ? "#" + std::to_string(handle.incarnation) : it->second; + } + // The working book in queue order, by name. + std::string queue() const { + std::string out; + for (const auto& live : core().live()) out += name_of(live.handle()) + " "; + return out; + } + + Mode mode; + Script script; + std::map h; + std::map names; + std::vector queues; + int bar = 0; + int refused = 0; + int kept = 0; +}; + +no::Request tx(double units) { no::Request r; r.intent = no::Transact{units}; return r; } +no::Request red(double units) { + no::Request r; + r.intent = no::Reduce{no::ExplicitUnits{units}}; + return r; +} +no::Request in_group(no::Request r, std::uint64_t group, std::int64_t cohort, + no::GroupEffect effect) { + r.group = no::Member{group, cohort, effect}; + return r; +} + +std::string fmt(const char* format, double a, double b = 0.0, double c = 0.0) { + char buffer[160]; + std::snprintf(buffer, sizeof buffer, format, a, b, c); + return buffer; +} + +// The journal, one line per command event: its ordinal, its kind and the +// request it concerns by name, and what a group effect wrote -- its cause's +// ordinal and its units. CloseBoundEvents are left out: keep_binding spares +// them and takes their ordinals (R5 lane V19-D). +std::string transcript(const ScriptHost& host) { + std::string out; + for (const auto& row : host.native_events(0)) { + if (!row.command) continue; + std::visit([&](const auto& event) { + using E = std::decay_t; + if constexpr (std::is_same_v) { + return; + } else { + out += std::to_string(event.ordinal) + " "; + if constexpr (std::is_same_v) { + out += "Accepted " + host.name_of(event.handle()); + } else if constexpr (std::is_same_v) { + out += "Replaced " + host.name_of(event.successor()); + } else if constexpr (std::is_same_v) { + out += "Applied " + host.name_of(event.handle()) + + fmt(" %.17g %.17g", event.closed_units, event.opened_units); + } else if constexpr (std::is_same_v) { + const auto* after = std::get_if(&event.after); + out += "Reduced " + host.name_of(event.recipient) + " cause " + + std::to_string(event.cause.ordinal) + + fmt(" %.17g %.17g -> %.17g", event.requested_delta, event.before.q, + after ? after->q : -1.0); + } else if constexpr (std::is_same_v) { + out += "Deferred " + host.name_of(event.recipient) + " cause " + + std::to_string(event.cause.ordinal) + + fmt(" %.17g", event.pending_after.total); + } else if constexpr (std::is_same_v) { + out += "Cancelled " + host.name_of(event.handle()) + " reason " + + std::to_string(static_cast(event.reason)); + if (event.cause) out += " cause " + std::to_string(event.cause->ordinal); + } else { + out += "kind " + std::to_string(row.command->index()); + } + out += "\n"; + } + }, *row.command); + } + return out; +} + +struct Outcome { + bool completed = false; + unsigned code = 0, discriminator = 0; + std::uint64_t cause = 0, recipient = 0; + std::string transcript; + std::vector queues; + std::uint64_t continuation = 0, broker = 0; + int refused = 0, kept = 0; + std::unique_ptr host; +}; + +NativeRunSpec spec_for(const std::string& name) { + NativeRunSpec s; + s.event_retention = NativeEventRetention::Full; + s.identity = {"k-oca-keep-" + name, 1}; + s.input_tf = "1"; + s.script_tf = "1"; + s.tickerid = "K-OCA-KEEP:PROBE"; + s.timezone = "UTC"; + s.session = "24x7"; + s.initial_capital = 1e9; + s.point_value = 1.0; + s.account_fx = 1.0; + s.price_tick = 0.01; + s.fee_kind = NativeFeeKind::CashPerExecution; + s.fee_value = 0.0; + return s; +} + +std::vector flat_bars(int n, double price = 100.0) { + std::vector bars; + for (int i = 0; i < n; ++i) { + bars.push_back(Bar{price, price, price, price, 10.0, 60'000LL * (i + 1)}); + } + return bars; +} + +Outcome run_one(const std::string& name, const Script& script, bool direct, Mode mode, + int bars = 8) { + Outcome out; + out.host = std::make_unique(direct, mode); + out.host->script = script; + CHECK(out.host->configure_native(spec_for(name)).status == NativeSetupStatus::Applied); + const auto tape = flat_bars(bars); + out.host->run(tape.data(), static_cast(tape.size())); + const auto state = out.host->native_state(); + out.completed = state.kind == NativeLifecycleKind::Completed; + out.code = static_cast(state.failure.code); + out.discriminator = static_cast(state.failure.discriminator); + out.cause = state.failure.context.cause.ordinal; + out.recipient = state.failure.context.recipient.incarnation; + out.transcript = transcript(*out.host); + out.queues = out.host->queues; + out.continuation = out.host->native_continuation_hash(); + out.broker = out.host->broker_state_hash(); + out.refused = out.host->refused; + out.kept = out.host->kept; + return out; +} + +// Every option set on both paths: the paths agree bit for bit, every run +// completes, and every option set records the plain replace's run. +void run_directed(const std::string& name, const Script& script, + const std::function& expect) { + std::string reference; + std::vector reference_queues; + for (const Mode mode : kModes) { + scenario = name + "/" + mode_name(mode); + const Outcome staged = run_one(name, script, false, mode); + const Outcome direct = run_one(name, script, true, mode); + CHECK(staged.completed == direct.completed); + CHECK(staged.code == direct.code && staged.discriminator == direct.discriminator); + CHECK(staged.transcript == direct.transcript); + CHECK(staged.queues == direct.queues); + CHECK(staged.continuation == direct.continuation); + CHECK(staged.broker == direct.broker); + std::printf("%-14s %-24s completed=%d code=%u disc=%u cause=%llu recipient=%llu kept=%d\n", + name.c_str(), mode_name(mode), staged.completed ? 1 : 0, staged.code, + staged.discriminator, static_cast(staged.cause), + static_cast(staged.recipient), staged.kept); + CHECK(staged.completed); + CHECK(staged.code == 0 && staged.discriminator == 0); + CHECK(staged.refused == 0); + CHECK(staged.kept == (keeps_handle(mode) ? 1 : 0)); + if (mode == Mode::Replace) { + reference = staged.transcript; + reference_queues = staged.queues; + } else { + CHECK(staged.transcript == reference); + CHECK(staged.queues == reference_queues); + } + expect(staged, mode); + } +} + +// The drain's two events, B's then A's, after the member's fill: their lines. +std::vector drain_after(const std::string& transcript, const std::string& filler) { + std::vector lines; + std::size_t at = 0; + bool seen = false; + while (at < transcript.size()) { + const std::size_t end = transcript.find('\n', at); + const std::string line = transcript.substr(at, end - at); + at = end + 1; + if (!seen) { + seen = line.find(" Applied " + filler + " ") != std::string::npos; + continue; + } + if (line.find(" Reduced ") != std::string::npos + || line.find(" Cancelled ") != std::string::npos + || line.find(" Deferred ") != std::string::npos) { + lines.push_back(line); + } else { + break; + } + } + return lines; +} + +std::uint64_t applied_ordinal(const std::string& transcript, const std::string& name) { + const std::size_t at = transcript.find(" Applied " + name + " "); + if (at == std::string::npos) return 0; + const std::size_t start = transcript.rfind('\n', at); + return std::stoull(transcript.substr(start == std::string::npos ? 0 : start + 1)); +} + +// ---- Directed cases ----------------------------------------------------------- + +// K-ULP5's probe: two resting buys of 5 in one group, A re-priced, a market +// buy of 1 in the group fills. +void probe(no::GroupEffect effect) { + Script s; + s[0] = [effect](ScriptHost& p) { + auto a = in_group(tx(5.0), 9, 2, effect); + a.trigger = no::Limit{1.0}; + p.sub("A", a); + auto b = in_group(tx(5.0), 9, 3, effect); + b.trigger = no::Limit{1.0}; + p.sub("B", b); + }; + s[1] = [effect](ScriptHost& p) { + auto a = in_group(tx(5.0), 9, 2, effect); + a.trigger = no::Limit{2.0}; + p.rep("A", a); + }; + s[2] = [effect](ScriptHost& p) { + p.sub("filler", in_group(tx(1.0), 9, 1, effect)); + p.queues.push_back(p.queue()); + }; + run_directed(std::string("probe-") + effect_name(effect), s, + [effect](const Outcome& o, Mode) { + // A re-price moves A behind B, whichever option set. + CHECK(o.queues == std::vector{"B A filler "}); + const auto cause = std::to_string(applied_ordinal(o.transcript, "filler")); + const auto drain = drain_after(o.transcript, "filler"); + CHECK(drain.size() == 2); + if (drain.size() != 2) return; + if (effect == no::GroupEffect::Reduce) { + CHECK(drain[0].find("Reduced B cause " + cause + " 1 5 -> 4") != std::string::npos); + CHECK(drain[1].find("Reduced A cause " + cause + " 1 5 -> 4") != std::string::npos); + } else { + const auto group = std::to_string(static_cast(no::CancelReason::Group)); + CHECK(drain[0].find("Cancelled B reason " + group + " cause " + cause) + != std::string::npos); + CHECK(drain[1].find("Cancelled A reason " + group + " cause " + cause) + != std::string::npos); + } + }); +} + +// A long of 10; two closes in one group, A a trail, B a stop the run never +// reaches; A re-priced with its running best retained; a market close of 1 in +// the group. +void trail(no::GroupEffect effect) { + Script s; + s[0] = [](ScriptHost& p) { p.sub("long", tx(10.0)); }; + s[1] = [effect](ScriptHost& p) { + auto a = in_group(red(2.0), 7, 2, effect); + no::Trail t; + t.offset = 1.0; + a.trigger = t; + p.sub("A", a); + auto b = in_group(red(2.0), 7, 3, effect); + b.trigger = no::Stop{50.0}; + p.sub("B", b); + }; + s[3] = [effect](ScriptHost& p) { + auto a = in_group(red(2.0), 7, 2, effect); + no::Trail t; + t.offset = 1.5; + a.trigger = t; + p.rep("A", a, /*retain=*/true); + }; + s[4] = [effect](ScriptHost& p) { + p.sub("filler", in_group(red(1.0), 7, 1, effect)); + p.queues.push_back(p.queue()); + }; + run_directed(std::string("trail-") + effect_name(effect), s, + [effect](const Outcome& o, Mode) { + CHECK(o.queues == std::vector{"B A filler "}); + const auto cause = std::to_string(applied_ordinal(o.transcript, "filler")); + const auto drain = drain_after(o.transcript, "filler"); + CHECK(drain.size() == 2); + if (drain.size() != 2) return; + if (effect == no::GroupEffect::Reduce) { + CHECK(drain[0].find("Reduced B cause " + cause + " 1 2 -> 1") != std::string::npos); + CHECK(drain[1].find("Reduced A cause " + cause + " 1 2 -> 1") != std::string::npos); + } else { + CHECK(drain[0].find("Cancelled B") != std::string::npos); + CHECK(drain[1].find("Cancelled A") != std::string::npos); + } + }); +} + +// A long of 10; two sell limits above the market in one group, each bound to +// the book at its first point; A re-priced (keep_binding carries its binding); +// a market close of 1 in the group. +void bound(no::GroupEffect effect) { + Script s; + s[0] = [](ScriptHost& p) { p.sub("long", tx(10.0)); }; + s[2] = [effect](ScriptHost& p) { + auto a = in_group(red(3.0), 5, 2, effect); + a.trigger = no::Limit{150.0}; + p.sub("A", a); + auto b = in_group(red(3.0), 5, 3, effect); + b.trigger = no::Limit{160.0}; + p.sub("B", b); + }; + s[4] = [effect](ScriptHost& p) { + auto a = in_group(red(3.0), 5, 2, effect); + a.trigger = no::Limit{155.0}; + p.rep("A", a); + }; + s[5] = [effect](ScriptHost& p) { + p.sub("filler", in_group(red(1.0), 5, 1, effect)); + p.queues.push_back(p.queue()); + }; + run_directed(std::string("bound-") + effect_name(effect), s, + [effect](const Outcome& o, Mode) { + CHECK(o.queues == std::vector{"B A filler "}); + const auto cause = std::to_string(applied_ordinal(o.transcript, "filler")); + const auto drain = drain_after(o.transcript, "filler"); + CHECK(drain.size() == 2); + if (drain.size() != 2) return; + if (effect == no::GroupEffect::Reduce) { + CHECK(drain[0].find("Reduced B cause " + cause + " 1 3 -> 2") != std::string::npos); + CHECK(drain[1].find("Reduced A cause " + cause + " 1 3 -> 2") != std::string::npos); + } else { + CHECK(drain[0].find("Cancelled B") != std::string::npos); + CHECK(drain[1].find("Cancelled A") != std::string::npos); + } + }); +} + +// ---- The order core itself ------------------------------------------------------ + +const no::RunIdentity kCoreRun{"k-oca-keep-core", 1}; + +no::EvaluationContext print_ctx(std::uint64_t point) { + no::EvaluationContext context; + context.cursor.point.ordinal = point; + context.cursor.point.effective_time_ms = 1000; + context.cursor.point.provenance = NativePriceProvenance::ObservedPrint; + context.driver_class = no::DriverEligibilityClass::ObservedPrint; + context.existing_matching_bit = true; + return context; +} + +no::TargetObservation flat_book() { + no::TargetObservation observation; + observation.current_position = no::PositionFlat{}; + return observation; +} + +no::TargetObservation long_book(std::int64_t cycle) { + no::TargetObservation observation; + observation.current_position = no::PositionNonflat{cycle, no::Side::Long}; + return observation; +} + +void commit(no::WorkingRequestCore& core, no::Preparation prep, + std::uint64_t& ord) { + CHECK(std::holds_alternative(prep)); + if (!std::holds_alternative(prep)) return; + auto installed = core.install_mutation(std::get(std::move(prep))); + CHECK(std::holds_alternative(installed)); + if (const auto* in = std::get_if(&installed)) ord += in->events.count; +} + +// Fills a market Transact of `units` at one print: its applied event's id. +no::EventId fill(no::WorkingRequestCore& core, const no::RequestHandle& handle, double units, + std::uint64_t& ord) { + commit(core, core.prepare_evaluation(handle, print_ctx(ord + 1), flat_book(), ord), ord); + const auto* live = core.find_live(handle); + CHECK(live != nullptr); + if (!live) return {}; + no::ExecutionProposal proposal; + proposal.cursor.point.ordinal = std::get(live->allowance).point_ordinal; + proposal.cursor.point.effective_time_ms = 1000; + proposal.raw_price = 100; + proposal.resolved_price = 100; + proposal.physical_action = no::Transact{units}; + proposal.scope = execution::Book{}; + proposal.inspected_closed_units = 0.0; + proposal.inspected_opened_units = units; + proposal.inspected_current_ticket = 1; + auto prep = core.prepare_execution(handle, proposal, ord); + CHECK(std::holds_alternative(prep)); + if (!std::holds_alternative(prep)) return {}; + no::CommittedExecutionFacts facts; + facts.result.status = execution::Status::Applied; + facts.result.opened_units = units; + facts.result.current_ticket = 1; + facts.cycle_after = 1; + facts.post_target = long_book(1); + facts.committed_action = proposal.physical_action; + auto installed = + core.install_execution(std::get(std::move(prep)), facts); + CHECK(std::holds_alternative(installed)); + if (const auto* in = std::get_if(&installed)) ord += in->events.count; + return no::EventId{kCoreRun, std::get(core.history().back()).ordinal}; +} + +// One recipient's group effect, staged (prepare, then install) or direct +// (apply): 0 installed, 1 no change, 2 refused. +struct Step { + int kind = -1; + no::NoChangeReason reason = no::NoChangeReason::NoTransition; + no::CoreFailure code = no::CoreFailure::Invariant; +}; + +Step effect_on(no::WorkingRequestCore& core, const no::EventId& cause, + const no::RequestHandle& recipient, std::uint64_t& ord, bool direct) { + Step step; + if (direct) { + auto result = core.apply_group_effect(cause, recipient, ord); + if (const auto* in = std::get_if(&result)) { + ord += in->events.count; + step.kind = 0; + } else if (const auto* none = std::get_if(&result)) { + step.kind = 1; + step.reason = none->reason; + } else { + step.kind = 2; + step.code = std::get(result).code; + } + return step; + } + auto prep = core.prepare_group_effect(cause, recipient, ord); + if (std::holds_alternative(prep)) { + commit(core, std::move(prep), ord); + step.kind = 0; + } else if (const auto* none = std::get_if(&prep)) { + step.kind = 1; + step.reason = none->reason; + } else { + step.kind = 2; + step.code = std::get(prep).code; + } + return step; +} + +no::RequestHandle member(no::WorkingRequestCore& core, double units, double limit, + std::int64_t cohort, no::GroupEffect effect, std::uint64_t& inc, + std::uint64_t& ord) { + no::Request r = in_group(tx(units), 9, cohort, effect); + if (limit > 0.0) r.trigger = no::Limit{limit}; + const auto res = core.submit(r, 1, inc, ord); + CHECK(res.status == no::SubmitStatus::Accepted && res.handle); + return res.handle ? *res.handle : no::RequestHandle{}; +} + +bool is_already_applied(const Step& step) { + return step.kind == 1 && step.reason == no::NoChangeReason::AlreadyApplied; +} + +void core_rows(no::GroupEffect effect, bool direct) { + scenario = std::string("core-") + effect_name(effect) + (direct ? "-direct" : "-staged"); + // One cause, its recipients in queue order and in reverse. + for (const bool reverse : {false, true}) { + no::WorkingRequestCore core(kCoreRun); + std::uint64_t inc = 1; + std::uint64_t ord = 1; + const auto a = member(core, 5.0, 1.0, 2, effect, inc, ord); + const auto b = member(core, 5.0, 1.0, 3, effect, inc, ord); + // A re-priced in place: it keeps its handle and moves behind B. + no::ReplaceOptions keep; + keep.keep_handle = true; + no::Request moved = in_group(tx(5.0), 9, 2, effect); + moved.trigger = no::Limit{2.0}; + const auto replaced = core.replace(a, moved, 1, inc, ord, std::nullopt, keep); + CHECK(replaced.status == no::ReplaceStatus::Replaced); + CHECK(replaced.successor && *replaced.successor == a); + CHECK(core.live().size() == 2 && core.live()[0].handle() == b + && core.live()[1].handle() == a); + const auto filler = member(core, 1.0, 0.0, 1, effect, inc, ord); + const no::EventId cause = fill(core, filler, 1.0, ord); + // Queue order: B, then the re-priced A -- not incarnation order. + CHECK(core.group_recipients(cause) == (std::vector{b, a})); + const no::RequestHandle first = reverse ? a : b; + const no::RequestHandle second = reverse ? b : a; + const std::size_t receipts = core.group_effect_receipt_count(); + const Step one = effect_on(core, cause, first, ord, direct); + CHECK(one.kind == 0); + const Step two = effect_on(core, cause, second, ord, direct); + CHECK(two.kind == 0); + if (two.kind == 2) { + std::printf(" [%s reverse=%d] second recipient refused code=%u\n", scenario.c_str(), + reverse ? 1 : 0, static_cast(two.code)); + } + // Read back in commit order. + CHECK(core.group_effect_receipt_count() == receipts + 2); + if (core.group_effect_receipt_count() == receipts + 2) { + const auto r0 = core.group_effect_receipt(receipts); + const auto r1 = core.group_effect_receipt(receipts + 1); + CHECK(r0.cause == cause && r0.recipient == first && r0.effect == effect); + CHECK(r1.cause == cause && r1.recipient == second && r1.effect == effect); + CHECK(r0.outcome_ordinal < r1.outcome_ordinal); + } + // A replay finds each receipt, on either path, and records nothing. + const std::size_t history = core.history_end(); + for (const auto& recipient : {a, b}) { + CHECK(is_already_applied(effect_on(core, cause, recipient, ord, false))); + CHECK(is_already_applied(effect_on(core, cause, recipient, ord, true))); + } + CHECK(core.history_end() == history); + if (effect == no::GroupEffect::Reduce) { + for (const auto& recipient : {a, b}) { + const auto* live = core.find_live(recipient); + CHECK(live && std::holds_alternative(live->remaining) + && std::get(live->remaining).q == 4.0); + } + } else { + CHECK(core.find_live(a) == nullptr && core.find_live(b) == nullptr); + } + } + // One cause drained in queue order, a newer cause's receipts after it, and + // the older cause again: its receipts are found -- the lookup's walk stops + // at the cause's end -- and a recipient its drain never reached is refused. + { + no::WorkingRequestCore core(kCoreRun); + std::uint64_t inc = 1; + std::uint64_t ord = 1; + const auto a = member(core, 5.0, 1.0, 2, effect, inc, ord); + const auto b = member(core, 5.0, 1.0, 3, effect, inc, ord); + const auto c = member(core, 5.0, 1.0, 5, effect, inc, ord); + no::ReplaceOptions keep; + keep.keep_handle = true; + no::Request moved = in_group(tx(5.0), 9, 2, effect); + moved.trigger = no::Limit{2.0}; + CHECK(core.replace(a, moved, 1, inc, ord, std::nullopt, keep).status + == no::ReplaceStatus::Replaced); + const auto f1 = member(core, 1.0, 0.0, 1, effect, inc, ord); + const no::EventId older = fill(core, f1, 1.0, ord); + CHECK(core.group_recipients(older) == (std::vector{b, c, a})); + // c is left out of the older cause's drain. + CHECK(effect_on(core, older, b, ord, direct).kind == 0); + CHECK(effect_on(core, older, a, ord, direct).kind == 0); + const auto f2 = member(core, 1.0, 0.0, 4, effect, inc, ord); + const no::EventId newer = fill(core, f2, 1.0, ord); + const auto later = core.group_recipients(newer); + CHECK(!later.empty()); + for (const auto& recipient : later) { + CHECK(effect_on(core, newer, recipient, ord, direct).kind == 0); + } + const std::size_t history = core.history_end(); + const std::size_t receipts = core.group_effect_receipt_count(); + for (const auto& recipient : {a, b}) { + CHECK(is_already_applied(effect_on(core, older, recipient, ord, false))); + CHECK(is_already_applied(effect_on(core, older, recipient, ord, true))); + } + const Step skipped_staged = effect_on(core, older, c, ord, false); + const Step skipped_direct = effect_on(core, older, c, ord, true); + CHECK(skipped_staged.kind == 2 && skipped_staged.code == no::CoreFailure::InvalidCause); + CHECK(skipped_direct.kind == 2 && skipped_direct.code == no::CoreFailure::InvalidCause); + CHECK(core.history_end() == history); + CHECK(core.group_effect_receipt_count() == receipts); + } + // A receipt for a cause older than the newest one's is still refused. + { + no::WorkingRequestCore core(kCoreRun); + std::uint64_t inc = 1; + std::uint64_t ord = 1; + const auto a = member(core, 5.0, 1.0, 2, effect, inc, ord); + const auto b = member(core, 5.0, 1.0, 3, effect, inc, ord); + const auto f1 = member(core, 1.0, 0.0, 1, effect, inc, ord); + const no::EventId older = fill(core, f1, 1.0, ord); + const auto f2 = member(core, 1.0, 0.0, 4, effect, inc, ord); + const no::EventId newer = fill(core, f2, 1.0, ord); + CHECK(older.ordinal < newer.ordinal); + CHECK(effect_on(core, newer, b, ord, direct).kind == 0); + const std::size_t history = core.history_end(); + const std::size_t receipts = core.group_effect_receipt_count(); + const Step late = effect_on(core, older, a, ord, direct); + CHECK(late.kind == 2 && late.code == no::CoreFailure::InvalidCause); + CHECK(core.history_end() == history); + CHECK(core.group_effect_receipt_count() == receipts); + CHECK(effect_on(core, newer, a, ord, direct).kind == 0); + const Step late_b = effect_on(core, older, b, ord, direct); + CHECK(late_b.kind == 2 && late_b.code == no::CoreFailure::InvalidCause); + CHECK(is_already_applied(effect_on(core, newer, a, ord, direct))); + CHECK(is_already_applied(effect_on(core, newer, b, ord, direct))); + } +} + +// ---- The seeded battery ------------------------------------------------------- + +enum class Kind : std::uint8_t { Entry, Exit, Child }; + +struct Slot { + int id = 0; + Kind kind = Kind::Exit; + std::optional handle; + int parent = -1; // a child's entry + int trigger = 0; // 0 limit, 1 stop, 2 trail, 3 market + int intent = 0; // exits: 0 explicit, 1 flatten, 2 fraction, 3 host-sized + bool buy = true; // entries + double units = 1.0; // entries and explicit exits + std::uint64_t group = 0; // 0: none + std::int64_t cohort = 0; + no::GroupEffect effect = no::GroupEffect::Cancel; + long level = 0; // quarter ticks, the last one placed +}; + +struct Tally { + long runs = 0, completed = 0, stopped = 0; + long fills = 0, group_fills = 0; + long reduced = 0, deferred = 0, group_cancels = 0; + long replaces = 0, kept = 0, retained = 0, cancels = 0; + long drains = 0; + long misordered = 0; // drains whose recipients came in other than incarnation order + long misordered_cancel = 0, misordered_reduce = 0; + long misordered_deferred = 0; // ... with a recipient whose units were deferred + void add(const Tally& o) { + runs += o.runs; completed += o.completed; stopped += o.stopped; + fills += o.fills; group_fills += o.group_fills; + reduced += o.reduced; deferred += o.deferred; group_cancels += o.group_cancels; + replaces += o.replaces; kept += o.kept; retained += o.retained; cancels += o.cancels; + drains += o.drains; misordered += o.misordered; + misordered_cancel += o.misordered_cancel; misordered_reduce += o.misordered_reduce; + misordered_deferred += o.misordered_deferred; + } +}; + +struct Record { + std::vector fills; // one word per fill + std::vector events; // one word per journal event + std::vector bars; // queue order and position, every bar + std::vector answers; // replace and cancel answers + std::vector receipts; // group-effect receipts, in commit order + std::vector trades; + std::vector trades_no_incarnation; + std::vector hashes; // continuation every bar (paths only) + std::uint64_t broker = 0; + std::string error; + bool completed = false; + unsigned code = 0, discriminator = 0; + Tally tally; +}; + +class GroupHost final : public NativeStrategyHost { +public: + GroupHost(std::uint64_t seed, Mode mode, bool direct) + : seed_(seed), mode_(mode), rng_(seed) { + as_native_consumer(execution_consumer()).set_direct_mutation(direct); + } + Record record; + + void on_native_run_begin() override { + rng_ = k3_book::Rng(seed_); + slots_.clear(); + by_incarnation_.clear(); + next_group_ = 1; + bar_ = 0; + } + + void on_native_bar(const Bar& bar, const NativeDecisionContext&) override { + ++bar_; + ref_ = static_cast(bar.close * 4.0); + refresh(); + observe_bar(); + // A flat book is entered at the market now and then, so the closes + // have a position to bind to. + if (physical_position().signed_units == 0.0 && rng_.percent(40)) { + Slot entry; + entry.kind = Kind::Entry; + entry.buy = rng_.percent(50); + entry.units = static_cast(rng_.between(2, 6)); + entry.trigger = 3; + place(entry); + } + if (rng_.percent(45)) new_group(); + if (rng_.percent(30)) join_group(); + reprice_some(rng_.between(0, 4)); + if (rng_.percent(12)) cancel_one(); + // A bounded book. + while (live_count() > 28) cancel_one(); + } + + void on_native_applied(const no::ExecutionAppliedEvent& event, + const NativeDecisionContext&) override { + const int slot = slot_of(event.handle()); + full_fold::Fold f; + f.i(slot); + f.u(event.ordinal); + f.u(event.cursor.point.ordinal); + f.i(event.cursor.point.interval_index); + f.i(event.cursor.point.effective_time_ms); + f.e(event.cursor.point.path_phase); + f.d(event.cursor.t); + f.d(event.raw_price); + f.d(event.resolved_price); + f.d(event.closed_units); + f.d(event.opened_units); + f.d(event.filled_working); + f.b(event.terminal); + f.i(event.cycle_before); + f.i(event.cycle_after); + record.fills.push_back(f.h); + ++record.tally.fills; + if (std::holds_alternative(event.request().group)) ++record.tally.group_fills; + // Commands from the fill callback: a re-price on the rest of the path, + // and now and then a new member. + if (rng_.percent(30)) reprice_some(1); + if (rng_.percent(10)) join_group(); + } + + no::ExecutionTerms resolve_execution_terms( + const NativeExecutionTermsFacts& facts) const override { + no::ExecutionTerms terms{facts.default_resolved_price, std::nullopt, + no::OpeningShape::Transact}; + if (std::holds_alternative(facts.definition->request.intent)) { + const double scope = facts.scope_exposure_units; + terms.units = scope > 0.0 ? (scope < 2.0 ? scope : 2.0) : 1.0; + } + return terms; + } + + const no::WorkingRequestCore& core() const { + return as_native_consumer(execution_consumer()).request_core(); + } + + int slot_of(const no::RequestHandle& handle) const { + const auto found = by_incarnation_.find(handle.incarnation); + return found == by_incarnation_.end() ? -1 : found->second; + } + +private: + void observe_bar() { + full_fold::Fold f; + f.i(bar_); + for (const auto& live : core().live()) f.i(slot_of(live.handle())); + f.d(physical_position().signed_units); + f.u(physical_position().lot_count); + record.bars.push_back(f.h); + record.hashes.push_back(native_continuation_hash()); + } + + void refresh() { + for (auto& slot : slots_) { + if (slot.handle && !core().find_live(*slot.handle)) slot.handle.reset(); + } + } + + long live_count() const { + return static_cast(std::count_if(slots_.begin(), slots_.end(), + [](const Slot& s) { return s.handle.has_value(); })); + } + + bool has_waiting_children(int parent) const { + for (const auto& slot : slots_) { + if (slot.kind != Kind::Child || slot.parent != parent || !slot.handle) continue; + const auto* live = core().find_live(*slot.handle); + if (live && std::holds_alternative(live->authority)) return true; + } + return false; + } + + no::Request request_for(const Slot& slot, bool retain) const { + no::Request request; + if (slot.kind == Kind::Entry) { + request.intent = no::Transact{slot.buy ? slot.units : -slot.units}; + request.label = "e" + std::to_string(slot.id); + } else { + switch (slot.intent) { + case 0: request.intent = no::Reduce{no::ExplicitUnits{slot.units}}; break; + case 1: request.intent = no::Flatten{}; break; + case 2: request.intent = no::Reduce{no::ScopeFraction{0.5}}; break; + default: request.intent = no::HostSized{no::HostSizedKind::Close, std::nullopt}; break; + } + request.label = "x" + std::to_string(slot.id); + } + if (slot.kind == Kind::Child) { + const auto& parent = slots_[static_cast(slot.parent)]; + no::WaitForApplied wait; + wait.parent = parent.handle ? *parent.handle : no::RequestHandle{}; + wait.scope = slot.intent == 1 ? no::NativeArmScope::Book : no::NativeArmScope::OwnerLot; + request.owner = wait; + if (slot.intent != 1) request.intent = no::Reduce{no::OwnerOpenedUnits{}}; + } + switch (slot.trigger) { + case 0: request.trigger = no::Limit{ticks(slot.level)}; break; + case 1: request.trigger = no::Stop{ticks(slot.level)}; break; + case 2: { + no::Trail t; + t.offset = ticks(2 + (slot.id % 5)); + if (!retain) t.arm_price = ticks(slot.level); + request.trigger = t; + break; + } + default: break; + } + if (slot.group != 0) request.group = no::Member{slot.group, slot.cohort, slot.effect}; + return request; + } + + void place(Slot slot) { + slot.id = static_cast(slots_.size()); + if (slot.group != 0 && slot.cohort == 0) slot.cohort = slot.id + 1; + const auto result = submit(request_for(slot, false)); + if (result.handle) { + slot.handle = *result.handle; + by_incarnation_[result.handle->incarnation] = slot.id; + } + full_fold::Fold f; + f.i(slot.id); + f.e(result.status); + f.b(result.reason.has_value()); + if (result.reason) f.e(*result.reason); + record.answers.push_back(f.h); + slots_.push_back(slot); + } + + // One member of `group`: an entry, a close, a bracket leg waiting for an + // entry, or a market member whose fill drains the group at the next point. + void add_member(std::uint64_t group, no::GroupEffect effect, std::int64_t cohort) { + Slot slot; + slot.group = group; + slot.effect = effect; + slot.cohort = cohort; + const int pick = rng_.below(100); + const long away = rng_.between(1, 14); + if (pick < 30) { + slot.kind = Kind::Entry; + slot.buy = rng_.percent(50); + slot.units = static_cast(rng_.between(1, 3)); + slot.trigger = rng_.below(2); + // A buy limit rests below the market, a buy stop above it. + const bool below = slot.buy == (slot.trigger == 0); + slot.level = ref_ + (below ? -away : away); + } else if (pick < 62) { + slot.kind = Kind::Exit; + slot.intent = rng_.below(4); + slot.units = static_cast(rng_.between(1, 2)); + slot.trigger = rng_.below(3); + slot.level = ref_ + (rng_.percent(50) ? away : -away); + } else if (pick < 82) { + // A leg waiting for a working entry, or a new entry of its own. + int parent = -1; + std::vector entries; + for (const auto& s : slots_) { + if (s.kind == Kind::Entry && s.trigger != 3 && s.handle) entries.push_back(s.id); + } + if (!entries.empty() && rng_.percent(70)) { + parent = entries[static_cast(rng_.below(static_cast(entries.size())))]; + } else { + Slot entry; + entry.kind = Kind::Entry; + entry.buy = rng_.percent(50); + entry.units = static_cast(rng_.between(1, 3)); + entry.trigger = rng_.percent(50) ? 3 : 0; + entry.level = ref_ + (entry.buy ? -away : away); + place(entry); + if (!slots_.back().handle) return; + parent = slots_.back().id; + } + const Slot& owner = slots_[static_cast(parent)]; + slot.kind = Kind::Child; + slot.parent = parent; + slot.intent = rng_.percent(25) ? 1 : 0; + slot.trigger = rng_.below(3); + slot.level = ref_ + ((owner.buy == (slot.trigger == 0)) ? away : -away); + } else { + // A market member: its fill drains the group. + if (rng_.percent(50)) { + slot.kind = Kind::Entry; + slot.buy = rng_.percent(50); + slot.units = static_cast(rng_.between(1, 2)); + } else { + slot.kind = Kind::Exit; + slot.intent = rng_.percent(70) ? 0 : 2; + slot.units = 1.0; + } + slot.trigger = 3; + } + place(slot); + } + + void new_group() { + const std::uint64_t group = next_group_++; + const auto effect = rng_.percent(50) ? no::GroupEffect::Cancel : no::GroupEffect::Reduce; + const int members = rng_.between(2, 5); + std::int64_t shared = 0; + for (int i = 0; i < members; ++i) { + // Now and then a member shares its predecessor's cohort, which no + // fill of the other reaches. + const std::int64_t cohort = shared != 0 && rng_.percent(15) ? shared : 0; + add_member(group, effect, cohort); + shared = slots_.back().cohort; + } + } + + void join_group() { + std::vector members; + for (const auto& slot : slots_) { + if (slot.group != 0 && slot.handle) members.push_back(slot.id); + } + if (members.empty()) return; + const Slot& other = + slots_[static_cast(members[static_cast(rng_.below(static_cast(members.size())))])]; + add_member(other.group, other.effect, 0); + } + + // Re-prices up to `count` live requests, picked and ordered at random. + void reprice_some(int count) { + std::vector live; + for (const auto& slot : slots_) { + if (slot.handle && slot.trigger != 3) live.push_back(slot.id); + } + for (std::size_t i = live.size(); i > 1; --i) { + const std::size_t j = static_cast(rng_.below(static_cast(i))); + std::swap(live[i - 1], live[j]); + } + for (int k = 0; k < count && k < static_cast(live.size()); ++k) { + if (slots_[static_cast(live[static_cast(k)])].handle) { + reprice(live[static_cast(k)]); + } + } + } + + void reprice(int id) { + Slot& slot = slots_[static_cast(id)]; + slot.level = ref_ + (slot.level - ref_) / 2 + rng_.between(-3, 3); + if (slot.level < 8) slot.level = 8; + const bool retain = slot.trigger == 2 && rng_.percent(50); + no::ReplaceOptions options; + options.retain_trigger_state = retain; + // A request whose children wait on it would end them under a plain + // replace and keep them under keep_handle: the host keeps its handle + // only where the two agree (R5 lane V19-D). + if (keeps_handle(mode_) && !has_waiting_children(id)) options.keep_handle = true; + if (keeps_binding(mode_)) options.keep_binding = true; + // A leg whose entry is gone names no parent; its replace is refused. + const auto result = replace(*slot.handle, request_for(slot, retain), options); + ++record.tally.replaces; + if (retain) ++record.tally.retained; + full_fold::Fold f; + f.i(slot.id); + f.e(result.status); + f.b(result.reason.has_value()); + if (result.reason) f.e(*result.reason); + record.answers.push_back(f.h); + if (result.status == no::ReplaceStatus::Replaced && result.successor) { + if (*result.successor == *slot.handle) ++record.tally.kept; + slot.handle = *result.successor; + by_incarnation_[result.successor->incarnation] = slot.id; + } else if (result.status != no::ReplaceStatus::ReplaceRejected) { + slot.handle.reset(); + } + } + + void cancel_one() { + std::vector live; + for (const auto& slot : slots_) { + if (slot.handle) live.push_back(slot.id); + } + if (live.empty()) return; + Slot& slot = slots_[static_cast(live[static_cast(rng_.below(static_cast(live.size())))])]; + const auto result = cancel(*slot.handle); + ++record.tally.cancels; + full_fold::Fold f; + f.i(slot.id); + f.e(result.status); + record.answers.push_back(f.h); + slot.handle.reset(); + } + + std::uint64_t seed_; + Mode mode_; + k3_book::Rng rng_; + std::vector slots_; + std::map by_incarnation_; + std::uint64_t next_group_ = 1; + long ref_ = 400; + int bar_ = 0; +}; + +// The journal mapped to the host's slots, ordinal for ordinal, and what a +// group effect wrote, with its cause. +void read_journal(const GroupHost& host, Record& record) { + auto cursor = [](full_fold::Fold& f, const no::MatchCursor& c) { + f.u(c.point.ordinal); + f.i(c.point.interval_index); + f.i(c.point.effective_time_ms); + f.e(c.point.path_phase); + f.d(c.t); + }; + for (const auto& row : host.native_events(0)) { + if (row.kind != NativeEventKind::Command || !row.command) continue; + full_fold::Fold f; + bool keep = true; + std::visit([&](const auto& event) { + using E = std::decay_t; + f.u(row.command->index()); + f.u(event.ordinal); + if constexpr (std::is_same_v) { + keep = false; + } else if constexpr (std::is_same_v) { + f.i(host.slot_of(event.handle())); + } else if constexpr (std::is_same_v) { + f.i(host.slot_of(event.successor())); + } else if constexpr (std::is_same_v) { + f.i(host.slot_of(event.handle())); + f.e(event.reason); + f.b(event.cause.has_value()); + if (event.cause) f.u(event.cause->ordinal); + if (event.reason == no::CancelReason::Group) ++record.tally.group_cancels; + } else if constexpr (std::is_same_v) { + f.i(host.slot_of(event.handle())); + cursor(f, event.cursor); + } else if constexpr (std::is_same_v) { + f.i(host.slot_of(event.handle())); + f.e(event.reason); + cursor(f, event.cursor); + } else if constexpr (std::is_same_v) { + f.i(host.slot_of(event.definition->handle)); + f.e(event.kind); + f.d(event.reached_price); + cursor(f, event.cursor); + } else if constexpr (std::is_same_v) { + f.i(host.slot_of(event.definition->handle)); + f.u(event.after.index()); + } else if constexpr (std::is_same_v) { + f.i(host.slot_of(event.recipient)); + f.u(event.cause.ordinal); + f.d(event.requested_delta); + f.d(event.actual_deduction); + f.d(event.before.q); + ++record.tally.reduced; + } else if constexpr (std::is_same_v) { + f.i(host.slot_of(event.recipient)); + f.u(event.cause.ordinal); + f.d(event.deferred_delta); + f.d(event.pending_after.total); + f.u(event.pending_after.count); + ++record.tally.deferred; + } else if constexpr (std::is_same_v) { + f.i(host.slot_of(event.handle())); + f.d(event.resolved_price); + f.d(event.filled_working); + cursor(f, event.cursor); + } else if constexpr (std::is_same_v) { + f.i(host.slot_of(event.handle())); + f.d(event.pending_total); + f.d(event.effective_deduction); + cursor(f, event.cursor); + } else if constexpr (std::is_same_v) { + f.i(host.slot_of(event.definition->handle)); + f.d(event.source_units); + f.d(event.pending_total); + f.d(event.effective_deduction); + } else { + // Rejections, NotWorking / InvalidHandle answers: the kind alone. + } + }, *row.command); + if (keep) record.events.push_back(f.h); + } +} + +// The group-effect receipts the core committed, in commit order, mapped to the +// host's slots; and the drains they record: one cause's receipts, counted +// misordered when their recipients came in other than incarnation order -- +// the drains the receipt check refused before this lane. +void read_receipts(const GroupHost& host, Record& record) { + const auto& core = host.core(); + // The outcome events that deferred a deduction into a recipient's pending + // total: their ordinals. + std::vector deferred; + for (const auto& row : host.native_events(0)) { + if (!row.command) continue; + if (const auto* d = std::get_if(&*row.command)) { + deferred.push_back(d->ordinal); + } + } + std::uint64_t cause = 0; + std::uint64_t last = 0; + bool ordered = true; + bool any_deferred = false; + no::GroupEffect effect = no::GroupEffect::Cancel; + auto close_drain = [&] { + if (cause == 0) return; + ++record.tally.drains; + if (ordered) return; + ++record.tally.misordered; + ++(effect == no::GroupEffect::Cancel ? record.tally.misordered_cancel + : record.tally.misordered_reduce); + if (any_deferred) ++record.tally.misordered_deferred; + }; + for (std::size_t index = 0; index < core.group_effect_receipt_count(); ++index) { + const auto receipt = core.group_effect_receipt(index); + full_fold::Fold f; + f.u(receipt.cause.ordinal); + f.i(host.slot_of(receipt.recipient)); + f.e(receipt.effect); + f.u(receipt.outcome_ordinal); + record.receipts.push_back(f.h); + if (receipt.cause.ordinal != cause) { + close_drain(); + cause = receipt.cause.ordinal; + ordered = true; + any_deferred = false; + effect = receipt.effect; + } else if (receipt.recipient.incarnation < last) { + ordered = false; + } + last = receipt.recipient.incarnation; + if (std::binary_search(deferred.begin(), deferred.end(), receipt.outcome_ordinal)) { + any_deferred = true; + } + } + close_drain(); +} + +void read_trades(const GroupHost& host, Record& record) { + for (int index = 0; index < host.trade_count(); ++index) { + const Trade& trade = host.get_trade(index); + full_fold::Fold f; + f.i(trade.entry_time); + f.i(trade.exit_time); + f.d(trade.entry_price); + f.d(trade.exit_price); + f.d(trade.qty); + f.d(trade.pnl); + f.b(trade.is_long); + f.i(trade.entry_bar_index); + f.i(trade.exit_bar_index); + f.s(trade.entry_id); + f.s(trade.exit_id); + f.d(trade.commission); + f.b(trade.open_at_end); + record.trades_no_incarnation.push_back(f.h); + f.u(trade.entry_incarnation); + record.trades.push_back(f.h); + } +} + +Record run_battery_one(std::uint64_t seed, Mode mode, bool direct) { + k3_book::BookConfig config; + config.seed = seed; + config.bars = 60; + config.path = static_cast(seed % 3); + config.calc_on_fills = seed % 4 == 1; + const k3_book::Tape tape = k3_book::make_tape(config); + NativeRunSpec spec = k3_book::make_spec(config, tape); + spec.identity = {"k-oca-keep-battery", 1}; + auto host = std::make_unique(seed, mode, direct); + CHECK(host->configure_native(spec).status == NativeSetupStatus::Applied); + host->run(tape.bars.data(), static_cast(tape.bars.size())); + Record record = std::move(host->record); + const auto state = host->native_state(); + record.completed = state.kind == NativeLifecycleKind::Completed; + record.code = static_cast(state.failure.code); + record.discriminator = static_cast(state.failure.discriminator); + record.error = host->last_error(); + record.broker = host->broker_state_hash(); + read_journal(*host, record); + read_receipts(*host, record); + read_trades(*host, record); + record.tally.runs = 1; + record.tally.completed = record.completed ? 1 : 0; + record.tally.stopped = record.completed ? 0 : 1; + return record; +} + +bool same_run(const Record& a, const Record& b, bool incarnations) { + return a.fills == b.fills && a.events == b.events && a.bars == b.bars + && a.answers == b.answers && a.receipts == b.receipts && a.error == b.error + && a.completed == b.completed + && (incarnations ? a.trades == b.trades + : a.trades_no_incarnation == b.trades_no_incarnation); +} + +Tally battery(std::uint64_t seed, int scripts) { + Tally total; + long mismatched = 0; + long keep_stops = 0; + for (int i = 0; i < scripts; ++i) { + const std::uint64_t script = seed * 1000003ULL + static_cast(i); + Record reference; + for (const Mode mode : kModes) { + scenario = "battery-" + std::to_string(seed) + "-" + std::to_string(i) + "/" + + mode_name(mode); + const Record staged = run_battery_one(script, mode, false); + const Record direct = run_battery_one(script, mode, true); + // The two core paths of one option set: bit for bit. + CHECK(same_run(staged, direct, true)); + CHECK(staged.hashes == direct.hashes && staged.broker == direct.broker); + CHECK(staged.code == direct.code && staged.discriminator == direct.discriminator); + if (!staged.completed) { + if (keeps_handle(mode)) ++keep_stops; + std::printf(" %s STOPPED code=%u disc=%u\n", scenario.c_str(), staged.code, + staged.discriminator); + } + CHECK(staged.completed); + if (mode == Mode::Replace) { + reference = staged; + // The plain replace's drains always come in incarnation order. + CHECK(staged.tally.misordered == 0); + } else { + // Every option set records the plain replace's run. + const bool same = same_run(staged, reference, false); + CHECK(same); + if (!same) ++mismatched; + } + total.add(staged.tally); + } + } + scenario = "battery-" + std::to_string(seed); + std::printf("battery seed %llu: scripts=%d runs=%ld completed=%ld STOPPED=%ld (keep_handle %ld) " + "mismatched=%ld fills=%ld group=%ld reduced=%ld deferred=%ld cancelled=%ld " + "replaces=%ld kept=%ld retained=%ld cancels=%ld drains=%ld misordered=%ld " + "(cancel %ld, reduce %ld, deferred %ld)\n", + static_cast(seed), scripts, total.runs, total.completed, + total.stopped, keep_stops, mismatched, total.fills, total.group_fills, + total.reduced, total.deferred, total.group_cancels, total.replaces, total.kept, + total.retained, total.cancels, total.drains, total.misordered, + total.misordered_cancel, total.misordered_reduce, total.misordered_deferred); + CHECK(total.stopped == 0); + CHECK(mismatched == 0); + // Every seed reaches the shape this row is about, under both effects and + // for a recipient whose units are deferred. + CHECK(total.misordered_cancel > 0 && total.misordered_reduce > 0); + CHECK(total.misordered_deferred > 0); + CHECK(total.reduced > 0 && total.deferred > 0 && total.group_cancels > 0); + return total; +} + +} // namespace + +int main() { + for (const auto effect : {no::GroupEffect::Reduce, no::GroupEffect::Cancel}) { + probe(effect); + trail(effect); + bound(effect); + core_rows(effect, false); + core_rows(effect, true); + } + Tally all; + for (std::uint64_t seed : {11ULL, 22ULL, 33ULL}) all.add(battery(seed, 30)); + scenario = "battery"; + CHECK(all.kept > 0 && all.retained > 0); + std::printf("%d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} From 55d3a61a24ae5023402f1e3b264c8d0d58a5feb4 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 05:16:05 +0800 Subject: [PATCH 25/83] Docs: pf_native_state_v1::failure_discriminator says what the runtime writes (R5 lane K-OCA-KEEP, K-ULP5 finding 3) native_c_api.h said the field "reads 0" because the kernel records no discriminator on this surface. strategy_native_state_v1 (src/native_c_host.cpp) copies NativeFailure::discriminator, which the consumer sets to the request core's CoreFailure for a refused preparation (code CONTRACT or SETTLEMENT_FAILURE; p5b's stopped run on the C host read 7, UnrepresentableReservation), to the InstallError of a refused install (CONTRACT), to the settlement's execution::Status for a failed inspection or commit (SETTLEMENT_FAILURE) and to a staged FX curve's NativeFxCurveError (INVALID_SPECIFICATION); it is 0 otherwise, as on every completed run (test_native_c_api.c checks that). Comment-only: every object whose dependencies name native_c_api.h (libpineforge.a, libpineforge_kernel.a, test_native_c_api and 76 kernel test TUs, 121 objects) is byte-identical compiled with the header before and after, and so are direct compiler runs of c_abi.cpp, native_c_host.cpp and native_execution_consumer.cpp (full and kernel) and test_native_c_api.c. No page repeated the claim: the pages that name a discriminator (native-engine.md, native-settlement.md) already give the runtime's values. The header's ten new lines move the citations of later lines, re-anchored from ad7a10f5 (89 anchors, digits only; 1678/1678). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit a25a3ce4caa8096cf4e33a7fb327addc6373d315) INT26 pick note: INT25's C-SURFACE-1 had already replaced the "reads 0" comment this commit corrects, with a shorter one ("The kernel's opaque durable failure discriminator; 0 when no more specific reason was recorded"). The merged comment is this commit's, which names what the runtime writes for each failure kind; both say the same of a completed run. The four pages' hunks are this lane's re-anchor and merged on digits only; they are re-anchored again once on the integrated tree (INT26 item vii). --- docs/adr/0001-kernel-adapter-boundary.md | 30 +++--- docs/pages/native-engine.md | 4 +- docs/pages/pine-to-native.md | 116 +++++++++++------------ include/pineforge/native_c_api.h | 16 +++- 4 files changed, 87 insertions(+), 79 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 3c03142f..f7bbc2c2 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -368,11 +368,11 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `src/c_abi.cpp` implements the **58** codegen-facing runtime `PF_API` symbols that `scripts/check_c_abi_runtime.py` pins by name, and `src/native_c_host.cpp` implements the **43** additive symbols of ``. A C host creates a host from a - callback table (`strategy_native_host_create_v1` `native_c_api.h:2639`), runs a batch - (`strategy_native_run_v1` `native_c_api.h:2652`), and **submits, replaces, cancels and - executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2680`), `_replace_v1` - (`native_c_api.h:2702`), `_cancel_v1` (`native_c_api.h:2742`), `_cancel_all_v1` (`native_c_api.h:2746`), `_cancel_where_v1` (`native_c_api.h:2767`), - `_execute_current_v1` (`native_c_api.h:2788`) — under the kernel's own legality rule. Streaming needs no new + callback table (`strategy_native_host_create_v1` `native_c_api.h:2638`), runs a batch + (`strategy_native_run_v1` `native_c_api.h:2651`), and **submits, replaces, cancels and + executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2679`), `_replace_v1` + (`native_c_api.h:2701`), `_cancel_v1` (`native_c_api.h:2741`), `_cancel_all_v1` (`native_c_api.h:2745`), `_cancel_where_v1` (`native_c_api.h:2766`), + `_execute_current_v1` (`native_c_api.h:2787`) — under the kernel's own legality rule. Streaming needs no new symbol: the whole `strategy_stream_*` family takes that handle unchanged. The header's COVERAGE block carries one line per public member of `NativeStrategyHost` with either its C spelling or the reason it has none, and `scripts/check_native_c_api_surface.py` proves the @@ -674,20 +674,20 @@ number names its symbol on this tree. | C++ hook | C route | witness in `tests/test_native_c_api.c` | |---|---|---| -| `WaitForApplied::first_match` / `WaitForApplied::scope` | `arm_first_match` (`native_c_api.h:2178`), `pf_native_request_v1`'s fifth published layout; a Book-scoped child may be the host-sized close `on_close_units` sizes | the arm-relation scenario | -| `native_sized_units` | `strategy_native_sized_units_v1` (`native_c_api.h:3053`), reading a SIZED request's own sizing block | the sizing-query scenario | -| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2564`): price, opening shape, grid policy, beside the units half `on_close_units` | the terms-hook scenario | -| `validate_execution_precommit` | `on_precommit` (`native_c_api.h:2573`): the plan, the inspection and the projected account, the closed rows' P&L borrowed for the call | the precommit scenario | -| `resolve_anchored_level` | `on_anchored_level` (`native_c_api.h:2580`) | the anchored-level scenario | -| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2591`): a 64-bit digest folded after the kernel's own bytes | the hash-extension scenario | +| `WaitForApplied::first_match` / `WaitForApplied::scope` | `arm_first_match` (`native_c_api.h:2180`), `pf_native_request_v1`'s fifth published layout; a Book-scoped child may be the host-sized close `on_close_units` sizes | the arm-relation scenario | +| `native_sized_units` | `strategy_native_sized_units_v1` (`native_c_api.h:3048`), reading a SIZED request's own sizing block | the sizing-query scenario | +| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2565`): price, opening shape, grid policy, beside the units half `on_close_units` | the terms-hook scenario | +| `validate_execution_precommit` | `on_precommit` (`native_c_api.h:2574`): the plan, the inspection and the projected account, the closed rows' P&L borrowed for the call | the precommit scenario | +| `resolve_anchored_level` | `on_anchored_level` (`native_c_api.h:2581`) | the anchored-level scenario | +| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2592`): a 64-bit digest folded after the kernel's own bytes | the hash-extension scenario | **What stays as it is, and why.** | surface | ruling | reason | |---|---|---| -| `cancel_all` (`native_host.hpp:1255`) / `cancel_where` answer a count, not a `CancelResult` per request (E12 f7) | **retained** | A bulk cancel is many commands in one call, and each withdrawn request records its own `CancelledEvent` with its own `CancelReason` — dependants of a cancelled owner included — in the history a host already polls (`strategy_native_events_v1`). That is the per-request answer; the count is the call's summary. A result vector would restate the history in a second, allocating shape that C could not take without a caller-sized array. The C spellings answer the same count (`strategy_native_cancel_all_v1` `native_c_api.h:2746`). | +| `cancel_all` (`native_host.hpp:1255`) / `cancel_where` answer a count, not a `CancelResult` per request (E12 f7) | **retained** | A bulk cancel is many commands in one call, and each withdrawn request records its own `CancelledEvent` with its own `CancelReason` — dependants of a cancelled owner included — in the history a host already polls (`strategy_native_events_v1`). That is the per-request answer; the count is the call's summary. A result vector would restate the history in a second, allocating shape that C could not take without a caller-sized array. The C spellings answer the same count (`strategy_native_cancel_all_v1` `native_c_api.h:2745`). | | `cohort_add` (`native_host.hpp:1273`) / `cohort_remove` answer `void` (E12 f7) | **retained; the typed receipt is a named follow-up** | The kernel judges every enrolment into a `CohortReceipt` (Applied, InvalidHandle, UnknownOrigin, TerminalOrigin) and folds it into the continuation (`hash_cohort_receipt` `native_execution_consumer.cpp:881`, since v19 once, at the enrolment, into the running digest the continuation carries), so a replay that diverges there diverges in the hash. Nothing that matches or settles reads it: a roster is a relation the close reads at its match, and a refused enrolment is a member the close does not take. A host that needs the receipt AT the call needs `NativeStrategyHost::cohort_add_result` first, a kernel-header change outside this lane; its C spelling would then be an `_ext_v1` with a receipt word. Until then `strategy_native_cohort_add_v1`'s `PF_NATIVE_OK` means the enrolment was issued. | -| `native_sized_units` (`native_host.hpp:1302`) / `native_liquidation_price` answer `std::optional` (E12 f7) | **retained** | Observation queries, not commands: each empty is documented cause by cause in its own comment (unconfigured, non-positive money or denominator, a below-one-step quotient; no margin model, no maintenance fraction for the side, a flat book, no finite solution), and every cause is a fact the host can read itself. A typed reason would name what the caller already knows. C answers `PF_NATIVE_ABSENT` and NaN (`strategy_native_liquidation_price_v1` `native_c_api.h:3025`). | +| `native_sized_units` (`native_host.hpp:1302`) / `native_liquidation_price` answer `std::optional` (E12 f7) | **retained** | Observation queries, not commands: each empty is documented cause by cause in its own comment (unconfigured, non-positive money or denominator, a below-one-step quotient; no margin model, no maintenance fraction for the side, a flat book, no finite solution), and every cause is a fact the host can read itself. A typed reason would name what the caller already knows. C answers `PF_NATIVE_ABSENT` and NaN (`strategy_native_liquidation_price_v1` `native_c_api.h:3020`). | | `pf_native_working_v1` without the leg's anchor or owner relation (E13 f4) | **carried** | The C++ host reads them off `NativeWorkingRequest::definition`, so the C readout's relation tail carries them: `anchor` (`native_c_api.h:1683`) through `arm_scope`, the request as the kernel holds it now (an armed leg reads ABSOLUTE with its installed level). | | `hash_source_extension` (`engine.hpp:411`) | **C++-only** | The deprecated spelling of `hash_host_extension`, kept so an existing C++ subclass compiles and folds unchanged. A C host has only ever had the current spelling, `on_hash_extension`. | | `NativeReportPolicy::KernelRecordedAtHostMarks` (`native_run_spec.hpp:64`) | **C++-only** (lane E22's ruling, restated) | Under it the host names each report point from inside its own callbacks, and the C table has no call that marks one: a C host could only declare a series nobody records. `pf_native_report_policy_e` (`native_c_api.h:657`) leaves its value unnamed, and the enum guard holds that exclusion. A C++ host now reaches the same kernel producer through `NativeStrategyHost::mark_native_report_point`; a mark outside a running HostMarks run returns false. | @@ -697,7 +697,7 @@ number names its symbol on this tree. | `strategy_configure_native_fx_curve_v1` (`pineforge.h:584`) | **typed additive route** | `strategy_configure_native_fx_curve_ext_v1` writes the C twin of `NativeFxCurveValidation` and its offending index. The old `-1` route remains unchanged, while a C caller can now distinguish a decreasing timestamp, non-positive rate, allocation failure or wrong phase. | | `on_native_timeframe_bar` (`native_host.hpp:873`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:797`) | **query routed** | The callback signature stays frozen. `strategy_native_timeframe_bar_interval_v1` is valid only during that callback and copies the kernel calendar interval without deriving it from the delivered bar or delivery timestamp. | | `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7645-7657`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1077-1082`) sees the same values, so no hash contract moves. | -| `pf_native_lot_excursion_v1` (`native_c_api.h:1869`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | +| `pf_native_lot_excursion_v1` (`native_c_api.h:1871`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | ## TradingView-calibrated kernel mechanisms (rule 2's own account) @@ -755,7 +755,7 @@ The table records, at the tree the doc wave read, what each block described and | `ta::StdDev` `include/pineforge/ta.hpp:430` (the `TradingView` comment `sha256:7cc1bbcd32dfe673285d51a8ca7080890ac551858099b523480d998f46408666` :420-429) | the population (biased) standard deviation | biased vs sample is a textbook choice, and the kernel states which | a per-instance switch is the *right* answer where two conventions genuinely compete, which is why `ta::EmaSeeding` exists and this one does not need it | `tests/test_ta_indicators_extras.cpp` | | `float_band_eq` `include/pineforge/ta_compare_band.hpp:38` | an absolute 1e-10 float comparison for indicator equality | an epsilon band is needed by any float comparison; the width is the calibration | a band-width field on a comparison the indicators share | `tests/test_dmi_parity.cpp`, `tests/test_ta_osc_edge.cpp` | | the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8606-8609` (lookahead) and `clear_if_gapped` `:8600-8603` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | -| `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2228`, `::gaps` `include/pineforge/native_c_api.h:2232` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:795`, `pf_native_gaps_e` `:806`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | +| `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2230`, `::gaps` `include/pineforge/native_c_api.h:2234` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:795`, `pf_native_gaps_e` `:806`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | | the authoritative-feed partition `src/engine_aux_security.cpp` | a feed is the venue's own bars of one timeframe; its stamps are the period partition | already ruled: design §2.ii row s, and the residual table's own row | the policy knob is installing the feed or not | `tests/test_native_htf_subscriptions.cpp`, `tests/test_native_security_feed.cpp` | | `session_template_knows_early_close` `src/engine_security.cpp` | the instrument-class classification | already ruled: design §2.ii row t; `SymInfo::type`'s vocabulary is fixed by the frozen C ABI | — | `tests/test_native_wm_buckets.cpp` | | `skip_entry_bar_high` / `skip_entry_bar_low` `include/pineforge/engine.hpp:153` | the intrabar-fill excursion mask: the part of a bar traversed before a priced entry filled is not that trade's excursion | the mask is generic — an excursion is measured from the fill — and since R5 lane E6 no host raises the flags: the excursion owner declares where its fill sat and the kernel derives the pair from the bar's path (`declare_opened_lot_entry_bar_mask` `src/engine_path_resolve.cpp:95`); with no declaration the kernel samples the whole bar | already spelled the generic way: `owns_lot_excursions()` + `closed_lot_excursion` hand the whole measurement to a host | `tests/test_l11a_host_excursion.cpp` | diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index fb6152f7..a11263dc 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -490,7 +490,7 @@ Serialized external C++ calls may command only **between realtime inputs**, never reentrantly during input processing. A host written in C issues the same five commands through `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` -(`native_c_api.h:2730-2780`), under the same legality rule; see *Driving the +(`native_c_api.h:2729-2779`), under the same legality rule; see *Driving the kernel from C* below. `native_order::Request` values belong to `native_order_v7` @@ -2931,7 +2931,7 @@ These are existing refusals, not implied future features: A C host has the same stream and the same commands. Streaming needs no new symbol — `strategy_stream_begin` and its family (`pineforge.h:748`) take a `pf_strategy_t` from `strategy_native_host_create_v1` unchanged — and -`strategy_native_submit_v1` (`native_c_api.h:2680`) obeys the one legality +`strategy_native_submit_v1` (`native_c_api.h:2679`) obeys the one legality rule its C++ spelling does. Rebuild strategy libraries against this engine. An ABI-v4 module without the diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 49ce1d5b..bb115364 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -114,8 +114,8 @@ A Pine script declares its broker once, in the `strategy()` call, and inherits the rest from the chart. A native host declares *everything* in one `NativeRunSpec` (native_run_spec.hpp:553) applied by `configure_native` (native_host.hpp:1184); a C host sends `pf_native_run_spec_v1` -(pineforge.h:548) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2267) -through `strategy_configure_native_ext_v1` (native_c_api.h:3113). Setup is +(pineforge.h:548) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2269) +through `strategy_configure_native_ext_v1` (native_c_api.h:3108). Setup is atomic: `Ready` → begin → `Completed`, and an incomplete value is refused rather than defaulted. @@ -129,15 +129,15 @@ rather than defaulted. | `slippage` | `slippage_ticks` native_run_spec.hpp:583 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3958-3959. TradingView's round-then-slip order is adapter-only. | | `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7524-7525, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | | `pyramiding` | `max_open_lots` native_run_spec.hpp:595 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:2914). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). | -| `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:344 with `size_basis` native_c_api.h:2104 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | -| `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2108 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | -| `calc_on_order_fills` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2278 | `native_calc_on_fills_strategy.cpp` | `BarCloseAndFills` recalculates once at the cursor of each applied execution, bounded by `max_recalculations_per_point` native_run_spec.hpp:626. The generic cadence only: TradingView's waypoint-only refill, its two-fills-at-open rule and its script-state rollback stay in the adapter. | -| `calc_on_every_tick` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2278 | `native_calc_on_fills_strategy.cpp` | `EveryModeledPoint` additionally recalculates at every modeled point of the delivered path and at every observed print. In a stream, `on_native_tick` native_host.hpp:866 is one call per accepted realtime print whatever the trigger is. | -| `margin_long` | `NativeMarginModel::initial_long` native_run_spec.hpp:241 | `margin_initial_long` native_c_api.h:2288 | `native_margin_strategy.cpp` | A fraction, not a percent. `0.0` is the maintenance-only spelling for that side: the kernel enforces no opening requirement there and the host owns opening admission. | -| `margin_short` | `NativeMarginModel::initial_short` native_run_spec.hpp:242 | `margin_initial_short` native_c_api.h:2292 | `native_margin_strategy.cpp` | Per side, unlike the one-scalar `initial_margin_fraction` native_run_spec.hpp:597 the model replaces. `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 add the liquidation half Pine has no word for. | +| `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:344 with `size_basis` native_c_api.h:2106 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | +| `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2110 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | +| `calc_on_order_fills` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2280 | `native_calc_on_fills_strategy.cpp` | `BarCloseAndFills` recalculates once at the cursor of each applied execution, bounded by `max_recalculations_per_point` native_run_spec.hpp:626. The generic cadence only: TradingView's waypoint-only refill, its two-fills-at-open rule and its script-state rollback stay in the adapter. | +| `calc_on_every_tick` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2280 | `native_calc_on_fills_strategy.cpp` | `EveryModeledPoint` additionally recalculates at every modeled point of the delivered path and at every observed print. In a stream, `on_native_tick` native_host.hpp:866 is one call per accepted realtime print whatever the trigger is. | +| `margin_long` | `NativeMarginModel::initial_long` native_run_spec.hpp:241 | `margin_initial_long` native_c_api.h:2290 | `native_margin_strategy.cpp` | A fraction, not a percent. `0.0` is the maintenance-only spelling for that side: the kernel enforces no opening requirement there and the host owns opening admission. | +| `margin_short` | `NativeMarginModel::initial_short` native_run_spec.hpp:242 | `margin_initial_short` native_c_api.h:2294 | `native_margin_strategy.cpp` | Per side, unlike the one-scalar `initial_margin_fraction` native_run_spec.hpp:597 the model replaces. `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 add the liquidation half Pine has no word for. | | `close_entries_rule` | none — a native close names the lots it closes | — | `native_selected_strategy.cpp` | Pine picks FIFO or ANY for the whole script. A native reduce is explicit instead: `BindOpening` native_order.hpp:445 closes one opening's lots, `BindOpenings` native_order.hpp:454 a chosen set, a bare `Reduce` native_order.hpp:220 the book in FIFO order. The adapter keeps the run-level rule (`close_entries_rule` pine_adapter.hpp:88). | | `backtest_fill_limits_assumption` | none — a generic limit fills when the level is reached | — | `tests/test_native_order_core.cpp` | TradingView's assumption is "the price must exceed the limit by *n* ticks". The generic knob beside it is `Limit::fill_through` native_order.hpp:239, which makes a limit market-if-touched; the *n*-tick assumption itself is a TradingView rule and stays in the adapter. | -| `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:378 | `intrabar_kind` native_c_api.h:2331 | `tests/test_native_calc_timing.cpp` | The host owns the lower bars, as `lower_tf` native_run_spec.hpp:393 or `synthesized` native_run_spec.hpp:407. The C magnifier arguments of `run_backtest_full` pineforge.h:648 configure a *compiled Pine strategy*, never a native host. | +| `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:378 | `intrabar_kind` native_c_api.h:2333 | `tests/test_native_calc_timing.cpp` | The host owns the lower bars, as `lower_tf` native_run_spec.hpp:393 or `synthesized` native_run_spec.hpp:407. The C magnifier arguments of `run_backtest_full` pineforge.h:648 configure a *compiled Pine strategy*, never a native host. | | `fill_orders_on_standard_ohlc` | none — the kernel matches the bars you feed it | — | — | TradingView's switch exists because a Heikin-Ashi chart's candles are not the traded prices. A native host feeds the standard OHLC it wants matched, and derives any transformed series itself. | | `max_bars_back` | none — a native host owns its own history | — | `hello_kernel.cpp` | Pine infers a buffer length for every series. `pineforge::Series` series.hpp:94 is a fixed-capacity ring you size yourself, so nothing is inferred and nothing silently truncates. | | `risk_free_rate` | none — the ratio is a report field with TradingView's fixed rate | `sharpe_monthly` pineforge.h:276 | `native_sized_report_strategy.cpp` | `pf_metrics_t::equity.sharpe_monthly` and `pf_metrics_t::equity.sortino_monthly` (`sortino_monthly` pineforge.h:282) use a fixed 2 %/yr, the TradingView default; nothing in the kernel takes a declared rate. Their pre-1.0 spellings `sharpe_tv` / `sortino_tv` were removed for 1.0; the serialized report keys stay `sharpe_tv` / `sortino_tv`. A host that needs another rate computes it from `pf_report_t::equity_curve`. | @@ -280,18 +280,18 @@ Everything here is one call: `submit` native_host.hpp:1201, or `submit_market` native_host.hpp:1215 for the market-only shorthand, which *refuses* non-market extras rather than dropping them. `submit` returns a `RequestHandle` native_order_identity.hpp:135; keep it, it is the order id. In C the same call -is `strategy_native_submit_v1` native_c_api.h:2680 over a -`pf_native_request_v1` native_c_api.h:2095. +is `strategy_native_submit_v1` native_c_api.h:2679 over a +`pf_native_request_v1` native_c_api.h:2097. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2680 | `native_market_strategy.cpp` | Pine's entry *reverses* an opposite position; `Transact` does not. Spell the reversal explicitly with the row below. | -| `strategy.order()` | the same `Request` native_order.hpp:483 | `strategy_native_submit_v1` native_c_api.h:2680 | `native_market_strategy.cpp` | `strategy.order` is the non-reversing entry, so it maps 1:1 with no caveat. | -| `strategy.exit()` | `submit_bracket` native_toolkit.hpp:160 over a `BracketSpec` native_toolkit.hpp:37 | `strategy_native_submit_v1` native_c_api.h:2680 per leg | `native_bracket_strategy.cpp` | The full bracket is described below in [Brackets](@ref pine_to_native_brackets). | +| `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2679 | `native_market_strategy.cpp` | Pine's entry *reverses* an opposite position; `Transact` does not. Spell the reversal explicitly with the row below. | +| `strategy.order()` | the same `Request` native_order.hpp:483 | `strategy_native_submit_v1` native_c_api.h:2679 | `native_market_strategy.cpp` | `strategy.order` is the non-reversing entry, so it maps 1:1 with no caveat. | +| `strategy.exit()` | `submit_bracket` native_toolkit.hpp:160 over a `BracketSpec` native_toolkit.hpp:37 | `strategy_native_submit_v1` native_c_api.h:2679 per leg | `native_bracket_strategy.cpp` | The full bracket is described below in [Brackets](@ref pine_to_native_brackets). | | `strategy.close()` | `Reduce` native_order.hpp:220 with `OwnerOpenedUnits` native_order.hpp:191 and `BindOpening` native_order.hpp:445 | `PF_NATIVE_REDUCE_OWNER_OPENED` native_c_api.h:350 with `PF_NATIVE_OWNER_BIND_OPENING` native_c_api.h:455 | `native_selected_strategy.cpp` | Closes the lots one opening produced. The cycle comes from `ExecutionAppliedEvent::cycle_after` native_order.hpp:1165. A fractional close is `ScopeFraction` native_order.hpp:207 — `qty_percent = 50` is `fraction = 0.5`, with `ScopeClaim` native_order.hpp:195 and `ScopeBasis` native_order.hpp:202 saying whether siblings are netted and when the scope is measured. On a `quantity_grid` a fraction below 1 is floored onto the grid, and `qty_percent = 100` (`fraction = 1`) of the gross scope, settled in one fill, closes the whole scope unfloored. | | `strategy.close_all()` | `Flatten` native_order.hpp:36 | `PF_NATIVE_INTENT_FLATTEN` native_c_api.h:333 | `hello_kernel.cpp` / `hello_kernel_c.c` | The whole book, in one settlement cycle. | -| `strategy.cancel()` | `cancel_where` native_host.hpp:1266 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2767 | `native_trail_risk_strategy.cpp` | Withdraws exactly the live requests whose `Request::label` native_order.hpp:485 is that id, and answers how many. `cancel` native_host.hpp:1237 is the single-request form for a host that kept the handle. | -| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1255 | `strategy_native_cancel_all_v1` native_c_api.h:2746 | `native_trail_risk_strategy.cpp` | Neither field is indexed: every form walks the live book once. A `PendingUntilArmed` native_order.hpp:394 child is absent from `native_working_requests` native_host.hpp:1251 before its arm, and all three cancels still address it. | +| `strategy.cancel()` | `cancel_where` native_host.hpp:1266 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2766 | `native_trail_risk_strategy.cpp` | Withdraws exactly the live requests whose `Request::label` native_order.hpp:485 is that id, and answers how many. `cancel` native_host.hpp:1237 is the single-request form for a host that kept the handle. | +| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1255 | `strategy_native_cancel_all_v1` native_c_api.h:2745 | `native_trail_risk_strategy.cpp` | Neither field is indexed: every form walks the live book once. A `PendingUntilArmed` native_order.hpp:394 child is absent from `native_working_requests` native_host.hpp:1251 before its arm, and all three cancels still address it. | | `strategy.oca.cancel` | `GroupEffect::Cancel` native_order.hpp:466 | `PF_NATIVE_GROUP_CANCEL` native_c_api.h:474 | `native_bracket_strategy.cpp` | Membership is `Member` native_order.hpp:468 inside `Group` native_order.hpp:473. | | `strategy.oca.reduce` | `GroupEffect::Reduce` native_order.hpp:466 | `PF_NATIVE_GROUP_REDUCE` native_c_api.h:475 | `tests/test_native_resting_group_contract.cpp` | The sibling's quantity is reduced instead of cancelled. | | `strategy.oca.none` | `NoGroup` native_order.hpp:467 | `PF_NATIVE_GROUP_NONE` native_c_api.h:468 | `hello_kernel.cpp` | The default: no group, no sibling effect. | @@ -322,7 +322,7 @@ when a rejection's `RequestRejectReason` native_order.hpp:821 matters. | `stop=` | `Stop` native_order.hpp:246 | `PF_NATIVE_TRIGGER_STOP` native_c_api.h:388 | `native_price_grid_strategy.cpp` | Stop-limit is `StopLimit` native_order.hpp:252. | | `trail_points` | `FromOwnerFill{+ticks, true}` native_order.hpp:331 | `anchor = PF_NATIVE_ANCHOR_FROM_OWNER_FILL`, `anchor_offset = ticks`, `anchor_offset_in_ticks = 1` native_c_api.h:2112-2119 | `native_bracket_strategy.cpp` | Pine points are the arm distance from the entry, in ticks. The adapter lowers that anchor distance separately from the ride offset; it is not `TrailTicks`. | | `trail_offset` | `TrailTicks` native_order.hpp:293 (resolved into `Trail::offset` native_order.hpp:291) | `p1 = ticks`, `trail_offset_in_ticks = 1` native_c_api.h:2113-2117 | `native_trail_risk_strategy.cpp` | Pine offset is the ride distance behind the running best, also expressed in ticks. Zero is legal and means "ride the best"; the first adverse move past it exits. | -| `trail_price` | `Trail::arm_price` native_order.hpp:292 | `trail_has_arm_price` native_c_api.h:2118 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1080 (C: `strategy_native_trail_state_v1` native_c_api.h:2992). The kernel compares `arm_price` with the **raw** path. TradingView tests the activation on the **tick-quantized** bar, with or without an offset and the placement close included (R5 lanes E5 and E9's tapes), so the Pine adapter arms at the activation's half-tick threshold — a placement close whose tick reaches the activation arms there — and books from a running best that starts at the activation; a one-shot (`trail_offset` 0) books the tick its resting threshold stands for, rounded away from the position. Since R5 lane E14 that running best is the **kernel's** own: the trailing leg names the activation — or the favourable one of the activation and the placement print when that print armed it — in the generic `Trail::best_seed` native_order.hpp:294, so the ride no longer starts half a tick short of the activation. | +| `trail_price` | `Trail::arm_price` native_order.hpp:292 | `trail_has_arm_price` native_c_api.h:2120 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1080 (C: `strategy_native_trail_state_v1` native_c_api.h:2987). The kernel compares `arm_price` with the **raw** path. TradingView tests the activation on the **tick-quantized** bar, with or without an offset and the placement close included (R5 lanes E5 and E9's tapes), so the Pine adapter arms at the activation's half-tick threshold — a placement close whose tick reaches the activation arms there — and books from a running best that starts at the activation; a one-shot (`trail_offset` 0) books the tick its resting threshold stands for, rounded away from the position. Since R5 lane E14 that running best is the **kernel's** own: the trailing leg names the activation — or the favourable one of the activation and the placement print when that print armed it — in the generic `Trail::best_seed` native_order.hpp:294, so the ride no longer starts half a tick short of the activation. | `limit=` and `stop=` are listed once here rather than once per command, because a native trigger is a property of the `Request` native_order.hpp:483 @@ -377,10 +377,10 @@ of a predicate, and it reports the kernel verbatim too: `ReplaceResult` and `SubmitResult` of every command that actually reached the kernel, and `cancel_outcome` says whether a cancel was commanded at all. Pine's `strategy.cancel` tells a script neither. In C -each leg is its own `strategy_native_submit_v1` native_c_api.h:2680 with +each leg is its own `strategy_native_submit_v1` native_c_api.h:2679 with `PF_NATIVE_OWNER_WAIT_FOR_APPLIED` native_c_api.h:454, and its `first_match` and `scope` are the request's `arm_first_match` / `arm_scope` -native_c_api.h:2178-2179, so a C bracket child arms after its owner's print or over +native_c_api.h:2180-2181, so a C bracket child arms after its owner's print or over the whole book exactly as a C++ one does. The Pine adapter lowers its own queued relative legs exactly this way — a @@ -406,10 +406,10 @@ block is the `PF_NATIVE_SPEC_EXT_RISK` native_c_api.h:735 tail. | --- | --- | --- | --- | --- | | `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:596 | `allowed_open_directions` pineforge.h:541 | `native_risk_limits_strategy.cpp` | The one `strategy.risk.*` rule that is already the kernel's, per ADR-0001's `risk` ruling row. Rejects with `OpeningDirection` native_execution_consumer.cpp:2954. | | `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:594 | `max_abs_units` pineforge.h:542 | `tests/test_native_run_spec.cpp` | Tests the *resulting* book. TradingView gates the *live* book before the fill, so an entry is refused only once the book already holds the limit — ruled **adapter-policy** in ADR-0001's `max_abs_units` row, with lane N12's measurement (the adapter ends at 4 where the kernel cap ends at 2). | -| `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2314 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:259 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:261 is set. Blocks openings to the end of the run. | -| `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:312 | `risk_max_intraday_loss` native_c_api.h:2317 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | -| `strategy.risk.max_cons_loss_days()` | `NativeRiskLimits::max_consecutive_loss_days` native_run_spec.hpp:313 | `risk_max_consecutive_loss_days` native_c_api.h:2319 | `native_risk_limits_strategy.cpp` | Counts *days* that close with a realized loss; TradingView's streak counts trades. `NativeRiskDay` native_run_spec.hpp:270 chooses the session day or the civil date in the spec timezone. | -| `strategy.risk.max_intraday_filled_orders()` | `NativeRiskLimits::max_fills_per_day` native_run_spec.hpp:314 | `risk_max_fills_per_day` native_c_api.h:2321 | `native_trail_risk_strategy.cpp` | Applied fills counted as they settle, so the fills of one matching point all settle and the limit blocks the next admission. Read the ledger back with `native_risk_state` native_host.hpp:1311. | +| `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2316 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:259 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:261 is set. Blocks openings to the end of the run. | +| `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:312 | `risk_max_intraday_loss` native_c_api.h:2319 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | +| `strategy.risk.max_cons_loss_days()` | `NativeRiskLimits::max_consecutive_loss_days` native_run_spec.hpp:313 | `risk_max_consecutive_loss_days` native_c_api.h:2321 | `native_risk_limits_strategy.cpp` | Counts *days* that close with a realized loss; TradingView's streak counts trades. `NativeRiskDay` native_run_spec.hpp:270 chooses the session day or the civil date in the spec timezone. | +| `strategy.risk.max_intraday_filled_orders()` | `NativeRiskLimits::max_fills_per_day` native_run_spec.hpp:314 | `risk_max_fills_per_day` native_c_api.h:2323 | `native_trail_risk_strategy.cpp` | Applied fills counted as they settle, so the fills of one matching point all settle and the limit blocks the next admission. Read the ledger back with `native_risk_state` native_host.hpp:1311. | The whole block is **native-only by ruling**: no adapter run declares it, and lane N12 measured why — TradingView's drawdown latch samples at the close and @@ -428,19 +428,19 @@ liquidation level, and the ticket the forced close is booked under. | concept | C++ | C | Runs in | | --- | --- | --- | --- | -| opening requirement per side | `initial_long` native_run_spec.hpp:241 / `initial_short` native_run_spec.hpp:242 | `margin_initial_long` native_c_api.h:2288 | `native_margin_strategy.cpp` | -| maintenance requirement per side | `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 | `margin_maintenance_long` native_c_api.h:2293 | `native_margin_strategy.cpp` | -| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:140 | `margin_sizing` native_c_api.h:2283 | `native_margin_strategy.cpp` | -| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:158 | `margin_check` native_c_api.h:2284 | `native_margin_strategy.cpp` | -| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2351 | `tests/test_native_margin_model.cpp` | -| which base the level is solved from | `NativeLiquidationLevelBase` native_run_spec.hpp:182 | `margin_level_base` native_c_api.h:2352 | `tests/test_native_margin_model.cpp` | -| the forced close's ticket | `liquidation_label` native_run_spec.hpp:251 | `margin_liquidation_label` native_c_api.h:2353 | `native_margin_strategy.cpp` | -| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:948 | — C hosts gate through `on_precommit` native_c_api.h:2573 | `native_margin_strategy.cpp` | -| the host's own requirement | `resolve_margin_requirement` native_host.hpp:964 | `on_margin_requirement` native_c_api.h:2510 | `native_margin_strategy.cpp` | -| which check points run at all | `margin_check_allowed` native_host.hpp:976 | `on_margin_check` native_c_api.h:2519 | `native_margin_strategy.cpp` | +| opening requirement per side | `initial_long` native_run_spec.hpp:241 / `initial_short` native_run_spec.hpp:242 | `margin_initial_long` native_c_api.h:2290 | `native_margin_strategy.cpp` | +| maintenance requirement per side | `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 | `margin_maintenance_long` native_c_api.h:2295 | `native_margin_strategy.cpp` | +| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:140 | `margin_sizing` native_c_api.h:2285 | `native_margin_strategy.cpp` | +| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:158 | `margin_check` native_c_api.h:2286 | `native_margin_strategy.cpp` | +| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2353 | `tests/test_native_margin_model.cpp` | +| which base the level is solved from | `NativeLiquidationLevelBase` native_run_spec.hpp:182 | `margin_level_base` native_c_api.h:2354 | `tests/test_native_margin_model.cpp` | +| the forced close's ticket | `liquidation_label` native_run_spec.hpp:251 | `margin_liquidation_label` native_c_api.h:2355 | `native_margin_strategy.cpp` | +| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:948 | — C hosts gate through `on_precommit` native_c_api.h:2574 | `native_margin_strategy.cpp` | +| the host's own requirement | `resolve_margin_requirement` native_host.hpp:964 | `on_margin_requirement` native_c_api.h:2511 | `native_margin_strategy.cpp` | +| which check points run at all | `margin_check_allowed` native_host.hpp:976 | `on_margin_check` native_c_api.h:2520 | `native_margin_strategy.cpp` | | a step of the account FX curve, checked though no price moved | `NativeMarginCheckKind::FxRoll` native_host.hpp:542 | `PF_NATIVE_MARGIN_CHECK_FX_ROLL` native_c_api.h:931 | `native_fx_roll_strategy.cpp` | -| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:983 | `on_margin_call_units` native_c_api.h:2526 | `native_margin_strategy.cpp` | -| the liquidation event | `MarginCallEvent` native_order.hpp:1262 delivered to `on_native_margin_call` native_host.hpp:989 | `on_margin_call` native_c_api.h:2474 | `native_margin_strategy.cpp` | +| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:983 | `on_margin_call_units` native_c_api.h:2527 | `native_margin_strategy.cpp` | +| the liquidation event | `MarginCallEvent` native_order.hpp:1262 delivered to `on_native_margin_call` native_host.hpp:989 | `on_margin_call` native_c_api.h:2475 | `native_margin_strategy.cpp` | The one-scalar `initial_margin_fraction` native_run_spec.hpp:597 predates the model and remains the simple spelling; the two are mutually exclusive. It is @@ -455,15 +455,15 @@ part both ways (`tests/test_adapter_margin_schedule_differential.cpp`, M11). | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| script body at bar close | `on_native_bar` native_host.hpp:881 | `on_bar` native_c_api.h:2464 | `hello_kernel.cpp` / `hello_kernel_c.c` | The one pure virtual. Called once per closed script bar — and, with the default cadence, at every calculation there is: the kernel routes all of them through `on_native_recalculate` native_host.hpp:901, whose default forwards here. | -| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:901 with `NativeCalculationReason` native_host.hpp:813 | `on_recalculate` native_c_api.h:2492 | `native_calc_on_fills_strategy.cpp` | **Installing it REPLACES `on_native_bar` for every calculation** — in C++ you override the forwarding away, and in C a non-NULL `on_recalculate` is what the kernel calls instead of `on_bar`. A host that wants both calls the other itself from here; a C host that installs only `on_recalculate` and still expects `on_bar` silently trades nothing. | -| `barstate.isconfirmed` | `NativeCalculationReason::BarClose` native_host.hpp:814 | `on_recalculate` native_c_api.h:2492 | `native_calc_on_fills_strategy.cpp` | The script bar's own calculation is delivered for every run, whatever the cadence is. | +| script body at bar close | `on_native_bar` native_host.hpp:881 | `on_bar` native_c_api.h:2465 | `hello_kernel.cpp` / `hello_kernel_c.c` | The one pure virtual. Called once per closed script bar — and, with the default cadence, at every calculation there is: the kernel routes all of them through `on_native_recalculate` native_host.hpp:901, whose default forwards here. | +| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:901 with `NativeCalculationReason` native_host.hpp:813 | `on_recalculate` native_c_api.h:2493 | `native_calc_on_fills_strategy.cpp` | **Installing it REPLACES `on_native_bar` for every calculation** — in C++ you override the forwarding away, and in C a non-NULL `on_recalculate` is what the kernel calls instead of `on_bar`. A host that wants both calls the other itself from here; a C host that installs only `on_recalculate` and still expects `on_bar` silently trades nothing. | +| `barstate.isconfirmed` | `NativeCalculationReason::BarClose` native_host.hpp:814 | `on_recalculate` native_c_api.h:2493 | `native_calc_on_fills_strategy.cpp` | The script bar's own calculation is delivered for every run, whatever the cadence is. | | `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1756 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | -| bar-open pre-pass | `on_native_bar_open` native_host.hpp:877 | `on_bar_open` native_c_api.h:2463 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | -| the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1061 | `strategy_native_partial_bar_v1` native_c_api.h:2977 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | -| a fill, as it happens | `on_native_applied` native_host.hpp:926 | `on_applied` native_c_api.h:2466 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5374. | -| raw input before aggregation | `on_native_input` native_host.hpp:863 | `on_input` native_c_api.h:2461 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | -| a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:916 | `on_sub_bar` native_c_api.h:2502 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | +| bar-open pre-pass | `on_native_bar_open` native_host.hpp:877 | `on_bar_open` native_c_api.h:2464 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | +| the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1061 | `strategy_native_partial_bar_v1` native_c_api.h:2972 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | +| a fill, as it happens | `on_native_applied` native_host.hpp:926 | `on_applied` native_c_api.h:2467 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5374. | +| raw input before aggregation | `on_native_input` native_host.hpp:863 | `on_input` native_c_api.h:2462 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | +| a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:916 | `on_sub_bar` native_c_api.h:2503 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | | `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1454 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. | | `session.ismarket` | `in_session` market_driver.hpp:161 | `in_session` native_c_api.h:1472 | `tests/test_native_session_day_facts.cpp` | The script bar's label is in session on the run's own calendar (`NativeRunSpec::session` / `timezone`); a D/W/M bar holds whole session days, so it is always true there. The calendar applies a session's day mask to the session day's trading date: Sunday 17:00 CT of a CME-style `1700-1600:23456` is Monday's session, in session. Pine's own `session.ismarket` is still generated as the time-of-day predicate `pine_session_ismarket` session_time.hpp:247, whose mask reads each instant's weekday, so on such a masked overnight session the two differ: on six `lab tv` tapes TradingView flags every bar in market, the calendar's fact matches it on every bar, and the predicate misses every Sunday open under a weekday mask (`1700-1600:23456`) and every bar of `0000-2400`, which it cannot parse as a whole day (`tests/test_session_ismarket_tape.cpp`). Unmasked sessions (`1700-1600`, `1700-1700`, `24x7`, `0930-1600`) agree on all three. In C, read the three session bytes only when `session_facts` native_c_api.h:1471 is 1. | | `session.isfirstbar` | `opens_session_day` market_driver.hpp:162 | `opens_session_day` native_c_api.h:1473 | `tests/test_native_session_day_facts.cpp` | In session, and the bar before it is not, or is on another session day. The session day rolls at the session's first window start and is keyed to its trading date, so an overnight session is one day across local midnight (Tokyo `2230-0500`: its days open at 22:30). "The bar before" is the one the run holds — the batch input, the stream warmup — else the calendar's previous eligible input slot across declared breaks; a run's first bar opens its day. | @@ -553,10 +553,10 @@ range-end reporting is off. | `strategy.max_contracts_held_all` | `max_contracts_held_all` engine.hpp:1836 | — | `native_open_lots_strategy.cpp` | `pf_trade_stats_t` has no max-contracts-held field. A C host must derive the peak from position/open-lot observations; there is no direct C report counterpart. | | `strategy.max_contracts_held_long` | `max_contracts_held_long` engine.hpp:1837 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the long-side peak from position/open-lot observations. | | `strategy.max_contracts_held_short` | `max_contracts_held_short` engine.hpp:1838 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the short-side peak from position/open-lot observations. | -| `strategy.position_size` | `NativePhysicalPosition::signed_units` native_host.hpp:297 | `strategy_native_position_v1` native_c_api.h:2793 | `hello_kernel.cpp` | Signed: no separate direction field. | -| `strategy.position_avg_price` | `NativePhysicalPosition::average_price` native_host.hpp:298 | `strategy_native_position_v1` native_c_api.h:2793 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | -| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1285 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2834 | `native_open_lots_strategy.cpp` | Pine reports the newest entry's id; the native book lets you read any lot's. | -| `strategy.margin_liquidation_price` | `native_liquidation_price` native_host.hpp:1308 | `strategy_native_liquidation_price_v1` native_c_api.h:3025 | `native_margin_strategy.cpp` | Solved from the declared maintenance requirement, so it answers `nullopt` when the model declares none or the slope is degenerate (a long at full maintenance has no finite level). | +| `strategy.position_size` | `NativePhysicalPosition::signed_units` native_host.hpp:297 | `strategy_native_position_v1` native_c_api.h:2792 | `hello_kernel.cpp` | Signed: no separate direction field. | +| `strategy.position_avg_price` | `NativePhysicalPosition::average_price` native_host.hpp:298 | `strategy_native_position_v1` native_c_api.h:2792 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | +| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1285 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2833 | `native_open_lots_strategy.cpp` | Pine reports the newest entry's id; the native book lets you read any lot's. | +| `strategy.margin_liquidation_price` | `native_liquidation_price` native_host.hpp:1308 | `strategy_native_liquidation_price_v1` native_c_api.h:3020 | `native_margin_strategy.cpp` | Solved from the declared maintenance requirement, so it answers `nullopt` when the model declares none or the slope is degenerate (a long at full maintenance has no finite level). | | `strategy.convert_to_account()` | none — the kernel converts with the FX you declare | `strategy_configure_native_fx_curve_v1` pineforge.h:584 | `native_fx_roll_strategy.cpp` / `tests/test_native_fx_curve.cpp` | Pine's converter needs TradingView's own FX series. Natively, `account_fx` native_run_spec.hpp:581 is one scalar and `configure_native_fx_curve` native_host.hpp:1190 stages an immutable timestamped curve; codegen emits the Pine call as the identity, because no fixed table reproduces TradingView's series. | | `strategy.convert_to_symbol()` | none — same reason, the other direction | `strategy_configure_native_fx_curve_v1` pineforge.h:584 | `tests/test_native_fx_curve.cpp` | Same ruling. | @@ -566,8 +566,8 @@ range-end reporting is off. `native_open_lots(mark)` native_host.hpp:1285 answers one `NativeOpenLot` native_host.hpp:328 per open physical lot, in book order (oldest first, `NativePhysicalPosition::lot_count` native_host.hpp:299 rows). The C twin is -`strategy_native_open_lot_count_v1` native_c_api.h:2826 then -`strategy_native_open_lot_get_v1` native_c_api.h:2834 into a +`strategy_native_open_lot_count_v1` native_c_api.h:2825 then +`strategy_native_open_lot_get_v1` native_c_api.h:2833 into a `pf_native_open_lot_v1` native_c_api.h:1732, whose two strings borrow the snapshot until the next count call. `mark` is the price the three marked fields are computed at; Pine's builtins mark at the current `close`. Reading it moves @@ -581,7 +581,7 @@ native_order.hpp:445 names. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.opentrades` | `native_open_lots` native_host.hpp:1285 | `strategy_native_open_lot_count_v1` native_c_api.h:2826 | `native_open_lots_strategy.cpp` | The row count, which equals `lot_count` native_host.hpp:299. | +| `strategy.opentrades` | `native_open_lots` native_host.hpp:1285 | `strategy_native_open_lot_count_v1` native_c_api.h:2825 | `native_open_lots_strategy.cpp` | The row count, which equals `lot_count` native_host.hpp:299. | | `strategy.opentrades.entry_id()` | `entry_label` native_host.hpp:333 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The opening request's `label` native_order.hpp:485. | | `strategy.opentrades.entry_comment()` | `entry_comment` native_host.hpp:334 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The opening request's `comment` native_order.hpp:486. | | `strategy.opentrades.entry_bar_index()` | `entry_bar_index` native_host.hpp:336 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Script-bar index of the opening fill. | @@ -656,8 +656,8 @@ classifies its own rows by order family instead. Which leg closed a row is its | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `request.security()` | `NativeTimeframeSubscription` native_run_spec.hpp:485 in `subscriptions` native_run_spec.hpp:632, or `declare_timeframe_subscriptions` native_host.hpp:1124 inside `on_native_run_begin` native_host.hpp:860 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2898 | `native_htf_strategy.cpp` | A subscription is a series instance: several may share one timeframe. Completed buckets arrive at `on_native_timeframe_bar` native_host.hpp:873 and the latest is pulled with `native_series_bar` native_host.hpp:1107. `authoritative_bars` native_run_spec.hpp:487 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:795 and `pf_native_gaps_e` native_c_api.h:806 (lane E7). The Pine adapter runs its own plain sites through these same subscriptions, ruled **adapter-hook** in ADR-0001's `subscriptions` row. | -| `request.security_lower_tf()` | `NativeAuxiliaryFeed` native_run_spec.hpp:514 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:482 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3175 | `native_auxiliary_feed_strategy.cpp` | Not the same shape: Pine returns an intrabar *array* per bar, the kernel gives you a finer *series* routed by time. A host that wants the raw sub-bars puts them in `IntrabarPath` native_run_spec.hpp:378 and reads `on_native_sub_bar` native_host.hpp:916 instead. | +| `request.security()` | `NativeTimeframeSubscription` native_run_spec.hpp:485 in `subscriptions` native_run_spec.hpp:632, or `declare_timeframe_subscriptions` native_host.hpp:1124 inside `on_native_run_begin` native_host.hpp:860 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2897 | `native_htf_strategy.cpp` | A subscription is a series instance: several may share one timeframe. Completed buckets arrive at `on_native_timeframe_bar` native_host.hpp:873 and the latest is pulled with `native_series_bar` native_host.hpp:1107. `authoritative_bars` native_run_spec.hpp:487 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:795 and `pf_native_gaps_e` native_c_api.h:806 (lane E7). The Pine adapter runs its own plain sites through these same subscriptions, ruled **adapter-hook** in ADR-0001's `subscriptions` row. | +| `request.security_lower_tf()` | `NativeAuxiliaryFeed` native_run_spec.hpp:514 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:482 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3168 | `native_auxiliary_feed_strategy.cpp` | Not the same shape: Pine returns an intrabar *array* per bar, the kernel gives you a finer *series* routed by time. A host that wants the raw sub-bars puts them in `IntrabarPath` native_run_spec.hpp:378 and reads `on_native_sub_bar` native_host.hpp:916 instead. | | `barmerge.gaps_off` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:489 set false | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_HOLD` native_c_api.h:807 | `native_htf_strategy.cpp` | The delivered bucket stands until the next delivery replaces it. | | `barmerge.gaps_on` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:489 set true | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_CLEAR` native_c_api.h:809 | `native_htf_strategy.cpp` | The series is cleared on every input bar it delivers nothing on, so the pull answers empty — the native spelling of `na`. | | `barmerge.lookahead_off` | `NativeTimeframeSubscription::lookahead` native_run_spec.hpp:488 set false | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_COMPLETION` native_c_api.h:796 | `native_htf_strategy.cpp` | The default and the honest one: a bucket is delivered when it completes. | @@ -742,21 +742,21 @@ static int on_bar(void* user, const pf_bar_t* bar, const pf_native_decision_v1* } ``` -The host is a `pf_native_callbacks_v1` native_c_api.h:2456 table handed to -`strategy_native_host_create_v1` native_c_api.h:2639; the run is +The host is a `pf_native_callbacks_v1` native_c_api.h:2457 table handed to +`strategy_native_host_create_v1` native_c_api.h:2638; the run is `pf_native_run_spec_v1` pineforge.h:548 plus `pf_native_run_spec_ext_v1` -native_c_api.h:2267 through `strategy_configure_native_ext_v1` -native_c_api.h:3113, and `strategy_native_run_v1` native_c_api.h:2652 drives +native_c_api.h:2269 through `strategy_configure_native_ext_v1` +native_c_api.h:3108, and `strategy_native_run_v1` native_c_api.h:2651 drives the bars into a `pf_report_t` pineforge.h:469. Three rules govern the C door: 1. **Every struct is tagged and size-prefixed.** Set `struct_size` - native_c_api.h:2096 and `version` native_c_api.h:2097; a mismatch is refused + native_c_api.h:2098 and `version` native_c_api.h:2099; a mismatch is refused with a documented negative status and mutates nothing. 2. **A callback must not unwind, and must return 0.** A non-zero return latches `NativeFailureCode::CallbackException` native_host.hpp:69 and ends the run - `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2879. + `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2878. 3. **Installing `on_recalculate` replaces `on_bar`.** A C host that installs only the recalculate hook and still expects the bar hook silently trades nothing. diff --git a/include/pineforge/native_c_api.h b/include/pineforge/native_c_api.h index 6381ffd3..3084ca7e 100644 --- a/include/pineforge/native_c_api.h +++ b/include/pineforge/native_c_api.h @@ -1761,10 +1761,18 @@ typedef struct pf_native_state_v1 { * PF_NATIVE_FAILURE_CALLBACK_EXCEPTION for a callback * that returned non-zero. */ uint32_t failure_operation; /**< #pf_native_failure_operation_t. */ - uint32_t failure_discriminator; /**< The kernel's opaque durable failure - * discriminator; 0 when no more specific - * reason was recorded. Settlement - * failures can carry a nonzero core reason. */ + uint32_t failure_discriminator; /**< The kernel's own word beside the code, as C++ + * `NativeFailure::discriminator` holds it: the + * request core's `native_order::CoreFailure` for a + * refused preparation (CONTRACT or + * SETTLEMENT_FAILURE; 7 is + * `UnrepresentableReservation`), its `InstallError` + * for a refused install (CONTRACT), the settlement's + * `execution::Status` for a failed inspection or + * commit (SETTLEMENT_FAILURE), a staged FX curve's + * `NativeFxCurveError` (INVALID_SPECIFICATION); 0 + * when the failure carries none, as every completed + * run does. */ uint64_t failure_ordinal; /**< The point the failure was latched at, 0 when absent. */ uint64_t consumed_high_water; int64_t decision_floor_ms; From b364b858cbda562e52fc1355153961c91803caae Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 05:21:44 +0800 Subject: [PATCH 26/83] Kernel: a pending total a request's units absorb takes none, so the request settles them in one fill and a whole-scope close on a grid closes whole (R5 lane K-OCA-KEEP, K-ULP5 finding 2) Ruling (ADR-0001 rule-2 row "a request on a quantity grid whose pending group deduction its units absorb"): one definition of settling in one fill. Generic, no knob. K-ULP4's grid rule admits the book's own quantity -- a whole-scope ScopeFraction's held total, a host-sized close answered with its scope's total -- for a request that settles it in one fill: no point budget and no group deduction pending against it. settles_in_one_fill (src/native_execution_consumer.cpp) read any PendingDeferred as pending, but since K-ULP5 a pending total below half an ulp of the units is absorbed at the terms and takes nothing. Now settles_in_one_fill asks WorkingRequestCore::effective_host_units whether the pending total takes any; an absorbed one takes none. The three call sites (the fraction's resolution, the matcher's terms check, the current-execution preview) pass the units they test. What moved, reproduced on 2a03c658 (grid 0.1: a dust lot of one ulp closed by an OCA-Reduce sibling defers 5.55e-17 into F, which closes a 1.1000000000000227 lot): - F a whole-scope fraction: floored to 1.1000000000000001, it left a 2.2648549702353193e-14 dust lot (on f1af50dc, before K-ULP5, the run stopped: code 6, discriminator 7); now it closes the lot whole. - F a host-sized close answered with its scope's total: refused InvalidTerms and the lot left open, on f1af50dc as on 2a03c658 -- a run that completed before K-ULP5 too, found by the lane's review; now it closes the lot whole, matched at its stop or as the current execution, whose preview no longer answers InvalidTerms. The Pine adapter does not reach the rule (it emits no ScopeFraction, and it answers a close of its scope's whole total with ExplicitUnits, whose grid check never asks whether the request settles in one fill), and runs without a grid cannot move. Gate (the brief's): K-ULP4's battery (kulp_random_k4.cpp, unmodified) answers all 12,000 per-scenario hashes and every decimal-host run as on 2a03c658; K-ULP5's absorption and C-host batteries are unchanged (no grid); grid-fraction-oca-pending (a pending total that takes 0.3) is still floored. No pinned hash moved. The rows' own records move, 2a03c658 -> this commit (continuation / broker hash, staged = direct): grid-absorbed-fraction 2536ca6f7cf7d871 (same) / 7e5b9f14781aab5d -> 6c0d1f47982b48e9 grid-absorbed-host 973f81ffee75000a -> cd2c35e6c5ea9ece / 6da44867ad27aa55 -> 5f8c73fdb35d9258 grid-absorbed-host-current b549772a04b93de4 -> 6d442bf4aac15221 / 61aa04ffcfa15269 -> 3d3affc848c26292 Tests: grid-absorbed-fraction, -host and -host-current in test_native_group_keep_handle, staged and direct (hashes compared); the current row checks the preview and the execution itself (its result is the applied close of the residue, and F is gone). On 2a03c658 the three fail 26 checks: floored with dust, refused InvalidTerms, the preview answering InvalidTerms and the execution refused. Docs: native-engine.md and native-settlement.md define settling in one fill with the absorbed total; the ADR row. Citations re-anchored from a25a3ce4 (104 anchors, digits only; 1678/1678). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 399601b9af0a28a5eabb1f458affde48c0cd1824) INT26 pick note (supervisor ruling: item 5 accepted, including the host-sized close the commit discloses). The kernel and test hunks applied without conflict. The ADR row sits after this lane's receipt-order row (0a046423) and before H-DOCGATES' inventory rows; the other three pages merged on citation digits only. --- docs/adr/0001-kernel-adapter-boundary.md | 19 +-- docs/design/native-feature-parity.md | 68 ++++----- docs/native-settlement.md | 3 +- docs/pages/native-engine.md | 26 ++-- docs/pages/pine-to-native.md | 10 +- src/native_execution_consumer.cpp | 35 +++-- tests/CMakeLists.txt | 5 +- tests/test_native_group_keep_handle.cpp | 182 +++++++++++++++++++++-- 8 files changed, 266 insertions(+), 82 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index f7bbc2c2..abdb3b23 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -77,7 +77,7 @@ summary of it. (`native_host.hpp:881`); also `on_native_input` (`native_host.hpp:863`), `on_native_tick` (`native_host.hpp:866`), `on_native_timeframe_bar` (`native_host.hpp:873`), `on_native_bar_open` — at the modeled opening, before its matching pass - (`native_host.hpp:877`, `on_native_bar_open` `native_execution_consumer.cpp:7066-7068`) — + (`native_host.hpp:877`, `on_native_bar_open` `native_execution_consumer.cpp:7062-7064`) — `on_native_recalculate` for every calculation of the run (`native_host.hpp:901`), `on_native_sub_bar` (`native_host.hpp:916`) and post-fill `on_native_applied` (`native_host.hpp:926`). `on_bar` is `final` (`native_host.hpp:845`). @@ -111,7 +111,7 @@ summary of it. bare host needs no override at all, while `HostSized` hands the units to the host — and an unresolved `HostSized` whose `resolve_execution_terms` (`native_host.hpp:935`) returns no units is a `TermsUnresolved` rejection - (`TermsUnresolved` `native_execution_consumer.cpp:4963`). + (`TermsUnresolved` `native_execution_consumer.cpp:4957`). - **Observing executions.** Implement `on_native_applied` (`native_host.hpp:926`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` (`native_host.hpp:1317`). Do *not* implement @@ -122,7 +122,7 @@ summary of it. | What | Surface | |---|---| | Run state | `native_state()` → `NativeStateView` — kind, phase, `NativeCompletion` BatchComplete / StreamEnded, failure, high water (`NativeCompletion` `native_host.hpp:50`, `NativeStateView` `:282`, `NativeRunPhase` `:41`) | - | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:4936`) | + | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:4930`) | | Cohorts | `cohort_open` (`native_host.hpp:1270`) / `cohort_add` (`:1273`) / `cohort_remove` (`:1276`) — the handle a `BindCohort` owner names | | Mid-callback execution | `current_execution_point` (`native_host.hpp:1075`), `inspect_current_execution` (`native_host.hpp:1087`) / `execute_current` (`native_host.hpp:1092`) | | Margin seams | four, not one: the run spec's own `margin` model (`NativeMarginModel` `native_run_spec.hpp:240`) with its three hooks — `resolve_margin_requirement` (`native_host.hpp:964`), `margin_check_allowed` (`native_host.hpp:976`), `resolve_margin_call_units` (`native_host.hpp:983`) — and `validate_execution_precommit` (`native_host.hpp:948`), whose `AdmitWithHostMargin` verdict hands the opening check to the host | @@ -534,6 +534,7 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | the settlement's quantity refusals and their outcome -- `execution::Status::UnrepresentableQuantity` from `next_close_split`, `order_action::plan` and an opening beside a surviving book (`src/engine_execution.cpp`), which `NativeExecutionConsumer::consume_matched_request` answers as `MatchRejectReason::UnrepresentableQuantity` (`include/pineforge/native_order.hpp`; C `PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY`), as it answers the request core's `CoreFailure::NonrepresentableQuantity` from `check_execution` / `prepare_execution` (a fill the request's own units cannot absorb); the quantity grid's own quantities (`CommandContext::units_are_scope_boundary` for a `Reduce` of a FIFO boundary of its scope, measured at submit, what it settles held at its candidate to the grid, a boundary of the scope as it stands or at least its held total, and a `ScopeFraction` whose product is its gross scope's held total, unfloored -- each for a request that settles in one fill); and the opt-in `NativeRunSpec::quantity_tolerance` walk (`tolerant_close_split`) | any host, by the quantities it requests and, for the tolerance, in its spec (C: the tail of `pf_native_run_spec_ext_v1`); the Pine adapter requests its quantities through its own `1e-10` rules, emits no `ScopeFraction` and declares no tolerance (next section) | **a typed refusal of the request, the book's own quantities on the grid, and an opt-in tolerance** (R5 lane K-ULP4, the owner-level ruling of audit X1). Rule 2 (a), mechanism: binary64 bookkeeping is venue-neutral -- a host of any venue meets the same absorbed quantities, gets the same terminal typed outcome for the one request, and keeps its run; the grid admits the quantities a host chooses and the book's own sizes the same way for every venue; the tolerance is data, a spec value with no platform branch. Rule 2 (b), knob: the refusal is none -- it replaces a whole-run `SettlementFailure` (code 6, discriminator 5) no host could want, and a request that settles exactly settles as before, bit for bit; the tolerance is a knob, but not for a decision the run has already made another way: absent, the walk is exact and the spec digest is the one it had before the field (pinned from 91d65ad6), so no existing run faces a hash-visible choice. What moved: runs that stopped on such a request now complete with its refusal (the two point-budget rows of `tests/test_native_resting_acceptance.cpp` and `tests/test_native_resting_replay_contract.cpp` pinned the old run failure and now pin the refusal); on a quantity grid a whole-scope fraction and a boundary `Reduce` that were floored or `OffGrid` now settle -- 48 of AUDIT4's 3,091 previously clean battery scenarios, each one where one of the two rules fired, and no other. Pinned by `tests/test_native_unrepresentable_refusal.cpp` and `tests/test_native_quantity_tolerance.cpp` (kernel-only), `check_unrepresentable_quantity` of `tests/test_native_c_api.c`, and the genuine-* cases of `tests/test_native_partial_close_split.cpp` and `tests/test_native_exact_sum_close.cpp`. | | an OCA-Reduce sibling's deduction binary64 cannot take -- at most half an ulp of what it is taken from, so that fl(units - d) is the units again -- absorbed where the request core deducts a member's fill (`absorbed_deduction`, `pending_sum` and `absorbed_pending_delta` in `src/native_order.cpp`, read by `WorkingRequestCore::prepare_group_effect` / `apply_group_effect`, by `prepare_terms` / `apply_terms` through `effective_host_units`, and by `prepare_owner_applied` / `apply_owner_applied`), and recorded as a `ReservationReducedEvent` with `actual_deduction` 0, a `DeferredGroupAdjustmentEvent` with `deferred_delta` 0 that joins no chain, or a `TermsResolvedEvent` / `QuantityBoundEvent` with `effective_deduction` 0 beside a positive `pending_total` (`include/pineforge/native_order.hpp`; C: `closed_units` 0 on a `PF_NATIVE_EVENT_RESERVATION_REDUCED` or `PF_NATIVE_EVENT_DEFERRED_GROUP` row) | any host, by the OCA-Reduce groups it submits (C: `PF_NATIVE_GROUP_REDUCE`), and the Pine adapter by the `strategy.oca.reduce` groups it maps (`PineExecutionAdapter::group_for`, `src/source/pine_adapter.cpp`) | **absorbed as a no-op** (R5 lane K-ULP5, the owner-level ruling on K-ULP4's finding 2). Rule 2 (a), mechanism: binary64 bookkeeping is venue-neutral -- a deduction too small to move the sibling's remaining units (or its pending total) leaves them where binary64 subtraction puts them, unchanged, for a host of any venue, and the member's fill stands; a pending total below half an ulp of a later fill rounds into it, as the addition does. Rule 2 (b), knob: none -- it replaces a whole-run `SettlementFailure` (code 6, discriminator 7, after the fill was booked) that no host could want, a deduction binary64 can take is taken as before, bit for bit, and only a pending total that overflows binary64 still fails the run with that discriminator. What moved: runs that stopped on such a deduction now complete -- K-ULP4's p5b probe on the C++ and the C host, and the 914 of the 3,000 runs of the seeded OCA-Reduce battery that stopped on f1af50dc -- and the rows that pinned the old contract now pin the absorption (the G5 rows of `tests/test_native_resting_acceptance.cpp` and `tests/test_native_resting_replay_contract.cpp`, `checked_reservation_arithmetic` of `tests/test_native_order_resting_core.cpp`, A-T4d of `tests/test_native_execution_terms.cpp`, `numeric_absorption_and_chain_authentication` of `tests/test_native_order_terms_core.cpp`); the overflow rows still pin the failure. No completed run's deductions move -- each was one binary64 could take -- and `inspect_current_execution`, which threw `std::overflow_error` for a host-sized request whose pending total its units absorb, now answers it. Pinned by `tests/test_native_group_absorption.cpp` (kernel-only) and `check_reservation_absorbed` of `tests/test_native_c_api.c`. | | the order of one cause's group-effect receipts -- the queue order `WorkingRequestCore::group_recipients` walks, which a `ReplaceOptions::keep_handle` re-price parts from incarnation order: `receipt_lookup` bisects the receipts by cause and walks that cause's, and `prepare_group_effect` / `apply_group_effect` refuse only a cause older than the newest receipt's (`src/native_order.cpp`; `WorkingRequestCore::group_effect_receipt`, `include/pineforge/native_order.hpp`) | any C++ host that re-prices a group member with `keep_handle` and then fills a sibling (`NativeStrategyHost::replace(handle, request, options)`; no C spelling, the C replace takes no options); the Pine adapter never sets `keep_handle` | **generic: a drain applies a group's effect in queue order, whatever the handles' numbers** (R5 lane K-OCA-KEEP, K-ULP5's finding 1). Rule 2 (a), mechanism: which live siblings a member's fill reaches, and in what order, is the book's own queue for a host of any venue, and the receipts are that drain's idempotence record, in the order it applied them; a request re-priced with its handle kept is reached where a plain replace's successor is, so the two runs record one timeline, as V19-D's contract already said. Rule 2 (b), knob: none -- it replaces a whole-run `Contract` failure (code 2, discriminator 1, `CoreFailure::InvalidCause`, after the member's fill was booked) that no host could want: the receipt check held one cause's receipts to incarnation order, and the re-priced member, drained behind a younger sibling, was refused; a cause older than the newest receipt's is still refused. What moved: runs that stopped on such a drain now complete -- K-ULP5's `probe_keep_handle_oca`, staged and direct, under `Cancel` and `Reduce`, and all 180 runs of the seeded battery under a keep_handle option set (of its 360), which stopped on 2a03c658 -- each recording the plain replace's run event for event. No completed run moves and no hash value moved: a completed run's receipts were already in incarnation order within each cause, and the lookup answers such a store as before (the K-ULP4 and K-ULP5 batteries' per-run hashes unchanged; corpus parity unchanged by construction, the Pine adapter never setting `keep_handle`). Pinned by `tests/test_native_group_keep_handle.cpp` (kernel-only). | +| a request on a quantity grid whose pending group deduction its units absorb -- it settles them all in one fill (`settles_in_one_fill`, `src/native_execution_consumer.cpp`, which asks `WorkingRequestCore::effective_host_units` whether the pending total takes any), so a whole-scope `ScopeFraction` resolves to its scope's held total unfloored and a host-sized close answered with that total is its own quantity, at the match and at the current execution alike | any host, by the OCA-Reduce groups and the quantity grid it declares (C: a fraction, and a close sized by `on_close_units`); the Pine adapter does not reach it: it emits no `ScopeFraction`, and it answers a close of its scope's whole total with `ExecutionGridPolicy::ExplicitUnits`, whose grid check never asks whether the request settles in one fill | **one definition of settling in one fill** (R5 lane K-OCA-KEEP, K-ULP5's finding 2). Rule 2 (a), mechanism: K-ULP4's grid rule admits the book's own quantity for a request that settles it in one fill, and K-ULP5's absorption makes a sub-ulp pending total take nothing, for a host of any venue; the two now agree. Rule 2 (b), knob: none. What moved: a run in which such a request closes its scope's total now closes it whole -- a whole-scope fraction was floored onto the grid and closed short, leaving a dust lot (before K-ULP5 such a run stopped wherever the floored units absorbed the total too, code 6, discriminator 7), and a host-sized close answered with its scope's total was refused `InvalidTerms` and left the lot open, before K-ULP5 as after: the `grid-absorbed-*` rows of `tests/test_native_group_keep_handle.cpp`, whose fraction closed 1.1000000000000001 of a 1.1000000000000227 lot on 2a03c658. K-ULP4's own battery answers every scenario's hash as before, K-ULP5's absorption and C-host batteries are unchanged (no grid), `grid-fraction-oca-pending` of `tests/test_native_unrepresentable_refusal.cpp` (a pending total that takes 0.3) is still floored, and no pinned hash moved. Pinned by `tests/test_native_group_keep_handle.cpp` (kernel-only). | | the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:1825` and `:1934`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:295`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | | the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1478`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | | the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1616`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | @@ -686,7 +687,7 @@ number names its symbol on this tree. | surface | ruling | reason | |---|---|---| | `cancel_all` (`native_host.hpp:1255`) / `cancel_where` answer a count, not a `CancelResult` per request (E12 f7) | **retained** | A bulk cancel is many commands in one call, and each withdrawn request records its own `CancelledEvent` with its own `CancelReason` — dependants of a cancelled owner included — in the history a host already polls (`strategy_native_events_v1`). That is the per-request answer; the count is the call's summary. A result vector would restate the history in a second, allocating shape that C could not take without a caller-sized array. The C spellings answer the same count (`strategy_native_cancel_all_v1` `native_c_api.h:2745`). | -| `cohort_add` (`native_host.hpp:1273`) / `cohort_remove` answer `void` (E12 f7) | **retained; the typed receipt is a named follow-up** | The kernel judges every enrolment into a `CohortReceipt` (Applied, InvalidHandle, UnknownOrigin, TerminalOrigin) and folds it into the continuation (`hash_cohort_receipt` `native_execution_consumer.cpp:881`, since v19 once, at the enrolment, into the running digest the continuation carries), so a replay that diverges there diverges in the hash. Nothing that matches or settles reads it: a roster is a relation the close reads at its match, and a refused enrolment is a member the close does not take. A host that needs the receipt AT the call needs `NativeStrategyHost::cohort_add_result` first, a kernel-header change outside this lane; its C spelling would then be an `_ext_v1` with a receipt word. Until then `strategy_native_cohort_add_v1`'s `PF_NATIVE_OK` means the enrolment was issued. | +| `cohort_add` (`native_host.hpp:1273`) / `cohort_remove` answer `void` (E12 f7) | **retained; the typed receipt is a named follow-up** | The kernel judges every enrolment into a `CohortReceipt` (Applied, InvalidHandle, UnknownOrigin, TerminalOrigin) and folds it into the continuation (`hash_cohort_receipt` `native_execution_consumer.cpp:890`, since v19 once, at the enrolment, into the running digest the continuation carries), so a replay that diverges there diverges in the hash. Nothing that matches or settles reads it: a roster is a relation the close reads at its match, and a refused enrolment is a member the close does not take. A host that needs the receipt AT the call needs `NativeStrategyHost::cohort_add_result` first, a kernel-header change outside this lane; its C spelling would then be an `_ext_v1` with a receipt word. Until then `strategy_native_cohort_add_v1`'s `PF_NATIVE_OK` means the enrolment was issued. | | `native_sized_units` (`native_host.hpp:1302`) / `native_liquidation_price` answer `std::optional` (E12 f7) | **retained** | Observation queries, not commands: each empty is documented cause by cause in its own comment (unconfigured, non-positive money or denominator, a below-one-step quotient; no margin model, no maintenance fraction for the side, a flat book, no finite solution), and every cause is a fact the host can read itself. A typed reason would name what the caller already knows. C answers `PF_NATIVE_ABSENT` and NaN (`strategy_native_liquidation_price_v1` `native_c_api.h:3020`). | | `pf_native_working_v1` without the leg's anchor or owner relation (E13 f4) | **carried** | The C++ host reads them off `NativeWorkingRequest::definition`, so the C readout's relation tail carries them: `anchor` (`native_c_api.h:1683`) through `arm_scope`, the request as the kernel holds it now (an armed leg reads ABSOLUTE with its installed level). | | `hash_source_extension` (`engine.hpp:411`) | **C++-only** | The deprecated spelling of `hash_host_extension`, kept so an existing C++ subclass compiles and folds unchanged. A C host has only ever had the current spelling, `on_hash_extension`. | @@ -696,7 +697,7 @@ number names its symbol on this tree. | `strategy_configure_native_v1` (`c_abi.cpp:896`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:3979`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | | `strategy_configure_native_fx_curve_v1` (`pineforge.h:584`) | **typed additive route** | `strategy_configure_native_fx_curve_ext_v1` writes the C twin of `NativeFxCurveValidation` and its offending index. The old `-1` route remains unchanged, while a C caller can now distinguish a decreasing timestamp, non-positive rate, allocation failure or wrong phase. | | `on_native_timeframe_bar` (`native_host.hpp:873`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:797`) | **query routed** | The callback signature stays frozen. `strategy_native_timeframe_bar_interval_v1` is valid only during that callback and copies the kernel calendar interval without deriving it from the delivered bar or delivery timestamp. | -| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7645-7657`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1077-1082`) sees the same values, so no hash contract moves. | +| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7626-7638`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1085-1090`) sees the same values, so no hash contract moves. | | `pf_native_lot_excursion_v1` (`native_c_api.h:1871`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | ## TradingView-calibrated kernel mechanisms (rule 2's own account) @@ -754,7 +755,7 @@ The table records, at the tree the doc wave read, what each block described and | `ta::ATR` `include/pineforge/ta.hpp:321` (the `TradingView` comment `sha256:4f6f6939aec2773684b114a13dd002eb58990d6d96c3056ad87361753faf7a0a` :333-340) | average true range over Wilder's `RMA` | Wilder's ATR is the textbook definition; the comment records which convention and the tape that pinned the recompute behaviour | a seeding/averaging field on an indicator whose definition is the convention | `tests/test_ta_rma_warmup.cpp`, `tests/test_recompute.cpp` | | `ta::StdDev` `include/pineforge/ta.hpp:430` (the `TradingView` comment `sha256:7cc1bbcd32dfe673285d51a8ca7080890ac551858099b523480d998f46408666` :420-429) | the population (biased) standard deviation | biased vs sample is a textbook choice, and the kernel states which | a per-instance switch is the *right* answer where two conventions genuinely compete, which is why `ta::EmaSeeding` exists and this one does not need it | `tests/test_ta_indicators_extras.cpp` | | `float_band_eq` `include/pineforge/ta_compare_band.hpp:38` | an absolute 1e-10 float comparison for indicator equality | an epsilon band is needed by any float comparison; the width is the calibration | a band-width field on a comparison the indicators share | `tests/test_dmi_parity.cpp`, `tests/test_ta_osc_edge.cpp` | -| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8606-8609` (lookahead) and `clear_if_gapped` `:8600-8603` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | +| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8571-8574` (lookahead) and `clear_if_gapped` `:8565-8568` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | | `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2230`, `::gaps` `include/pineforge/native_c_api.h:2234` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:795`, `pf_native_gaps_e` `:806`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | | the authoritative-feed partition `src/engine_aux_security.cpp` | a feed is the venue's own bars of one timeframe; its stamps are the period partition | already ruled: design §2.ii row s, and the residual table's own row | the policy knob is installing the feed or not | `tests/test_native_htf_subscriptions.cpp`, `tests/test_native_security_feed.cpp` | | `session_template_knows_early_close` `src/engine_security.cpp` | the instrument-class classification | already ruled: design §2.ii row t; `SymInfo::type`'s vocabulary is fixed by the frozen C ABI | — | `tests/test_native_wm_buckets.cpp` | @@ -769,9 +770,9 @@ The table records, at the tree the doc wave read, what each block described and | the formatting library -- `include/pineforge/str_utils.hpp`, `src/str_utils.cpp` (`str_tostring`, `str_format_values`, `str_format`) | the number text of Pine's `str.tostring` / `str.format` built-ins: a number's shortest round-trip decimal spelling, scaled and rounded half-up on its digits, the default, `percent`, `volume` (K / M / B / T) and pattern modes, MessageFormat placeholders and quoting, `NaN` and `Infinity` -- the published rendering TradingView's exported tapes pin (R5 lane C6), ported from the codegen formatter (lane C6b) by R5 lane B-ENGINE; the `mintick` mode, which generated code delegates to it, is kept as it was | a formatting library is a function of its inputs, the same for every venue, as the indicator library is: it spells a number the way the source language's own `str.*` library defines, and no kernel order, fill, settlement, margin or hash code calls it -- of the thirty-five kernel translation units only `src/str_utils.cpp` names these functions | a rendering switch on functions whose definition the calling script chose by calling them | `tests/test_str_number_format.cpp` (lane C6b's 42-case edge battery, every numeric string of lane C6's TradingView exports, byte-identical output on macOS arm64 and Linux x86-64) and `tests/test_str_utils.cpp` | | the language calendar functions -- `include/pineforge/session_time.hpp`, `src/session_time.cpp` (`timeframe_time`, `local_hour` and its siblings, `session_in_market`, `session_trading_day_open_ms`, ...) | the definitions of Pine's `time()` / `time_close()` / bare-time / `session.is*` built-ins for generated code: session and timezone parsing, the invalid- and 24-hour-session spellings, `time("60")` day keying, `session.is*` on daily charts, each comment naming the tape that pinned it | the inputs are data -- a session string, a timezone, a timeframe -- so another venue is driven by its own session template; nothing branches on a platform, and a native host's calendar is the separate generic `native_calendar` | a rules switch on functions whose definition the generated script chose by calling them | `tests/test_session_time.cpp`, `tests/test_pine_time_day_stamp_grid.cpp` and the corpus parity gate | | `present_session_day` `src/native_execution_consumer.cpp:7268`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (`tests/fixtures/session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | `tests/test_native_session_day_facts.cpp` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), the C scenario of `tests/test_native_c_api.c`, and `tests/test_session_day_facts_adapter.cpp` (the adapter selects them flag for flag) | -| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7877-7893` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | -| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7854-7875` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | -| `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6605` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | +| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7856-7872` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | +| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7833-7854` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | +| `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6601` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | | the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:334`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1541`, `:1556`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:1854`). A bare host never calls the builder, so its calendar stays nominal | `tests/test_o_close_pct_day_anchor.cpp` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | | the bar-time accessors -- `_decompose_bar_time` and `_bar_hour` through `_bar_weekofyear` `include/pineforge/engine.hpp:1066-1093` | the presented bar's stamp broken into calendar fields in UTC, memoized per stamp | a function of the stamp alone, and UTC is the engine's storage timezone | none: they read no spec field and fold into no digest. They are protected `BacktestEngine` members, part of the codegen contract. Their comment claimed that generated code reads them for Pine's bare `hour` / `minute` / `dayofweek`. It does not: the codegen transpiler emits `pine_hour(current_bar_.timestamp, syminfo_.timezone)` and its siblings on the exchange timezone (`pineforge_codegen/codegen/tables.py` at the corpus's pinned commit `f3285d79`), which are the language calendar functions above. No generated strategy in `corpus/` names a `_bar_*` accessor. The comment now says so (R5 lane H-DOCGATES) | `tests/test_chart_timezone.cpp` (`_bar_hour()` is the UTC hour) and `tests/test_intraday_rollover_chart_tz_l4a.cpp` | | the conventional offset spellings of `timezone_accepted` `include/pineforge/native_calendar.hpp:201` (its comment's "TV sign" line) | `UTC+05:30` and `GMT-4` mean what they say: five and a half hours ahead of UTC, four behind (ISO 8601's sign), where a POSIX `TZ` string of the same shape means the opposite; a POSIX rule string keeps POSIX's own sign | a zone spelling is data, and the sign is the one a venue printing `UTC+8` means; nothing branches on a platform | a sign-convention switch over a spelling whose meaning is fixed | `tests/test_native_calendar.cpp` (`resolve_civil("UTC+05:30", …)` and `resolve_civil("GMT-4", …)` against their fixed instants) | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 854f8e14..6fe708e4 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,7 +47,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2737-2748; `entry()` pine_adapter.cpp:5289, `order()` pine_adapter.cpp:10480) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5685-5732; `submit` native_host.hpp:1201 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2737-2748; `entry()` pine_adapter.cpp:5289, `order()` pine_adapter.cpp:10480) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | 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:8343 → `Flatten{}` pine_adapter.cpp:8372-8373; `Reduce{ExplicitUnits}` pine_adapter.cpp:7490-7499; `ReverseTo` pine_adapter.cpp:6324-6325) | **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:2707-3914 → `host.replace` pine_adapter.cpp:2723-2803) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4722-4768, `cancel_all` pine_adapter.cpp:18032-18047; `live_by_source_key_` pine_adapter.hpp:1985; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 is the enumeration; the id → handle book is `native_toolkit::OrderBook`, host-side and Pine-free (`tests/test_native_order_ergonomics.cpp`, `tests/test_native_anchored_legs.cpp`). | K+A | L7 | F:A3 F:A2 O:A5 O:A14 S:O3 | **Disagree:** F partial / O no (A5) + partial (A14) / S yes (folded into O3). R5-1: cancel-all / by-label is generic → kernel. TV state inheritance across a same-id re-issue stays A. The adapter's `strategy.cancel` / `strategy.cancel_all` keep their per-handle loops, measured by R5 lane F7 (audit M27): TradingView cancels in the SOURCE book, which holds commands of the same script call that have not reached the kernel yet (and a live `strategy.close` is labelled `__close__` there), so at the same point `cancel_where(id, NativeRequestField::Label)` and `cancel_all()` withdraw 0 — the close flattens at the next open, the reversal lands — where the source cancels withdraw them (`tests/test_adapter_brackets_relower.cpp`, M27 rows); each withdrawn source handle is also retired one by one. | @@ -57,7 +57,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL8 | Immediate execution at the current point (`immediately=true`) | **yes** — `inspect_current_execution` / `execute_current` native_host.hpp:1092-1093 | 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:13004; lowering `submit_one` pine_adapter.cpp:9040-9052, `BindCohort` pine_adapter.cpp:9541-9547, `submit_leg` pine_adapter.cpp:9565-9589; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8645-8648; `materialize_relative_exits` pine_adapter.cpp:17322-17363, pine_adapter.hpp:1820-1832) | **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:5611-5645. `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:1000 is where a source language's own level arithmetic lives (`tests/test_native_anchored_legs.cpp`, `tests/test_native_arm_options.cpp`). | K | L7 | F:A11 O:A11 S:O6 | **Disagree:** F partial (toolkit is enough) / O no (kernel `TriggerAnchor`). R5-2: L7 delivers relative anchors; kernel anchor primary, toolkit materialization is the zero-kernel fallback. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8645-8648; `materialize_relative_exits` pine_adapter.cpp:17322-17363, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19083-19112) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10880-10904), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15631, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15865) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:651-653 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). What `tests/test_adapter_margin_revival_cancel.cpp` replays on the NYSE:F 15m chart of seven lab tv tapes: badapter-v19dp1-control (no cancel: the margin call closes 928 through X) and v19fix-gapped-stop-control (X's stop at 10.15, gapped by the 05-01 open and parked: the same close), and badapter-v19dp1-cancel, v19fix-cancel-before-pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair (a cancel on 05-01 09:30 while X is dormant, on 04-30 15:45 before the reversal pair and on 04-30 10:00 while X is live, and on 05-01 09:30 and after the pair while X is parked; one tape file: margin calls 12 @10.39 and 92 @10.57, 836 open). Untaped, the same row holds the tapes' trades for `strategy.cancel_all` on 05-01 and before the pair and for `strategy.cancel` after the pair, and on the same bars with no quantity grid — where the pair holds X in the cancel's own callback — every spelling books what the cancel on the dormant exit books | @@ -66,9 +66,9 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:5697-5719; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1080 | K | — | F:B1 O:B1 S:T1 | — | +| TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:5692-5714; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1080 | 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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11779-11791) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4451-4456) **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:11692-11829) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11779-11791) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11692-11829) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4021-4067) | **partial** — the read exists `trail_state` native_host.hpp:1080; `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:287-289) is A. | | TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9591-9596, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11692-11829, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8615-8841, `trail_one_shot` pine_adapter.cpp:9680-9809 | n/a | A | — | F:B3 S:T2 | R5-1 | @@ -78,11 +78,11 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5902-5906, pine_adapter.cpp:5902) | **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:10915-12107) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4922-4956 | 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:2595-2650) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4963-4965 **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: rounded equity `source_money_round` pine_adapter.cpp:2547, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | +| SZ3 | Percent-of-equity sizing (`default_sizing_units` pine_adapter.cpp:2595-2650) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4957-4959 **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: rounded equity `source_money_round` pine_adapter.cpp:2547, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2556-2558) | **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:2608, `typed_entry_units` pine_adapter.cpp:2626; `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:2587; `percent_commission_live_equity` pine_adapter.cpp:4328-4368) | **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. | | SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2513-2528, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6645-6671) | **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:591, 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:4979-4982; the book's own quantities are on the grid -- a whole-scope `ScopeFraction` and a `Reduce` of a FIFO boundary of its scope (R5 lane K-ULP4) | K | — | F:C4 O:C5 S:S1 | **Disagree:** F, O partial / S yes. R5-3: the quantity grid is native already. L3's `Sized` intent reuses the snap policy. | +| SZ7 | Quantity step | **yes** — `quantity_grid` admission native_run_spec.hpp:591, 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:4973-4976; 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:12478-12506, `compute_exit_reservation` pine_adapter.cpp:4411-4532) | **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:4463-4786, `reserved_exit_qty` pine_adapter.cpp:8967-9277) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4443-4458) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | @@ -94,12 +94,12 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2361) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2947-2955 | 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:594 tests the resulting book native_execution_consumer.cpp:2961-2964 | 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:13226-13229) is TV. | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2361) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13226-13229) is TV. | | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6180-6226, cap left unset `max_open_lots` pine_adapter.cpp:2360-2362) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | | MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13280-13345 and leaves the kernel gate unset pine_adapter.cpp:13279-13281) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13935-13936, `margin_pct` pine_adapter.cpp:13343) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14035-14213, `submit_margin_call_units` pine_adapter.cpp:14174-14220, `schedule_margin_call_path` pine_adapter.cpp:18185-18262) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2943 **Closed:** `maintenance_long` / `maintenance_short` with `NativeLiquidationCheck::{PathAdverseExtreme, PathAdverseExtremeMark, CalculationOnly}`; the kernel solves the level, rests its own `Reduce`/`Flatten` bound to the live book, re-prices it with `CancelReason::Superseded` and books a `MarginCallEvent` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | F:D2 O:D3 S:M3 | TV parts → MG15 | +| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:14035-14213, `submit_margin_call_units` pine_adapter.cpp:14174-14220, `schedule_margin_call_path` pine_adapter.cpp:18185-18262) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13966-14002) | **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:983 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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1308 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. | | MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16283-16299, `apply_fx_opening_margin_slice` pine_adapter.cpp:18263-18291) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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) | @@ -116,13 +116,13 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:881, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2318-2319) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7524-7528 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16118) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7066-7070; `execute_current` legal native_host.hpp:1092 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2318-2319) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7505-7509 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16118) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7062-7066; `execute_current` legal native_host.hpp:1092 | 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:7009 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7060 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1061 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:863, `NativeInputContext` native_host.hpp:761-766 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16261, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6920, native_execution_consumer.cpp:6920-6950; commands are legal there `commands_allowed` native_execution_consumer.cpp:1398-1406; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5611-5645, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | -| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:866 fires per accepted realtime print native_execution_consumer.cpp:7148-7152; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | -| 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:7518-7635 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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 | — | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16261, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6916, native_execution_consumer.cpp:6895-6925; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:866 fires per accepted realtime print native_execution_consumer.cpp:7144-7148; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | +| 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:7499-7616 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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:2448-2504) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:234-320) 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:5757), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1659-1666 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | @@ -136,7 +136,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:637) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:916 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | | HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:21-220, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:241-413; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:514) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:255-336) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | -| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:863; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7877-7894, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7884, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | +| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:863; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7856-7873, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7863, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | ### 1.7 Fill price, slippage, commission, FX @@ -145,10 +145,10 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:5685-5732, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6268-6276; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | | FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11946-11955) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:472-479) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11962-11971, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4060; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6350-6351; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11962-11971, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:1190 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:8706). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | +| FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1190 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:8671). **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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9032-9037, `exit_limit_trigger` pine_adapter.cpp:9566-9587), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11671) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -161,8 +161,8 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:219; `fill_metrics_section` engine_report.cpp:119-139 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | | RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1683) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1408) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | -| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9280, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | -| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1358, native_execution_consumer.cpp:10682; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | +| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9236, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | +| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1358, native_execution_consumer.cpp:10622; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | | RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:538; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1931 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:411 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:405 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:411 is its deprecated spelling, which the generic default forwards to (`tests/test_native_host_hash_extension.cpp`). | K | L2 (the virtual rides the shared bump) | F:I3 O:J1 S:X3 | — | @@ -176,7 +176,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OT4 | Cooperative abort | **yes** — `request_abort` engine.hpp:2158, `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:896, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:940, probe `strategy_execution_contract` c_abi.cpp:873, stream `strategy_stream_begin` c_abi.cpp:624 **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:548 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:907 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 58 runtime exports (and eight generated per-strategy exports) (`tests/test_native_c_api.c`, `tests/test_native_c_api_frozen_header.cpp`). | K | L13 | F:J6 O:J5 | R5-7: post-v1. S covers it in its §2.3 without a row. | -| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1454 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7268), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1764 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:857; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | +| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1454 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7266), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1764 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:857; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | | OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:288-290) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | --- @@ -204,7 +204,7 @@ 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:82-91) and of the rich `BacktestEngine::run` overload (engine.hpp:1780-1785, engine_consumer.cpp:81-93). The kernel only forwards it as `NativeBeginArgs::overrides_opaque` native_host.hpp:746; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:292-295 | 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) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:29 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:356-359 (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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1836-1839; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1869, `:8767`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2308-2309. 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:260-261. **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:741) | 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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8732`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2308-2309. 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:260-261. **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:741) | 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:454-460) | 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:6421 | 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:1774 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) | @@ -268,7 +268,7 @@ PF_API int pf_native_events_v1(pf_strategy_t, uint64_t after_ordinal, pf_native_ |---|---|---| | Implementation | One new kernel TU (F `src/native_c_host.cpp`, O `c_native_host.cpp`, S `native_c_api.cpp`): `class CCallbackHost final : public NativeStrategyHost` forwarding each virtual to the table. Streaming reuses the existing `strategy_stream_*` symbols unchanged (they take any `BacktestEngine*`) | F O S | | Errors | A non-zero callback return maps to `NativeFailureCode::CallbackException`; C callbacks must not unwind. The C++ path already latches exceptions as `CallbackException` native_execution_consumer.cpp:7068 | F O | -| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1398-1405): inside a callback, or between realtime inputs | F | +| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1406-1413): inside a callback, or between realtime inputs | F | | Hardening | Tagged, size-prefixed PODs; `struct_size` + version; unknown-tag refusal; never cast the C request to a C++ variant; a C fixture compiles against the frozen previous header. S puts the API in a separate header `include/pineforge/native_c_api.h` | S | | SOP | Every symbol goes into `src/c_abi.cpp`, `pineforge.h`, `EXPECTED_RUNTIME` + both counts (check_c_abi_runtime.py:19-79), the README table and the Python harnesses, or all CI matrix jobs fail at the "C ABI runtime source check" step (`CLAUDE.md`) | F O S | | Not in v1 of the C API | `resolve_execution_terms` / `validate_execution_precommit` are the adapter's seam; a C host that needs sizing uses `Sized` (F). O offers an optional `resolve_terms` callback: deferred. *History: R5 lane F4 routed both (`on_execution_terms`, `on_precommit`); the terms hook's units answer outside a host-sized close is still C++-only (the 1.0 C boundary table)* | F O | @@ -281,8 +281,8 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the | | F (its §2.iv A) | O (its G10) | S (its P7) | Chosen | |---|---|---|---|---| | API shape | spec `subscriptions` + push callback `on_native_timeframe_bar` | `register_native_series` + pull `native_series_bar(id)` / `native_series_slot_is_new(id)` | spec `security_requests` + `configure_native_security_feed` + `native_security(id)` view | F's subscription in the spec + push callback, plus O's pull accessor | -| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:656-671, `contribute_input` native_execution_consumer.cpp:8013, `seal_script` native_execution_consumer.cpp:7978) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | -| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7098), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | +| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:656-671, `contribute_input` native_execution_consumer.cpp:7980, `seal_script` native_execution_consumer.cpp:7957) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | +| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7094), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | | Authoritative bars | `authoritative_bars` on the subscription | `Source::ExchangeFeed` through `set_native_security_feed` | `configure_native_security_feed(id, bars, tf)` | F's field, installed through the existing store | | Lookahead | none, by construction | `lookahead = false` default, available | `lookahead_on = false`, `gaps_on = true` | open → §4 U1 | @@ -290,7 +290,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the 2. **Wiring**, all inside the native consumer at Ready → begin and only when `subscriptions` is non-empty (zero work for the adapter): register one evaluator state per subscription through the existing `register_security_eval` (engine.hpp:1295-1297, *fresh*); install `authoritative_bars` into the existing store (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129); call `prepare_native_security_feeds` (engine.hpp:1639, defined `BacktestEngine::prepare_native_security_feeds` engine_aux_security.cpp:128, *fresh*) — the "made native-callable" step: reachable from the native consumer, still refused from host code in-run; pump `feed_security_eval_state` (engine.hpp:1312-1314, *fresh*) from the accepted-input path; deliver each completed bucket through `on_native_timeframe_bar`. 3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1304-1388 and its call order. The per-run evaluator init is today a *source-layer* member (`PineStrategyHost::init_security_eval_states_for_run` pine_scheduler.cpp:9-39, declared `init_security_eval_states_for_run` pine_strategy_host.hpp:694, *fresh*), so L6 adds a kernel-side twin instead of moving it. 4. **Delivery rule (F):** a subscription bar is delivered on the input bar that completes it — its last contributing input bar, or, for a `LazyComplete` bucket, the next period's first input bar — immediately before that input's script calculation, never earlier — and never before an earlier script interval's lazily sealed calculation has run: the pump is ordered against the script interval, `seal(k) → deliver(i+1) → calc(i+1)` (L6d). -5. **Interim, documented in L0:** `on_native_input` + `TimeframeAggregator` (HT6). Doc notes (O): `set_native_security_feed` is public but inert without evaluators and throws in-run (`guard_native_mutation` engine_aux_security.cpp:78, which latches `UnsupportedSource` native_execution_consumer.cpp:1447); `set_aux_security_feed` returns false in the kernel engine_consumer.cpp:158-160. +5. **Interim, documented in L0:** `on_native_input` + `TimeframeAggregator` (HT6). Doc notes (O): `set_native_security_feed` is public but inert without evaluators and throws in-run (`guard_native_mutation` engine_aux_security.cpp:78, which latches `UnsupportedSource` native_execution_consumer.cpp:1455); `set_aux_security_feed` returns false in the kernel engine_consumer.cpp:158-160. 6. **Entanglement to put in the lane brief** (*fresh* read of `set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:46-78): the feed machinery is TV-calibrated. "W" / "M" buckets are built from the installed native daily bars, and the native stamps define the period partition (a holiday session folds into the next trade date's bar). A bare host that supplies `authoritative_bars` inherits those rules. Document them as the contract or gate them behind a partition policy — decide in the lane, not silently (§4 E8). 7. **HT3 / HT4:** lower-TF arrays come from the L5 sub-bar hook in v1. An auxiliary feed is a subscription whose bars come from the host; the Pine chart-slice mapping is not exposed. Whether v1 accepts a feed *finer* than the input is left to the lane brief (S wants it, F calls it a TV harness construct). **Status (audit lane N7): HT4 done for a bare host; the adapter's auxiliary path retained, measured.** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed{tf, bars}`, strictly finer than `input_tf`, opt-in and hashed only when present) plus `NativeTimeframeSubscription::source = NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars through the existing evaluator step (`register_security_eval` against the feed's timeframe, `feed_security_eval_state` per feed bar — called, not edited); routing is by time alone (a feed bar rides on the first accepted input whose period it opened before), with `declare_auxiliary_feed` at begin, `append_auxiliary_bars` on a realtime stream and a C spelling (`PF_NATIVE_SPEC_EXT_AUXILIARY_FEED` tail, `strategy_native_append_auxiliary_bars_v1`). No epoch moved: `native_run_spec_v3` and `engine_script_run_v18` were open then (no release tag contained them, no frozen provider fixture of either existed), exactly as L6c's `gaps` and begin-time hook were added. `BacktestEngine::set_aux_security_feed` stays the source host's door and still answers a silent false on a bare host (the pre-begin setter pins, `tests/test_native_example_batch.cpp` among them, hold it to that). **Retained, why:** the adapter does not re-lower its auxiliary sites, on three measurements — 0 of 312 corpus probes install an auxiliary feed, so corpus byte-identity cannot witness the move; TradingView's chart slice leaves pre-range feed coverage inert where the kernel folds it by time (two hourly buckets against three over one feed, `tests/test_native_auxiliary_feed_twin.cpp` row B); and the adapter evaluates a chart bar's slice at its calculation, after that bar's matching pass, where the kernel delivers before it (position 1 against 0 at the same bucket, row C), which no predicate over an opaque generated `evaluate_security` can prove neutral. The congruent shape is the same series on both (row A) and can move once a population exercises it. Full contract: `docs/pages/native-engine.md`, "The auxiliary finer feed". @@ -341,7 +341,7 @@ the campaign's own anchor audit found that the first thing to rot: | the request-epoch sentence | `native_order_v4` | `native_order_v7` native_order.hpp:25 | | the two host-epoch sentences | host epoch v16 | `engine_script_run_v19` native_host.hpp:20 | | "What still requires Pine compatibility to build" | `BacktestEngine` still holds `CapAttachment` / `OrderPriority` / `IntradayCap` | `engine.hpp` has zero references to them; the cap type is `CapAttachment` intraday_cap.hpp:18 and the constructor is `PineStrategyHost`'s pine_strategy_host.hpp:244 | -| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9150-9190 shows no such refusal (still unpinned by a test; the page says so) | +| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9106-9146 shows no such refusal (still unpinned by a test; the page says so) | | added | — | the self-aggregation recipe beside the subscriptions it is an alternative to; the open-bar lookahead warning with `current_partial_bar` native_host.hpp:1061; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -411,7 +411,7 @@ closure markers and `scripts/check_design_inventory.py`). | L2 | `NativeReportPolicy { HostRecorded, KernelRecorded }` (R1 appended a third value, `KernelRecordedAtHostMarks` native_run_spec.hpp:64), `report_open_position_at_end`, `hash_host_extension`, `native_metrics()` → A.1 | at L2 the adapter stayed `HostRecorded`; since R1 it declares `KernelRecordedAtHostMarks` pine_adapter.cpp:2357, 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:5616-5620) and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | +| L5 | `NativeCalculationTrigger`, `on_native_recalculate(…, reason, cause)`, `on_native_sub_bar`, `current_partial_bar()`, `NativeOpenBarView` → A.4 | the adapter keeps `BarClose` + its own cascade; `born_on_remaining_path` (native_execution_consumer.cpp:5611-5615) and `drain_applied_notifications` (native_execution_consumer.cpp:6272-6285) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | | L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1304-1388 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | | L7 | `native_working_requests()`, `cancel_all()`, `cancel_where(label)`, `TriggerAnchor`, `TrailTicks`, zero offset, `ReplaceOptions`; toolkit `submit_bracket`, `OrderBook` → A.6 | new kinds unused by the adapter; anchor defaults to `Absolute`; the adapter's `tick * 0.5` sentinel keeps working | bracket + trailing probe through the builder: exact where TR5 / FP7 are inactive (on-grid levels) (F); the zero-offset trail test asserts the run no longer fails (O); parent-limit + stop sibling, parent rejection, replacement, parent-cycle revival (S) | | L8 | `NativePriceGrid { None, QuantizeFills, QuantizeFillsAndTriggers }`, `NativeGridRounding` → A.7 | `None` for the adapter, permanently — **native-only by ruling, audit lane P6: §3.6.2** | a host with `QuantizeFills` books on-grid prices; a second test pins that `QuantizeFillsAndTriggers` still differs from TV's half-tick threshold (FP5) (O) | @@ -1325,11 +1325,11 @@ virtual void hash_host_extension(BrokerStateHashSink&) const {} // F (S: hash_ NativeMetricsView native_metrics() const; // S: net_profit, equity, closed_trades, open_lots; non-virtual ``` -- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` + `record_equity_point(script_open_ms)` (engine.hpp:1380, engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7192, native_execution_consumer.cpp:7192, native_execution_consumer.cpp:7192) and after the `AfterCalculation` close; appends the per-bar broker hash when recording is on; synthesizes the open-position row at run end / `stream_end` with the existing `build_close_trade_with_costs` (engine_orders.cpp:70). +- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` + `record_equity_point(script_open_ms)` (engine.hpp:1380, engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7188, native_execution_consumer.cpp:7188, native_execution_consumer.cpp:7188) 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:2357), 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:1011). Same semantics, but it costs a host-epoch bump, so the spec form was chosen; the virtual was never added, and `NativeReportPolicy` is what landed (§3.4 row 3). -- TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5060-5102), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. +- TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5055-5097), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. ### A.2 L3 — sizing, fractional reduce, sibling reservation (F P1, P7 + O G2 + S P2, P3) @@ -1352,7 +1352,7 @@ struct ScopeFraction { double fraction = 1.0; ScopeClaim claim = ScopeClaim::Gro using ReductionSize = std::variant; // F ``` -- Resolution: `units = cash / (price * point_value * fx)`, `cash` = the value or `fraction * marked_equity(price)`, `price` = the candidate's `default_resolved_price`. It plugs in where `HostSized` units resolve today (native_execution_consumer.cpp:684; terms resolution `resolve_execution_terms` native_execution_consumer.cpp:4816). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:877); validation reuses `RequestRejectReason::InvalidQuantityBasis` (native_order.hpp:827). +- Resolution: `units = cash / (price * point_value * fx)`, `cash` = the value or `fraction * marked_equity(price)`, `price` = the candidate's `default_resolved_price`. It plugs in where `HostSized` units resolve today (native_execution_consumer.cpp:693; terms resolution `resolve_execution_terms` native_execution_consumer.cpp:4816). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:877); validation reuses `RequestRejectReason::InvalidQuantityBasis` (native_order.hpp:827). - `NetOfSiblings`: a fraction-sized reduce claims the bound scope minus the units already claimed by live sibling reduces on it; keys are opaque, never source IDs (S). - Not adopted: S's spec-level `default_sizing` + virtual `resolve_native_sizing` (duplicates the per-request intent, costs an epoch); S's `PercentFreeMargin` (needs L4's free-margin definition) and `EveryCalculation` timing; O's spec-level `size_net_of_fees` (per-request flag, 2 of 3); O's `PercentOfScope` basis (expressed as `ScopeFraction`); S's `ReplaceByKey` / `Fifo` modes (callsite semantics → adapter, R5-4). - TUs: `native_order.{hpp,cpp}`, `native_execution_consumer.cpp`, `native_run_spec.cpp`, the definition hash. @@ -1381,7 +1381,7 @@ virtual void on_native_margin_call(const native_order::MarginCallEvent&) {} - 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:13137-13158). 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:2575), 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:4662-4675 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). +- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:4654-4667 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). ### A.4 L5 — calculation timing (F P4 + O G8, G9 + S P6) @@ -1401,10 +1401,10 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1621-1622) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6982-6994) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1621-1622) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6978-6990) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). 2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5616-5620) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1659-1666) is calibrated against it. 3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6835-6842). `on_native_tick` stays the observation hook (S). -4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2557); valid in bar-open, applied and tick callbacks. +4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2553); valid in bar-open, applied and tick callbacks. 5. The kernel never attempts language-state rollback; that stays Pine's (`snapshot / restore / commit_coof_script_state`, pine_scheduler_native.cpp:52-104) (O). ### A.5 L6 — native HTF subscriptions (F §2.iv + O G10 + S P7; design in §2.iv) @@ -1490,7 +1490,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:1317) (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:373), 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:13690-13788); 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:4662-4675 and the per-point walk), `native_order.hpp`, the state hash. +- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:4654-4667 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/native-settlement.md b/docs/native-settlement.md index 54542fa2..985cb156 100644 --- a/docs/native-settlement.md +++ b/docs/native-settlement.md @@ -119,7 +119,8 @@ refused, typed, as above -- the settlement's own refusal shows beforehand in boundary re-check below are made at execution only; and `Flatten` is never refused for a quantity. On a quantity grid (`NativeRunSpec::quantity_grid`) the book's own quantities are on the grid, for a request that settles them in one -fill (no point budget, no group deduction pending against it): a +fill (no point budget, no group deduction pending against it that takes any; a +pending total its units absorb takes none, R5 lane K-OCA-KEEP): a `ScopeFraction` whose product is its scope's held total as it stands -- `fraction == 1` of the gross scope -- resolves to that total, which the grid does not floor (a scope net of siblings' claims, or frozen at acceptance at diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index a11263dc..497941c2 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -462,7 +462,7 @@ spec fields. The rich `run(bars, n, input_tf, script_tf, inputs, syminfo, overrides, …)` overload (`engine.hpp:1623-1634`) is **not** refused as a source mutation: it reaches `NativeExecutionConsumer::run_rich` -(`native_execution_consumer.cpp:9150-9190`), which admits the begin, checks the +(`native_execution_consumer.cpp:9106-9146`), which admits the begin, checks the timeframe arguments against the spec, preflights and pumps the batch exactly like the plain overload. `inputs` / `syminfo` / `overrides` are carried only as `NativeBeginArgs` fields to `prepare_native_begin` — the overrides as the @@ -626,13 +626,13 @@ a host reacts to its own execution and may submit again. A request born there, mid-bar on a continuous segment, is eligible on the **remaining path suffix** of that segment — the birth is admitted at the current cursor and the geometric search then sees only the unconsumed suffix (`born_on_remaining_path`, -`native_execution_consumer.cpp:5616-5620`). Requests accepted before the +`native_execution_consumer.cpp:5611-5615`). Requests accepted before the segment, and discrete points, keep the ordinary birth gate above. `on_native_bar_open` fires at the modeled opening, before that point's matching -pass (`native_execution_consumer.cpp:7066-7068`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:7062-7064`). **Lookahead warning:** the `Bar` it receives is the *complete* script bar — the consumer has already set -`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6958`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6954`), the complete bar unless the spec asks for `NativeOpenBarView::OpenOnly` — so its high, low and close are the finished bar's, not what is known at the open. A host that must decide on open-only information reads @@ -2034,7 +2034,7 @@ default, set while no run is active — because each row is a full the live state, not the run's length: the closed rows enter through a running digest). With the switch on, one row follows each point, after the extremes that point just folded -(`record_script_report_point`, `native_execution_consumer.cpp:7802`), so +(`record_script_report_point`, `native_execution_consumer.cpp:7783`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -2845,7 +2845,7 @@ Only completed buckets are published, so this recipe has no lookahead by construction. It is the same class the kernel's own subscription evaluator aggregates with, and the one the kernel's `script_bucket_completions` query feeds when the Pine scheduler asks how its input span buckets -(`TimeframeAggregator` `native_execution_consumer.cpp:7884`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:7863`). What it does **not** give you is what a declared subscription does: an `authoritative_bars` feed, the `gaps` and `lookahead` delivery rules, the lazy-seal chronology, a C spelling, and the @@ -3431,11 +3431,15 @@ engine quantity; a fraction that does not buy one whole step is submit. The one exception is a fraction whose product is its scope's held total as it stands -- `fraction == 1` of the gross scope, "close it all", for a request that settles it in one fill (no point budget, no group deduction -pending against it): it resolves to that total, which is the fold of the -book's own lots and is not floored, so it closes the scope whole whatever the -grid (R5 lane K-ULP4; floored, it closed up to a step short, left a dust lot, -or found nothing to close). A scope net of siblings' claims, or frozen at -acceptance at another total, is floored like any other fraction. +pending against it that takes any): it resolves to that total, which is the +fold of the book's own lots and is not floored, so it closes the scope whole +whatever the grid (R5 lane K-ULP4; floored, it closed up to a step short, left +a dust lot, or found nothing to close). A pending total the units absorb takes +none, so such a request settles them all too (R5 lane K-OCA-KEEP; until then +the fraction was floored and left a dust lot, and a host-sized close answered +with its scope's total was refused `InvalidTerms`). A scope net of siblings' +claims, or frozen at acceptance at another total, is floored like any other +fraction. Neither kind is emitted by the Pine adapter, which keeps resolving its own `HostSized` terms; `native_order` values therefore belong to `native_order_v7`. diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index bb115364..eadf0143 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -126,8 +126,8 @@ rather than defaulted. | `currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:537 | `hello_kernel.cpp` | With `ticker` native_run_spec.hpp:567, `tickerid` native_run_spec.hpp:568, `type` native_run_spec.hpp:569, `basecurrency` native_run_spec.hpp:571, `description` native_run_spec.hpp:572 and `volumetype` native_run_spec.hpp:573 this is Pine's whole `syminfo` block, named rather than inherited from a chart. | | `commission_type` | `NativeFeeKind` native_run_spec.hpp:19 | `fee_kind` pineforge.h:540 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | | `commission_value` | `fee_value` native_run_spec.hpp:589 | `fee_value` pineforge.h:540 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | -| `slippage` | `slippage_ticks` native_run_spec.hpp:583 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3958-3959. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7524-7525, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | +| `slippage` | `slippage_ticks` native_run_spec.hpp:583 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3950-3951. TradingView's round-then-slip order is adapter-only. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7505-7506, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | | `pyramiding` | `max_open_lots` native_run_spec.hpp:595 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:2914). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). | | `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:344 with `size_basis` native_c_api.h:2106 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | | `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2110 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | @@ -296,7 +296,7 @@ is `strategy_native_submit_v1` native_c_api.h:2679 over a | `strategy.oca.reduce` | `GroupEffect::Reduce` native_order.hpp:466 | `PF_NATIVE_GROUP_REDUCE` native_c_api.h:475 | `tests/test_native_resting_group_contract.cpp` | The sibling's quantity is reduced instead of cancelled. | | `strategy.oca.none` | `NoGroup` native_order.hpp:467 | `PF_NATIVE_GROUP_NONE` native_c_api.h:468 | `hello_kernel.cpp` | The default: no group, no sibling effect. | | `strategy.direction.all` | `NativeOpenDirections::Both` native_run_spec.hpp:76 | `allowed_open_directions` pineforge.h:541 | `native_risk_limits_strategy.cpp` | The default. | -| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:541 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2954. | +| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:541 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2947. | | `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:541 | `tests/test_native_run_spec.cpp` | Same, the other way. | | `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:540 | `native_sized_report_strategy.cpp` | `fee_value` is percent ÷ 100 of the absolute account notional. | | `strategy.commission.cash_per_contract` | `NativeFeeKind::CashPerUnit` native_run_spec.hpp:21 | `fee_kind` pineforge.h:540 | `tests/test_native_selected_requests.cpp` | Account currency per unit. | @@ -404,7 +404,7 @@ block is the `PF_NATIVE_SPEC_EXT_RISK` native_c_api.h:735 tail. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:596 | `allowed_open_directions` pineforge.h:541 | `native_risk_limits_strategy.cpp` | The one `strategy.risk.*` rule that is already the kernel's, per ADR-0001's `risk` ruling row. Rejects with `OpeningDirection` native_execution_consumer.cpp:2954. | +| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:596 | `allowed_open_directions` pineforge.h:541 | `native_risk_limits_strategy.cpp` | The one `strategy.risk.*` rule that is already the kernel's, per ADR-0001's `risk` ruling row. Rejects with `OpeningDirection` native_execution_consumer.cpp:2947. | | `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:594 | `max_abs_units` pineforge.h:542 | `tests/test_native_run_spec.cpp` | Tests the *resulting* book. TradingView gates the *live* book before the fill, so an entry is refused only once the book already holds the limit — ruled **adapter-policy** in ADR-0001's `max_abs_units` row, with lane N12's measurement (the adapter ends at 4 where the kernel cap ends at 2). | | `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2316 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:259 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:261 is set. Blocks openings to the end of the run. | | `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:312 | `risk_max_intraday_loss` native_c_api.h:2319 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | @@ -461,7 +461,7 @@ part both ways (`tests/test_adapter_margin_schedule_differential.cpp`, M11). | `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1756 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | | bar-open pre-pass | `on_native_bar_open` native_host.hpp:877 | `on_bar_open` native_c_api.h:2464 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | | the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1061 | `strategy_native_partial_bar_v1` native_c_api.h:2972 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | -| a fill, as it happens | `on_native_applied` native_host.hpp:926 | `on_applied` native_c_api.h:2467 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5374. | +| a fill, as it happens | `on_native_applied` native_host.hpp:926 | `on_applied` native_c_api.h:2467 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5369. | | raw input before aggregation | `on_native_input` native_host.hpp:863 | `on_input` native_c_api.h:2462 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | | a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:916 | `on_sub_bar` native_c_api.h:2503 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | | `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1454 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. | diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index 640346ad..5504e8f7 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -463,12 +463,21 @@ bool explicit_reduction_units_representable(double units, double exposure) noexc } // A request that settles its units in one fill, all of them: no point budget -// splits them and no group deduction is pending against them. Only such a -// request's units can be the book's own quantity, closed as it stands -// (R5 lane K-ULP4). -bool settles_in_one_fill(const native_order::LiveRequest& live) noexcept { - return std::holds_alternative(live.request().capacity) - && !std::holds_alternative(live.pending); +// splits them and no group deduction is pending against them that takes any +// -- a pending total `units` absorb takes none (R5 lane K-ULP5), so it +// settles them all (R5 lane K-OCA-KEEP). Only such a request's units can be +// the book's own quantity, closed as it stands (R5 lane K-ULP4). +bool settles_in_one_fill(const native_order::LiveRequest& live, double units) noexcept { + if (!std::holds_alternative(live.request().capacity)) { + return false; + } + if (!std::holds_alternative(live.pending)) return true; + double deduction = 0.0; + double after = 0.0; + bool exhausted = false; + return native_order::WorkingRequestCore::effective_host_units(live.pending, units, &deduction, + &after, &exhausted) + && !exhausted && deduction == 0.0; } // `own_quantity`: the answered units are the book's own -- a closing size, @@ -4776,9 +4785,11 @@ std::optional NativeExecutionConsumer::resolve_sized_units( // settlement arithmetic moves off any decimal grid: floored, the close fell // up to a whole step short, left a dust lot, or found nothing to close. A // scope net of siblings' claims or frozen at another total, a point budget - // and a pending group deduction settle something else, and are floored as - // before (R5 lane K-ULP4). - if (units == scope && scope == facts.scope_exposure_units && settles_in_one_fill(live)) { + // and a pending group deduction that takes any settle something else, and + // are floored as before (R5 lane K-ULP4); a pending total the units absorb + // takes none (R5 lane K-OCA-KEEP). + if (units == scope && scope == facts.scope_exposure_units + && settles_in_one_fill(live, units)) { return scope; } return representable_units(units, native_order::ExecutionGridPolicy::SnapToGrid, @@ -4974,7 +4985,8 @@ std::optional NativeExecutionConsumer::consume_mat if (terms.units && (!std::isfinite(*terms.units) || *terms.units < 0.0)) { return terminal(native_order::MatchRejectReason::InvalidTerms, terms); } - const bool own_quantity = closing_size && terms.units && settles_in_one_fill(*live) + const bool own_quantity = closing_size && terms.units + && settles_in_one_fill(*live, *terms.units) && *terms.units == terms_facts.scope_exposure_units; if (!execution_terms_grid_representable( terms, host_sized, unresolved, terms_facts.scope_exposure_units, @@ -6522,7 +6534,8 @@ NativeCurrentExecutionPreview NativeExecutionConsumer::inspect_current_execution out.terms_rejection = native_order::MatchRejectReason::InvalidTerms; return out; } - const bool own_quantity = closing_size && terms.units && settles_in_one_fill(*live) + const bool own_quantity = closing_size && terms.units + && settles_in_one_fill(*live, *terms.units) && *terms.units == facts.scope_exposure_units; if (!execution_terms_grid_representable( terms, host_sized, unresolved, facts.scope_exposure_units, spec_ptr(), diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e13ba7f7..88b15f2b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1198,7 +1198,10 @@ list(APPEND TEST_SOURCES test_native_group_absorption) # probe under Cancel and Reduce, a retained trail and a carried binding, the # order core's receipts in queue order and in reverse, and a seeded battery of # groups re-priced in random subsets and orders under every option set against -# the plain replace, staged and direct. Source-free, so it also runs in the +# the plain replace, staged and direct; and K-ULP5's finding 2 -- on a quantity +# grid, a whole-scope close whose pending group total its units absorb closes +# the scope whole (a fraction floored and left dust, a host-sized close was +# refused InvalidTerms, on 2a03c658). Source-free, so it also runs in the # kernel-only profile. list(APPEND TEST_SOURCES test_native_group_keep_handle) # R5 lane D2-A (PB): the consumer's O(1) has_command_after against the two diff --git a/tests/test_native_group_keep_handle.cpp b/tests/test_native_group_keep_handle.cpp index 408197f2..d74a86c4 100644 --- a/tests/test_native_group_keep_handle.cpp +++ b/tests/test_native_group_keep_handle.cpp @@ -32,6 +32,13 @@ // best retained (retain_trigger_state), then a sibling's fill // bound-* a close bound to the book re-priced with keep_handle and // keep_binding, then a sibling's fill +// grid-absorbed-* K-ULP5's finding 2: on a 0.1 quantity grid, a close of its +// scope's whole held total -- a ScopeFraction{1}, or a +// host-sized close answered with that total -- whose pending +// group deduction its units absorb settles them all in one +// fill, so it closes the scope whole: no floor, no dust lot, no +// InvalidTerms refusal; matched at a stop, or previewed and +// executed as the current execution // core-* the order core itself: one cause's receipts in queue order // and in reverse, each found again on a replay // (AlreadyApplied) and read back in commit order; after a @@ -60,14 +67,18 @@ // replace only, whose successor is the newest request in both orders. // // Fail-before: compiled against 2a03c658 (K-ULP5) the TU builds and fails -// 464 of 3,356 checks: the 12 probe-*, trail-* and bound-* runs under a +// 490 of 3,436 checks: the 12 probe-*, trail-* and bound-* runs under a // keep_handle option set stop with code 2, discriminator 1, cause the member's // fill and recipient the re-priced request (the 12 plain and keep_binding runs -// complete); core-* refuses the re-priced recipient InvalidCause in queue -// order, staged and direct (the reverse order passes, and the older-cause -// replay fails at its first drain); and of the battery's 360 runs, every one -// of the 180 under a keep_handle option set stops with code 2, discriminator -// 1, while no plain drain leaves incarnation order. +// complete); grid-absorbed-fraction floors F to 1.1000000000000001 and leaves +// a 2.2648549702353193e-14 dust lot, and both grid-absorbed-host rows refuse F +// InvalidTerms and leave the lot open (the current one's preview answers +// InvalidTerms first, and its execution is refused), staged and direct; core-* +// refuses the re-priced recipient InvalidCause in queue order, staged and +// direct (the reverse order passes, and the older-cause replay fails at its +// first drain); and of the battery's 360 runs, every one of the 180 under a +// keep_handle option set stops with code 2, discriminator 1, while no plain +// drain leaves incarnation order. // Source-free: the kernel-only profile registers the row. #include "../src/native_execution_consumer.hpp" #include "native_match_book_fixture.hpp" @@ -79,6 +90,7 @@ #include #include #include +#include #include #include #include @@ -144,6 +156,17 @@ struct ScriptHost final : public NativeStrategyHost { const no::WorkingRequestCore& core() const { return as_native_consumer(execution_consumer()).request_core(); } + std::int64_t cycle() const { return position_cycle_seq_; } + // A host-sized close is answered with its scope's held total. + no::ExecutionTerms resolve_execution_terms( + const NativeExecutionTermsFacts& facts) const override { + no::ExecutionTerms terms{facts.default_resolved_price, std::nullopt, + no::OpeningShape::Transact}; + if (std::holds_alternative(facts.definition->request.intent)) { + terms.units = facts.scope_exposure_units; + } + return terms; + } // Submits under `name`; answers whether the kernel accepted it. bool sub(const std::string& name, no::Request r) { r.label = name; @@ -190,6 +213,9 @@ struct ScriptHost final : public NativeStrategyHost { std::map h; std::map names; std::vector queues; + std::optional preview; + std::optional executed; + bool live_after = true; int bar = 0; int refused = 0; int kept = 0; @@ -269,7 +295,7 @@ struct Outcome { std::unique_ptr host; }; -NativeRunSpec spec_for(const std::string& name) { +NativeRunSpec spec_for(const std::string& name, std::optional grid = std::nullopt) { NativeRunSpec s; s.event_retention = NativeEventRetention::Full; s.identity = {"k-oca-keep-" + name, 1}; @@ -284,6 +310,7 @@ NativeRunSpec spec_for(const std::string& name) { s.price_tick = 0.01; s.fee_kind = NativeFeeKind::CashPerExecution; s.fee_value = 0.0; + if (grid) s.quantity_grid = *grid; return s; } @@ -296,12 +323,12 @@ std::vector flat_bars(int n, double price = 100.0) { } Outcome run_one(const std::string& name, const Script& script, bool direct, Mode mode, - int bars = 8) { + const std::vector& tape = flat_bars(8), + std::optional grid = std::nullopt) { Outcome out; out.host = std::make_unique(direct, mode); out.host->script = script; - CHECK(out.host->configure_native(spec_for(name)).status == NativeSetupStatus::Applied); - const auto tape = flat_bars(bars); + CHECK(out.host->configure_native(spec_for(name, grid)).status == NativeSetupStatus::Applied); out.host->run(tape.data(), static_cast(tape.size())); const auto state = out.host->native_state(); out.completed = state.kind == NativeLifecycleKind::Completed; @@ -512,6 +539,138 @@ void bound(no::GroupEffect effect) { }); } +// K-ULP5's finding 2 (item 5 of this lane): on a 0.1 quantity grid, a request +// whose pending group deduction its units absorb settles them all in one fill +// (settles_in_one_fill, src/native_execution_consumer.cpp). L1 = +// 0.30000000000000004 less a bound close of 0.3 leaves one ulp, +// 5.5511151231257827e-17; L2 = 1001.1 less a bound close of 1000 leaves +// 1.1000000000000227, off the grid. F, bound to L2, closes it all: a +// ScopeFraction{1}, or a host-sized close the host answers with its scope's +// total. D, a market Reduce of the dust lot bound to L1 in F's Reduce group, +// defers the dust into F's pending total, below half an ulp of the +// 1.1000000000000227 F settles. F rests behind a stop the price later falls +// through (the matcher's path), or -- the current path -- F and D are +// accepted in one callback, D is executed there, and F is previewed and +// executed after it. On 2a03c658 the fraction was floored to +// 1.1000000000000001 and left a dust lot of 2.2648549702353193e-14, and the +// host-sized close was refused InvalidTerms -- as it was before K-ULP5 -- and +// left the lot open; each now closes it whole. +enum class GridClose { Fraction, HostSized, HostSizedCurrent }; + +void grid_absorbed(GridClose kind) { + const bool current = kind == GridClose::HostSizedCurrent; + Script s; + s[0] = [](ScriptHost& p) { + p.sub("L1", tx(0.30000000000000004)); + p.sub("L2", tx(1001.1)); + }; + s[2] = [](ScriptHost& p) { + auto cut1 = red(0.3); + cut1.owner = no::BindOpening{p.h["L1"], p.cycle()}; + p.sub("cut1", cut1); + auto cut2 = red(1000.0); + cut2.owner = no::BindOpening{p.h["L2"], p.cycle()}; + p.sub("cut2", cut2); + }; + s[4] = [kind, current](ScriptHost& p) { + no::Request f; + if (kind == GridClose::Fraction) { + f.intent = no::Reduce{no::ScopeFraction{1.0}}; + } else { + f.intent = no::HostSized{no::HostSizedKind::Close, std::nullopt}; + } + f = in_group(f, 4, 1, no::GroupEffect::Reduce); + if (!current) f.trigger = no::Stop{95.0}; + f.owner = no::BindOpening{p.h["L2"], p.cycle()}; + p.sub("F", f); + const auto lots = p.native_open_lots(std::numeric_limits::quiet_NaN()); + if (lots.empty()) return; + auto d = in_group(red(lots.front().signed_units), 4, 2, no::GroupEffect::Reduce); + d.owner = no::BindOpening{p.h["L1"], p.cycle()}; + p.sub("D", d); + if (!current) return; + NativeCurrentExecution run; + run.target = p.h["D"]; + const auto d_result = p.execute_current(run); + CHECK(std::holds_alternative(d_result)); + run.target = p.h["F"]; + p.preview = p.inspect_current_execution(run); + p.executed = p.execute_current(run); + p.live_after = p.core().find_live(p.h["F"]) != nullptr; + }; + std::vector tape; + for (int i = 0; i < 12; ++i) { + const double px = i < 8 ? 100.0 : 90.0; + tape.push_back(Bar{px, px, px, px, 10.0, 60'000LL * (i + 1)}); + } + const double dust = 0.30000000000000004 - 0.3; + const double residue = 1001.1 - 1000.0; + const std::string name = kind == GridClose::Fraction ? "grid-absorbed-fraction" + : current ? "grid-absorbed-host-current" : "grid-absorbed-host"; + std::optional staged; + for (const bool direct : {false, true}) { + scenario = name + (direct ? "/direct" : "/staged"); + Outcome o = run_one(name, s, direct, Mode::Replace, tape, 0.1); + CHECK(o.completed && o.code == 0 && o.discriminator == 0); + CHECK(o.refused == 0); + CHECK(!no::quantity_on_grid(residue, 0.1)); + CHECK(dust > 0.0 && residue - dust == residue); + double d_closed = -1.0, f_closed = -1.0, pending = -1.0, deduction = -1.0; + int f_rejected = -1; + for (const auto& row : o.host->native_events(0)) { + if (!row.command) continue; + if (const auto* a = std::get_if(&*row.command)) { + if (o.host->name_of(a->handle()) == "D") d_closed = a->closed_units; + if (o.host->name_of(a->handle()) == "F") f_closed = a->closed_units; + } else if (const auto* t = std::get_if(&*row.command)) { + if (o.host->name_of(t->handle()) == "F") { + pending = t->pending_total; + deduction = t->effective_deduction; + } + } else if (const auto* m = std::get_if(&*row.command)) { + if (o.host->name_of(m->handle()) == "F") f_rejected = static_cast(m->reason); + } + } + // D closes the dust; F takes nothing off its units for it and closes + // L2's residue whole, unfloored and admitted; the book is flat. + CHECK(d_closed == dust); + CHECK(pending == dust && deduction == 0.0); + CHECK(f_rejected == -1); + CHECK(f_closed == residue); + const auto lots = o.host->native_open_lots(std::numeric_limits::quiet_NaN()); + CHECK(lots.empty()); + // The preview, taken while D's dust stood pending on F, refuses nothing, + // and the current execution closes the residue there and then. + bool refused_terms = false; + if (current) { + CHECK(o.host->preview.has_value()); + if (o.host->preview) { + CHECK(!o.host->preview->refusal); + refused_terms = o.host->preview->terms_rejection.has_value(); + } + CHECK(!refused_terms); + const auto* applied = o.host->executed + ? std::get_if(&*o.host->executed) : nullptr; + CHECK(applied != nullptr); + CHECK(applied && applied->closed_units == residue); + CHECK(!o.host->live_after); + } + std::printf("%-34s completed=%d D=%.17g F=%.17g pending=%.17g deducted=%.17g " + "rejected=%d preview_terms_rejected=%d lots=%zu continuation=%016llx " + "broker=%016llx\n", + scenario.c_str(), o.completed ? 1 : 0, d_closed, f_closed, pending, deduction, + f_rejected, refused_terms ? 1 : 0, lots.size(), + static_cast(o.continuation), + static_cast(o.broker)); + if (!direct) { + staged = std::move(o); + } else if (staged) { + CHECK(o.transcript == staged->transcript); + CHECK(o.continuation == staged->continuation && o.broker == staged->broker); + } + } +} + // ---- The order core itself ------------------------------------------------------ const no::RunIdentity kCoreRun{"k-oca-keep-core", 1}; @@ -1415,6 +1574,9 @@ int main() { core_rows(effect, false); core_rows(effect, true); } + grid_absorbed(GridClose::Fraction); + grid_absorbed(GridClose::HostSized); + grid_absorbed(GridClose::HostSizedCurrent); Tally all; for (std::uint64_t seed : {11ULL, 22ULL, 33ULL}) all.add(battery(seed, 30)); scenario = "battery"; From a6e6a005a78d2c25dd04895fe619ec7e5955b613 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 06:03:21 +0800 Subject: [PATCH 27/83] Adapter: a pending entry's trailing exit starts its running best at the activation, as TradingView's does (R5 lane PAR-ORDERS, E14 / H-MEASURE Finding 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A strategy.exit(trail_points, trail_offset) written while its entry is still pending exited 1 to 24 bars after TradingView on all 13 trades of H-MEASURE's two running-best tapes (hm-orders-h3-f-{long,short}-pending-seed). Root cause. anchor_relative_exits placed the leg as the kernel's anchored child, relative_leg_shapes' Trail{0, 0, TrailTicks{k}} under a FromOwnerFill anchor. At the parent's fill resolve_anchored_level installs only its arm level, and the fill point's exit() re-run builds the leg TradingView rides: the same arm, plus Trail::best_seed = the activation (E14). The adoption compare in submit_or_replace (the same_trigger lambda) matches arm_price, ticks and offset and leaves best_seed out, so the fill point adopted the unseeded child. Its best started at the raw arm print, half a tick or less short of the activation, and its stop stayed out of reach. Fix, adapter only. anchorable_relative_exit refuses a definition with a trailing offset of a tick or more -- the one relative leg exit() seeds, and one the anchored child cannot carry -- so the parent is not anchored and its fill point submits the seeded leg, the rule every other shape the arm cannot spell already follows (explicit qty, the ANY rule, streams). No kernel line changes (ADR-0001 rule 2: the seed is TradingView's start, Pine policy, already expressed by the generic Trail::best_seed). The alternative (compare best_seed too, i.e. H-MEASURE's scratch "never adopt a seeded leg") books the same 41 tape trades but still anchors, arms and withdraws every such child; on the three regression batteries below neither variant moves a trade's times, prices, quantity, P&L or excursions. Rows flipped to TradingView's expectation in this commit: - test_pending_entry_trail_tapes: the 13 recorded divergences of §2 now assert TradingView's bar and price (before 0/8 + 0/5, after 8/8 + 5/5; the arm, §1, stays 28/28); the shape pins read {0,0,0} with no anchored Trail and a best_seed equal to the activation. The flipped row against the base library: FAIL 715 checks, 148 failures. - test_adapter_brackets_relower: rel-trail-offset, rel-short-trail, mag-rel-trail-offset and coof-rel-trail-offset re-pinned {1,1,0} -> {0,0,0}; every closed row, equity figure and book digest there unchanged. Three regression witnesses move and are re-pinned in the lane's named hash commit: test_adapter_quiet_bar (16 scenarios), test_publication_witness (17 runs) and test_adapter_live_state_equivalence (22 digests). What moves is hash input only -- the source-layer fold, the kernel's command events and recorded broker-state hash rows, and Trade::entry_incarnation (no anchored child consumes a request number now): every trade's times, prices, quantity, P&L, excursions and bar indices are identical across the 272 quiet-bar runs and the 10,122 live-state trades, and the publication witness without its three hash inputs prints the base table byte for byte. Docs: native-engine.md's adoption paragraph, its off-kernel table (a new row) and the 49-scenario split {220,123,93} -> {216,119,93}; pine-to-native and the design's anchored-relative-legs paragraph; digits re-anchored. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit d474d7286df8a13643bace526b99c6f530b76987) INT26 pick note: the adapter and test hunks applied without conflict; the ADR and design pages merged on citation digits only (int16_digit_merge per line). hunkcheck: every hunk of d474d728 carried. The sentences it retires recorded this divergence as open: H-MEASURE's pending-entry trail tapes (99d3b95, picked here as e1d0fa3) on the native-engine and pine-to-native pages. --- docs/adr/0001-kernel-adapter-boundary.md | 16 ++-- docs/design/native-feature-parity.md | 116 +++++++++++------------ docs/pages/native-engine.md | 22 +++-- docs/pages/pine-to-native.md | 11 ++- src/source/pine_adapter.cpp | 12 +++ tests/test_adapter_brackets_relower.cpp | 15 ++- tests/test_pending_entry_trail_tapes.cpp | 99 +++++++------------ 7 files changed, 141 insertions(+), 150 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index abdb3b23..26ad8d59 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -209,8 +209,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:676`) for TV sizing and fill spelling; `validate_execution_precommit` (`pine_strategy_host.cpp:701`), whose - `validate_precommit` (`pine_adapter.cpp:13034`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:13279`) to own the opening margin decision; `owns_lot_excursions() = true` + `validate_precommit` (`pine_adapter.cpp:13046`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:13291`) to own the opening margin decision; `owns_lot_excursions() = true` (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:786`), so the Pine host keeps its own per-lot MFE/MAE, `ab9714be`'s model (RULING A48). Measured against TradingView by R5 lane H-MEASURE (`tests/test_e19_excursion_tape.cpp`, `lab tv` tapes): where @@ -234,7 +234,7 @@ kernel and own these quirks, each at its site: - **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:9591-9596`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:333) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11671`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11683`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:454-460`, 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 @@ -249,8 +249,8 @@ kernel and own these quirks, each at its site: (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9). - **Margin.** The kernel owns the margin *mechanism* — the level solve, the check points, the kernel request, its re-pricing, the receipt — and the adapter answers its three policy hooks - with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14069`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14105`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14081`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:14117`) 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 @@ -261,7 +261,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:16804`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16816`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -643,8 +643,8 @@ 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:342`), `source_level_on_price_grid` (`:328`), `nearest_tick` (`:266`) / `source_bar_fill_tick` (`:287`) / `directional_tick` (`:311`), 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`, `tests/test_native_price_grid.cpp` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13867`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14922`), `chart_day_key` (`pine_adapter.cpp:13764`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13226-13227`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13879`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14934`), `chart_day_key` (`pine_adapter.cpp:13776`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13238-13239`): 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:2360-2362`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 6fe708e4..b38f0629 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -50,16 +50,16 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2737-2748; `entry()` pine_adapter.cpp:5289, `order()` pine_adapter.cpp:10480) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | 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:8343 → `Flatten{}` pine_adapter.cpp:8372-8373; `Reduce{ExplicitUnits}` pine_adapter.cpp:7490-7499; `ReverseTo` pine_adapter.cpp:6324-6325) | **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:2707-3914 → `host.replace` pine_adapter.cpp:2723-2803) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4722-4768, `cancel_all` pine_adapter.cpp:18032-18047; `live_by_source_key_` pine_adapter.hpp:1985; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4722-4768, `cancel_all` pine_adapter.cpp:18044-18059; `live_by_source_key_` pine_adapter.hpp:1985; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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:4189; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7175-7526) | **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:5513-5548, `cohort_for` pine_adapter.cpp:2501; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7473-7490) | **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:1270, `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:2746-2752) | **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:1092-1093 | 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:13004; lowering `submit_one` pine_adapter.cpp:9040-9052, `BindCohort` pine_adapter.cpp:9541-9547, `submit_leg` pine_adapter.cpp:9565-9589; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8645-8648; `materialize_relative_exits` pine_adapter.cpp:17322-17363, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19083-19112) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10880-10904), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15631, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15865) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:651-653 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:13016; lowering `submit_one` pine_adapter.cpp:9040-9052, `BindCohort` pine_adapter.cpp:9541-9547, `submit_leg` pine_adapter.cpp:9565-9589; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8645-8648; `materialize_relative_exits` pine_adapter.cpp:17334-17375, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19095-19124) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10880-10904), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15643, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15877) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:651-653 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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,25 +68,25 @@ 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:5692-5714; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1080 | 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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11779-11791) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11692-11829) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11791-11803) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11704-11841) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4021-4067) | **partial** — the read exists `trail_state` native_host.hpp:1080; `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:287-289) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9591-9596, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11692-11829, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8615-8841, `trail_one_shot` pine_adapter.cpp:9680-9809 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9591-9596, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11704-11841, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8615-8841, `trail_one_shot` pine_adapter.cpp:9680-9809 | 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:5902-5906, pine_adapter.cpp:5902) | **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:10915-12107) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4922-4956 | 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. | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:10915-12119) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4922-4956 | 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:2595-2650) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4957-4959 **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: rounded equity `source_money_round` pine_adapter.cpp:2547, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2556-2558) | **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:2608, `typed_entry_units` pine_adapter.cpp:2626; `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:2587; `percent_commission_live_equity` pine_adapter.cpp:4328-4368) | **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. | | SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2513-2528, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6645-6671) | **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:591, 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:4973-4976; 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:12478-12506, `compute_exit_reservation` pine_adapter.cpp:4411-4532) | **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. | +| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:12490-12518, `compute_exit_reservation` pine_adapter.cpp:4411-4532) | **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:4463-4786, `reserved_exit_qty` pine_adapter.cpp:8967-9277) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4443-4458) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6706), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7913), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12735), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13331, pine_adapter.cpp:13384) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15679) | 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:2595), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2608, `typed_entry_units` pine_adapter.cpp:2626), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18678) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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). | +| 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:6706), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7913), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12747), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13343, pine_adapter.cpp:13396) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15691) | 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:2595), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2608, `typed_entry_units` pine_adapter.cpp:2626), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18690) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7175-7526; ledger `close_batch_bar_` pine_adapter.hpp:2038-2049; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | @@ -95,21 +95,21 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2361) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13226-13229) is TV. | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13238-13241) is TV. | | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6180-6226, cap left unset `max_open_lots` pine_adapter.cpp:2360-2362) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13280-13345 and leaves the kernel gate unset pine_adapter.cpp:13279-13281) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13935-13936, `margin_pct` pine_adapter.cpp:13343) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14035-14213, `submit_margin_call_units` pine_adapter.cpp:14174-14220, `schedule_margin_call_path` pine_adapter.cpp:18185-18262) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13966-14002) | **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:983 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. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13292-13357 and leaves the kernel gate unset pine_adapter.cpp:13291-13293) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13947-13948, `margin_pct` pine_adapter.cpp:13355) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14047-14225, `submit_margin_call_units` pine_adapter.cpp:14186-14232, `schedule_margin_call_path` pine_adapter.cpp:18197-18274) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13978-14014) | **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:983 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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1308 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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16283-16299, `apply_fx_opening_margin_slice` pine_adapter.cpp:18263-18291) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16774-16813) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13315-13318) | **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:13896-13910, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14922-14952) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14965-15199) | **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:13706-13785) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:13924, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13983, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14691-14731, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14215-14342, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14215) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, the restore's `+1e-6` floor and the frozen signal-time units (M10); `margin_check_allowed` never admits the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low — a recorded divergence, open (M7). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3817-3884, `record_market_review` pine_adapter.cpp:15203-15261) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16295-16311, `apply_fx_opening_margin_slice` pine_adapter.cpp:18275-18303) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16786-16825) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13327-13330) | **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:13908-13922, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14934-14964) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14977-15211) | **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:13718-13797) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:13936, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13995, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14703-14743, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14227-14354, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14227) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, the restore's `+1e-6` floor and the frozen signal-time units (M10); `margin_check_allowed` never admits the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low — a recorded divergence, open (M7). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3817-3884, `record_market_review` pine_adapter.cpp:15215-15273) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -117,10 +117,10 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:881, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2318-2319) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7505-7509 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16118) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7062-7066; `execute_current` legal native_host.hpp:1092 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16130) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7062-7066; `execute_current` legal native_host.hpp:1092 | 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:7009 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7060 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1061 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:863, `NativeInputContext` native_host.hpp:761-766 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16261, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6916, native_execution_consumer.cpp:6895-6925; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16273, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6916, native_execution_consumer.cpp:6895-6925; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:866 fires per accepted realtime print native_execution_consumer.cpp:7144-7148; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | | 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:7499-7616 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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:2448-2504) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | @@ -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:5685-5732, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6268-6276; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11946-11955) is A | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11958-11967) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:472-479) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11962-11971, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11974-11983, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:1190 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:8671). **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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9032-9037, `exit_limit_trigger` pine_adapter.cpp:9566-9587), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11671) | 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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9032-9037, `exit_limit_trigger` pine_adapter.cpp:9566-9587), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11683) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19083-19112 | 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:19095-19124 | 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:522, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:548) 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:308-585) 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:603. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1273 | 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) | @@ -298,7 +298,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2914-2981) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2945-3013) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:45, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -522,11 +522,11 @@ line numbers are this tree's (683a82f); kernel semantics are | 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:2361); 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:13226-13227; 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:16774, `update_risk_state` pine_adapter.cpp:13867-13894): 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:13219), 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_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:13238-13239; 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:16786, `update_risk_state` pine_adapter.cpp:13879-13906): 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:13231), 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:1200-1208): +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:17724-17733); 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:16277); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16389), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14952-14961, `submit_intraday_loss_close` pine_adapter.cpp:14922-14950), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17131); 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:5961, `:7620`, `:8344`, `:8472`, `:11449`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18030-18033) | `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:13858-13860); 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:13764-13785, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16289); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16401), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14964-14973, `submit_intraday_loss_close` pine_adapter.cpp:14934-14962), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17143); 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:5961, `:7620`, `:8344`, `:8472`, `:11461`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18042-18045) | `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:13870-13872); 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:13776-13797, `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, @@ -678,24 +678,24 @@ TradingView books the touch at the open 16 of 16: the adapter's bar and price 16 of 16, the `Trail` alone 0 of 16. **Anchored relative legs: measured (lane F7 recorded it, R5 lane H-MEASURE -taped it).** A trailing `strategy.exit` issued while its entry is still pending -is materialized as the kernel's anchored child at the fill -(`relative_leg_shapes`, M17) and names no `best_seed`: its ride starts where the -kernel arms it. Six `lab tv` tapes of that shape (NYSE:F 15m, the exit written -on the entry's signal bar; `tests/fixtures/pending_entry_trail`, replayed by -`tests/test_pending_entry_trail_tapes.cpp`) settle both halves. The arm: on 28 -trades whose arming bar's raw extreme stops inside the activation's tick cell -(16 market entries, 12 limit entries filling mid-bar), TradingView exits on -that bar at the activation ∓ 1 tick 28 of 28, and so does the adapter's -anchored child (E5's quantized arm holds for a pending entry). The running -best: on 13 trades whose prints after the arm fall between the two stops a best -can give, TradingView's best starts AT the activation 13 of 13, as E14 measured -for a filled entry; the anchored child's starts at the raw arm print, and the -adapter exits 1 to 24 bars later on all 13. The fill point's adoption compare -leaves `best_seed` out, so it adopts the unseeded child where `exit()` would -have submitted a seeded leg. These are recorded divergences (each row pinned at -the engine's bar and price); refusing to adopt a seeded fill-point leg closes -all 13 in a scratch build. +taped it, lane PAR-ORDERS fixed it).** A trailing `strategy.exit` issued while +its entry is still pending was materialized as the kernel's anchored child at +the fill (`relative_leg_shapes`, M17), which names no `best_seed`: its ride +started where the kernel arms it. Six `lab tv` tapes of that shape (NYSE:F 15m, +the exit written on the entry's signal bar; `tests/fixtures/pending_entry_trail`, +replayed by `tests/test_pending_entry_trail_tapes.cpp`) settle both halves. The +arm: on 28 trades whose arming bar's raw extreme stops inside the activation's +tick cell (16 market entries, 12 limit entries filling mid-bar), TradingView +exits on that bar at the activation ∓ 1 tick 28 of 28 (E5's quantized arm +holds for a pending entry). The running best: on 13 trades whose prints after +the arm fall between the two stops a best can give, TradingView's best starts +AT the activation 13 of 13, as E14 measured for a filled entry; the anchored +child's started at the raw arm print, and the adapter exited 1 to 24 bars later +on all 13, because the fill point's adoption compare left `best_seed` out and +adopted the unseeded child where `exit()` would have submitted a seeded leg. +Lane PAR-ORDERS makes a trailing offset of a tick or more unanchorable +(`anchorable_relative_exit`): the fill point submits the seeded leg, and the +row books TradingView's bar and price on all 41 trades. **The trail stop's own ULP (lane E16, closing E14's finding).** E14's tape left one trade of `e14-f-long-shallow-next` pinned as a recorded divergence: @@ -1014,7 +1014,7 @@ moving a single value, but not from inside lane F9's files: (`has_full_entry_bracket` pine_adapter.cpp:8756). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1696, `revive_brackets_after_margin` pine_adapter.cpp:1918, - `preserved_by_close_all` pine_adapter.cpp:11480), and + `preserved_by_close_all` pine_adapter.cpp:11492), 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. @@ -1258,7 +1258,7 @@ unchanged after it. | 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5757) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290); 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:16118-16147) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:16130-16159) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | @@ -1303,7 +1303,7 @@ unchanged after it. | **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:16283-16299) 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:2645) 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:16295-16311) 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:2645) 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. | @@ -1379,7 +1379,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:13137-13158). 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:2575), 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:13149-13170). 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:2575), 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:4654-4667 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). @@ -1401,7 +1401,7 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1621-1622) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6978-6990) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1624-1625) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6978-6990) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). 2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5616-5620) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1659-1666) is calibrated against it. 3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6835-6842). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2553); valid in bar-open, applied and tick callbacks. @@ -1489,7 +1489,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:1317) (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:373), 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:13690-13788); 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:13702-13800); 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:4654-4667 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 497941c2..65d859c4 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -984,12 +984,15 @@ functions `exit()` uses. The parent's fill point still runs the source same-bar ordering are facts of that point and have no kernel analogue; it *adopts* the armed child instead of submitting whenever that child is the request it was about to submit: the same trigger bits, the same OCA group, a -host-sized close of the book. The comparison leaves out a trail's -`best_seed`, so a pending entry's trailing exit adopts a child whose best -starts at the raw arm print where the request `exit()` builds would start it -at the activation; TradingView starts it at the activation, and the adapter -exits later on 13 of 13 taped trades (`tests/test_pending_entry_trail_tapes.cpp`, -recorded divergences). The quantized arm itself holds there, 28 of 28. Because the child is host-sized and +host-sized close of the book. A trailing offset of a tick or more is never +anchored: `exit()` starts that leg's running best at the activation +(`Trail::best_seed`), the anchored child cannot carry the seed, so +`anchorable_relative_exit` refuses the definition and the fill point submits +the seeded leg. On the six tapes of that shape +(`tests/test_pending_entry_trail_tapes.cpp`) the engine books TradingView's bar +and price 41 of 41, the quantized arm 28 of 28 and the running best 13 of 13; +the adopted unseeded child had exited 1 to 24 bars late on those 13 (lane +PAR-ORDERS). Because the child is host-sized and book-scoped, that holds for every quantity the source resolves at the match (a percentage, a sibling's remainder) and for every book the parent's fill leaves (flat, a reversal, a later same-id add), under `calc_on_order_fills` @@ -1004,12 +1007,13 @@ with its measurement (`tests/test_adapter_brackets_relower.cpp`): | explicit `qty=` (`rel-qty-explicit`) | `{0,0,0}` | `exit()` stages an explicit quantity per origin (`pending_bracket_legs_`) and submits it at a later flush: the fill point has no request for a child to be | | `strategy.cancel(exit id)` in the same evaluation (`rel-cancel-exit-id`) | `{0,0,0}` | the definition is gone before the evaluation ends: no leg exists | | `close_entries_rule = "ANY"`, stream runs | not anchored | the fill-point leg is cohort-bound (`BindCohort`), a scope the arm does not spell; a stream's fill point stages the leg | +| a trailing offset of a tick or more (`rel-trail-offset`, `rel-short-trail`, `mag-rel-trail-offset`, `coof-rel-trail-offset`) | `{0,0,0}` | `exit()` starts the leg's running best at the activation (`Trail::best_seed`, TradingView's start); the kernel's anchor installs the arm level alone, so the child cannot carry the seed | | a parent re-issued, cancelled or declined (`rel-limit-parent` `{12,4,8}`, `rel-reissue-changed` `{18,2,16}`, `rel-breakout-pair` `{40,14,24}`, `rel-declined` `{2,0,2}`) | withdrawn | the kernel ends a waiting child with its parent (`OwnerGone`), a replaced parent included; keeping the children across a replace needs a re-parent transition in the event log, which is an epoch decision | | a sibling with no capacity left (`rel-two-exits` `{4,2,2}`), a level below zero (`rel-negative-short` `{2,1,1}`) | withdrawn | the fill point submits nothing for that leg | Over the 49 pending-parent scenarios the suite pins, the split is -`{220, 123, 93}`, and every shape but the two recorded above runs on the -kernel. The queued definition and its +`{216, 119, 93}`, and every shape but the three `{0,0,0}` rows above runs on +the kernel. The queued definition and its shadow row stay: the definition outlives parents (it may be declared before any entry exists and is re-anchored when a parent is replaced) and is what the fallback re-runs, and the shadow row is the source projection of a child @@ -1450,7 +1454,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:14069-14093`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:14081-14105`), 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/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index eadf0143..9515f248 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -386,11 +386,12 @@ the whole book exactly as a C++ one does. The Pine adapter lowers its own queued relative legs exactly this way — a host-sized `Book` / `AfterArmPrint` child, with `resolve_anchored_level` carrying only TradingView's half-tick threshold — and adopts the armed child at -the parent's fill. An explicit `qty=`, `close_entries_rule="ANY"` and stream -runs keep the adapter's fill-point submission. The adopted child of a trailing -exit names no `best_seed`, so its ride starts at the raw arm print where -TradingView's starts at the activation: a recorded divergence on 13 of 13 taped -trades (`tests/test_pending_entry_trail_tapes.cpp`). +the parent's fill. An explicit `qty=`, `close_entries_rule="ANY"`, stream +runs and a trailing offset of a tick or more keep the adapter's fill-point +submission. The trailing leg starts its running best at the activation +(`Trail::best_seed`), as TradingView's does on 13 of 13 taped trades +(`tests/test_pending_entry_trail_tapes.cpp`), and the anchored child cannot +carry that seed. ## Risk limits {#pine_to_native_map_risk} diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index a818e11e..dfc6ca27 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -11025,6 +11025,16 @@ bool PineExecutionAdapter::anchorable_relative_exit( ++parents; } if (parents != 1) return false; + // A trailing offset of a tick or more is the one relative leg exit() + // seeds: its running best starts AT the activation (Trail::best_seed), as + // TradingView's does on 13 of 13 pending-entry tapes + // (tests/test_pending_entry_trail_tapes.cpp). The anchored child cannot + // carry that seed -- the kernel's FromOwnerFill anchor installs the arm + // level alone -- and adopted, it rode from the raw arm print and exited 1 + // to 24 bars late. So the definition is not anchorable, and the parent's + // fill point submits the seeded leg (R5 lane PAR-ORDERS). + for (const auto& shape : relative_leg_shapes(value, only_long)) + if (std::holds_alternative(shape.trigger)) return false; parent = only; parent_long = only_long; return true; @@ -11052,6 +11062,8 @@ PineExecutionAdapter::relative_leg_shapes(const PendingRelativeExit& value, const bool has_offset = std::isfinite(value.trail_offset) && value.trail_offset >= 0.0; const double offset_ticks = has_offset ? std::floor(value.trail_offset) : kNaN; if (trail_ticks >= 1.0 && has_offset && offset_ticks >= 1.0) { + // Never placed: exit() seeds this leg, which the anchored child + // cannot carry, so anchorable_relative_exit refuses its definition. shapes.push_back({PineOrderFamily::ExitTrail, native_order::Trail{0.0, 0.0, native_order::TrailTicks{offset_ticks}}, diff --git a/tests/test_adapter_brackets_relower.cpp b/tests/test_adapter_brackets_relower.cpp index dd08a63e..6b0b55fe 100644 --- a/tests/test_adapter_brackets_relower.cpp +++ b/tests/test_adapter_brackets_relower.cpp @@ -1059,13 +1059,20 @@ struct Named { // (rel-limit-parent, rel-reissue-changed, the breakout pair, ...), a zero- // capacity sibling (rel-two-exits) and an unrepresentable level // (rel-negative-short). +// R5 lane PAR-ORDERS re-pinned the four trailing-offset shapes (rel-trail- +// offset, rel-short-trail, mag-rel-trail-offset, coof-rel-trail-offset) from +// {1,1,0} to {0,0,0}: exit() seeds a trailing offset's running best at the +// activation (Trail::best_seed), the anchored child cannot carry that seed, +// so the definition is not anchorable and the fill point submits the seeded +// leg (TradingView's start, tests/test_pending_entry_trail_tapes.cpp). No +// closed row, equity figure or book digest here moved; only these counts did. constexpr Named kShapes[] = { {"RelBracketTp", "rel-bracket-tp", Shape::RelBracketTp, 1, {2, 2, 0}}, {"RelBracketSl", "rel-bracket-sl", Shape::RelBracketSl, 1, {2, 2, 0}}, {"RelBracketEveryBar", "rel-bracket-every-bar", Shape::RelBracketEveryBar, 1, {2, 2, 0}}, {"RelExitBeforeEntry", "rel-exit-before-entry", Shape::RelExitBeforeEntry, 1, {4, 4, 0}}, {"RelTrailOneShot", "rel-trail-one-shot", Shape::RelTrailOneShot, 1, {1, 1, 0}}, - {"RelTrailOffset", "rel-trail-offset", Shape::RelTrailOffset, 1, {1, 1, 0}}, + {"RelTrailOffset", "rel-trail-offset", Shape::RelTrailOffset, 1, {0, 0, 0}}, {"RelTrailZero", "rel-trail-zero", Shape::RelTrailZero, 1, {1, 1, 0}}, {"RelShort", "rel-short", Shape::RelShort, 1, {2, 2, 0}}, {"RelThreeWay", "rel-three-way", Shape::RelThreeWay, 1, {2, 2, 0}}, @@ -1087,7 +1094,7 @@ constexpr Named kShapes[] = { {"RelNegativeShort", "rel-negative-short", Shape::RelNegativeShort, 1, {2, 1, 1}}, {"RelTwoExits", "rel-two-exits", Shape::RelTwoExits, 1, {4, 2, 2}}, {"RelGapFill", "rel-gap-fill", Shape::RelGapFill, 1, {2, 2, 0}}, - {"RelShortTrail", "rel-short-trail", Shape::RelShortTrail, 1, {1, 1, 0}}, + {"RelShortTrail", "rel-short-trail", Shape::RelShortTrail, 1, {0, 0, 0}}, {"RelProfitOnly", "rel-profit-only", Shape::RelProfitOnly, 1, {1, 1, 0}}, {"RelQtyExplicit", "rel-qty-explicit", Shape::RelQtyExplicit, 1, {0, 0, 0}}, {"RelReissueChanged", "rel-reissue-changed", Shape::RelReissueChanged, 1, {18, 2, 16}}, @@ -1099,14 +1106,14 @@ constexpr Named kShapes[] = { Mode::Magnifier}, {"MagRelBracketTp", "mag-rel-bracket-tp", Shape::RelBracketTp, 1, {2, 2, 0}, Mode::Magnifier}, {"MagRelBracketSl", "mag-rel-bracket-sl", Shape::RelBracketSl, 1, {2, 2, 0}, Mode::Magnifier}, - {"MagRelTrailOffset", "mag-rel-trail-offset", Shape::RelTrailOffset, 1, {1, 1, 0}, Mode::Magnifier}, + {"MagRelTrailOffset", "mag-rel-trail-offset", Shape::RelTrailOffset, 1, {0, 0, 0}, Mode::Magnifier}, {"MagRelTrailOneShot", "mag-rel-trail-one-shot", Shape::RelTrailOneShot, 1, {1, 1, 0}, Mode::Magnifier}, {"MagRelLimitParent", "mag-rel-limit-parent", Shape::RelLimitParent, 1, {12, 4, 8}, Mode::Magnifier}, {"MagRelStopParent", "mag-rel-stop-parent", Shape::RelStopParent, 1, {2, 2, 0}, Mode::Magnifier}, {"MagRelShort", "mag-rel-short", Shape::RelShort, 1, {2, 2, 0}, Mode::Magnifier}, {"MagRelSlippage", "mag-rel-slippage", Shape::RelSlippage, 1, {4, 4, 0}, Mode::Magnifier}, {"CoofRelBracketTp", "coof-rel-bracket-tp", Shape::RelBracketTp, 1, {2, 2, 0}, Mode::CalcOnOrderFills}, - {"CoofRelTrailOffset", "coof-rel-trail-offset", Shape::RelTrailOffset, 1, {1, 1, 0}, Mode::CalcOnOrderFills}, + {"CoofRelTrailOffset", "coof-rel-trail-offset", Shape::RelTrailOffset, 1, {0, 0, 0}, Mode::CalcOnOrderFills}, {"PoocRelBracketTp", "pooc-rel-bracket-tp", Shape::RelBracketTp, 1, {2, 2, 0}, Mode::ProcessOnClose}, {"PoocRelShort", "pooc-rel-short", Shape::RelShort, 1, {2, 2, 0}, Mode::ProcessOnClose}, {"RelPyramidSetOnce", "rel-pyramid-set-once", Shape::RelPyramidSetOnce, 2, {2, 2, 0}}, diff --git a/tests/test_pending_entry_trail_tapes.cpp b/tests/test_pending_entry_trail_tapes.cpp index eee5f4be..5b6f4d36 100644 --- a/tests/test_pending_entry_trail_tapes.cpp +++ b/tests/test_pending_entry_trail_tapes.cpp @@ -5,11 +5,12 @@ * Every tape here issues strategy.exit(trail_points=n, trail_offset=k) on the * SIGNAL bar, together with the MARKET strategy.entry it names, so the exit * is placed while its entry is still pending; the entry fills at the next - * bar's open. The Pine adapter lowers such an exit to the kernel's anchored - * child (relative_leg_shapes: a Trail whose arm threshold is FromOwnerFill, - * materialized at the parent's fill through resolve_anchored_level) and adopts - * that child at the fill point. None of the 23 E5 / E9 / E14 tapes measured - * this shape: every one of them issued its exit once the entry had filled. + * bar's open. The Pine adapter queues such an exit and submits its leg at the + * parent's fill, where exit() builds the Trail with its arm threshold and a + * best_seed. It is never the kernel's anchored child: that child cannot carry + * the seed (anchorable_relative_exit). None of the 23 E5 / E9 / E14 tapes + * measured this shape: every one of them issued its exit once the entry had + * filled. * The tapes are `lab tv` exports on NYSE:F 15m (sub-cent prints), * tests/fixtures/pending_entry_trail (README.md there lists them). * @@ -21,19 +22,19 @@ * that bar at activation -/+ 1 tick, 28 of 28 (market entries 8 + 8, 11 of * them on the fill bar itself; limit entries 6 + 6), so the pending-entry trail arms on the * tick-quantized path exactly as lane E5 measured for a filled entry. The - * adapter's anchored child arms there too (source_trail_arm_level in - * resolve_anchored_level) and books TradingView's bar and price, 16 of 16. + * fill point's leg arms there too (source_trail_arm_level in exit()) and + * books TradingView's bar and price, 28 of 28. * * 2. The running best (lane E14's open measurement). Same shape, but the prints * after the arm land between the two stops a best can give: TradingView's * exit is the stop of a best that starts AT the activation (13 of 13), the - * reading lane E14 measured for a filled entry. The adapter's anchored child - * names no Trail::best_seed, so its best starts at the raw arm print, half a - * tick or less short of the activation, and its stop stays out of reach: - * the engine exits 1 to 24 bars later, 13 of 13. These are RECORDED - * divergences (the E14 protocol): each row asserts the engine DIFFERS from - * TradingView at exactly the bar and price listed, so the row fails loudly - * the day an anchored seed lands and the mask has to go, deliberately. + * reading lane E14 measured for a filled entry. The fill point's leg names + * that start as its Trail::best_seed, and the engine books TradingView's + * bar and price, 13 of 13. Lane H-MEASURE recorded these 13 as divergences: + * the adapter then adopted the kernel's anchored child, which names no + * seed, so its best started at the raw arm print, half a tick or less short + * of the activation, and it exited 1 to 24 bars later. Lane PAR-ORDERS + * flipped them: a seeded definition is not anchorable. */ #include @@ -82,12 +83,6 @@ struct FeedBar { #include "fixtures/pending_entry_trail/bars.inc" -// The engine's exit on a recorded divergence: bars after the fill bar, price. -struct EngineExit { - int bars_after_fill; - double price; -}; - enum class Question { Arm, RunningBest }; constexpr double kOffsetOne[] = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}; @@ -102,34 +97,21 @@ struct Probe { int entries; int bars_per_entry; // the signal bar .. past the timeout close's fill Question question; - // Entries whose engine exit is a RECORDED divergence, bit per entry index, - // with the engine's exit for each (nullptr: none). - unsigned recorded_divergences; - const EngineExit* engine_exits; -}; - -// Row 3's measured divergences (the adapter's anchored child, no best_seed). -constexpr EngineExit kH3LongEngine[] = { - {7, 10.31}, {7, 10.61}, {23, 11.44}, {6, 11.74}, - {8, 11.74}, {14, 12.27}, {25, 13.26}, {7, 11.72}, -}; -constexpr EngineExit kH3ShortEngine[] = { - {3, 9.94}, {17, 10.17}, {3, 13.67}, {2, 13.66}, {13, 13.68}, }; const Probe kProbes[] = { {"hm-orders-h4-f-long-pending-arm", true, kH4LongPoints, kH4LongOffsets, nullptr, - &kH4Long[0][0], 8, 28, Question::Arm, 0u, nullptr}, + &kH4Long[0][0], 8, 28, Question::Arm}, {"hm-orders-h4-f-short-pending-arm", false, kH4ShortPoints, kH4ShortOffsets, nullptr, - &kH4Short[0][0], 8, 28, Question::Arm, 0u, nullptr}, + &kH4Short[0][0], 8, 28, Question::Arm}, {"hm-orders-h4-f-long-pending-limit-arm", true, kH4LimitLongPoints, kOffsetOne, - kH4LimitLongLevels, &kH4LimitLong[0][0], 6, 30, Question::Arm, 0u, nullptr}, + kH4LimitLongLevels, &kH4LimitLong[0][0], 6, 30, Question::Arm}, {"hm-orders-h4-f-short-pending-limit-arm", false, kH4LimitShortPoints, kOffsetOne, - kH4LimitShortLevels, &kH4LimitShort[0][0], 6, 30, Question::Arm, 0u, nullptr}, + kH4LimitShortLevels, &kH4LimitShort[0][0], 6, 30, Question::Arm}, {"hm-orders-h3-f-long-pending-seed", true, kH3LongPoints, kH3LongOffsets, nullptr, - &kH3Long[0][0], 8, 28, Question::RunningBest, 0xFFu, kH3LongEngine}, + &kH3Long[0][0], 8, 28, Question::RunningBest}, {"hm-orders-h3-f-short-pending-seed", false, kH3ShortPoints, kH3ShortOffsets, nullptr, - &kH3Short[0][0], 5, 28, Question::RunningBest, 0x1Fu, kH3ShortEngine}, + &kH3Short[0][0], 5, 28, Question::RunningBest}, }; // ── the tape ────────────────────────────────────────────────────────── @@ -271,10 +253,10 @@ std::string utc(std::int64_t ms) { } // What the kernel recorded for the exit label: the accepted request (an -// anchored Trail, and whether it names a best_seed) and its TrailArm. +// anchored Trail, and the best_seed it names) and its TrailArm. struct ExitRecord { bool anchored_trail = false; - bool best_seed = false; + std::optional best_seed; std::optional arm_bar; std::optional arm_reached; }; @@ -288,7 +270,7 @@ ExitRecord exit_record(const TapeHost& host, const char* label) { if (const auto* t = std::get_if(&accepted->request().trigger)) { record.anchored_trail = record.anchored_trail || std::holds_alternative(accepted->request().anchor); - record.best_seed = record.best_seed || t->best_seed.has_value(); + if (t->best_seed) record.best_seed = t->best_seed; } } else if (const auto* act = std::get_if(&*event.command)) { if (act->definition->request.label != label @@ -338,18 +320,20 @@ void test_tapes_replay(Question question) { } else { CHECK(fill_bar == 1 && same_price(tv.entry_price, bars[1].open)); } - // The shape under test: the exit ran on the kernel's anchored child, - // which the fill point adopted, and that child names no best_seed. + // The shape under test: no anchored child (the definition is not + // anchorable), so the fill point submits the leg exit() builds, + // whose running best starts AT the activation. The fill print is + // short of it on every trade here, so the seed is the activation. const auto stats = host.anchored(); - CHECK(stats.anchored == 1 && stats.adopted == 1 && stats.withdrawn == 0); + CHECK(stats.anchored == 0 && stats.adopted == 0 && stats.withdrawn == 0); const ExitRecord record = exit_record(host, label); - CHECK(record.anchored_trail); - CHECK(!record.best_seed); // TradingView's exit is the trail's (never the timeout), on the bar // that armed it or later, at the stop of a best that starts AT the // activation: activation -/+ k ticks. const int k = static_cast(probe.trail_offsets[e]); const double activation = tv.entry_price + sign * probe.trail_points[e] * kTick; + CHECK(!record.anchored_trail); + CHECK(record.best_seed && same_price(*record.best_seed, activation)); const int tv_bar = bar_of(bars, tv.exit_ms); CHECK(tv.exit_signal == label); CHECK(tv_bar >= 1); @@ -369,22 +353,6 @@ void test_tapes_replay(Question question) { } const bool matches = host.exit_time(0) == tv.exit_ms && same_price(host.exit_price(0), tv.exit_price); - if (probe.recorded_divergences & (1u << e)) { - // The anchored child's best starts at the raw arm print, so - // its stop sits below (a short's above) TradingView's and the - // engine exits later, at exactly this bar and price. - CHECK(!matches); - const EngineExit want = probe.engine_exits[e]; - CHECK(host.exit_time(0) == bars[static_cast(1 + want.bars_after_fill)].timestamp); - CHECK(same_price(host.exit_price(0), want.price)); - CHECK(host.exit_time(0) > tv.exit_ms); - std::printf(" trade %d: RECORDED divergence — engine exit %s @%.10g " - "(+%d bars after the fill), TradingView %s @%.10g (+%d)\n", - e + 1, utc(host.exit_time(0)).c_str(), host.exit_price(0), - bar_of(bars, host.exit_time(0)) - 1, utc(tv.exit_ms).c_str(), - tv.exit_price, tv_bar - 1); - continue; - } CHECK(host.exit_time(0) == tv.exit_ms); CHECK(same_price(host.exit_price(0), tv.exit_price)); if (matches) { @@ -395,9 +363,8 @@ void test_tapes_replay(Question question) { utc(tv.exit_ms).c_str(), tv.exit_price); } } - if (probe.recorded_divergences == 0) - std::printf(" %d of %d trades match TradingView bar and price\n", matched, - probe.entries); + std::printf(" %d of %d trades match TradingView bar and price\n", matched, + probe.entries); } } From 2922ade048f3fef83d72f7fbe19c27da9733b9a9 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 06:30:51 +0800 Subject: [PATCH 28/83] Adapter: a process_orders_on_close add stays under the pyramiding cap when the pending opposite entry is not in the close pass (R5 lane PAR-ORDERS, H-MEASURE Finding 6c) Under the magnifier with process_orders_on_close and calc_on_order_fills, H-MEASURE's differential script (variant v3, pyramiding=1) opened 86 lots on one bar: every fill's recalculation re-issued strategy.entry("ML") as a same-side market add and each one filled. TradingView's tape of that run (hm-mag-diff-v3) books 12 rows and ends flat. Root cause. entry()'s pyramiding check (pine_adapter.cpp, the accepted_in_cycle block) is skipped under process_orders_on_close whenever any opposite opening is pending (opposite_opening_pending): the cap is deferred to the close pass, where ab9714be judged a market add at its fill (add_to_pyramid_market: position_entry_count_ >= pyramiding) and TradingView against the trades then open. That pass fills an opposite opening first only if it processes it; here the pending opposite entry was a PS limit short resting from the previous bar, so nothing moved the book, and no fill-time check exists for a fixed-size add (validate_precommit's lot test covers the variable default sizes only). Each exempted add filled, the fill recalculated, and the next add came. Fix, adapter only (ADR-0001 rule 2: Pine's pyramiding is adapter command policy; no kernel line changes). The exemption still applies when the opposite opening is in the close pass -- a market entry, or one this script bar's own calculation placed -- where the add may be a reversal by the time it fills. With only a priced opposite entry resting from an earlier bar, the add is held to the lots open now: the count ab9714be and TradingView apply at the fill. The row: test_magnified_aggregated_tape gains "the pyramiding cap": no path of any of the eight variants holds more lots than its pyramiding at any script call, and the v3 magnified run books TradingView's 12 rows and ends flat (tape hm-mag-diff-v3 committed from H-MEASURE's evidence, README row added). Against the base library the row fails: "v3 ..., magnifier: 86 lots open at once, pyramiding 1", 8500 units open at the end. After: 176 checks, 0 failures. Every command the change drops, traced on the tapes and the regression batteries, is a same-side add with the lots then open already at the cap (v3 magnified: ML at bars 33 and 59, 1 lot at pyramiding 1; quiet-bar Random10 bar 42: 3 lots at pyramiding 2; Random14 bar 41: 2 at 2; Random21 bar 9: 1 at 1). Those three random runs are the only quiet-bar runs whose trades move; test_publication_witness (4 runs) and test_adapter_live_state_equivalence (9 digests) move with them. All are re-pinned in the lane's named hash commit. Docs: the design's magnified-residual paragraph records the bypass as closed; digits re-anchored. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 8d312d81fed3802d98f495f93791a3663285f21e) INT26 pick note: the adapter, test and fixture hunks applied without conflict; the ADR and design pages merged on citation digits only. --- docs/adr/0001-kernel-adapter-boundary.md | 20 ++-- docs/design/native-feature-parity.md | 107 ++++++++++-------- docs/pages/native-engine.md | 2 +- src/source/pine_adapter.cpp | 32 +++++- tests/fixtures/magnified_aggregated/README.md | 5 + .../hm-mag-diff-v3/meta.json | 17 +++ .../hm-mag-diff-v3/metrics.json | 40 +++++++ .../hm-mag-diff-v3/strategy.pine | 43 +++++++ .../hm-mag-diff-v3/tv_trades.csv | 25 ++++ tests/test_magnified_aggregated_tape.cpp | 62 +++++++++- 10 files changed, 285 insertions(+), 68 deletions(-) create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v3/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v3/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v3/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v3/tv_trades.csv diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 26ad8d59..afd3de9b 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -209,8 +209,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:676`) for TV sizing and fill spelling; `validate_execution_precommit` (`pine_strategy_host.cpp:701`), whose - `validate_precommit` (`pine_adapter.cpp:13046`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:13291`) to own the opening margin decision; `owns_lot_excursions() = true` + `validate_precommit` (`pine_adapter.cpp:13076`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:13321`) to own the opening margin decision; `owns_lot_excursions() = true` (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:786`), so the Pine host keeps its own per-lot MFE/MAE, `ab9714be`'s model (RULING A48). Measured against TradingView by R5 lane H-MEASURE (`tests/test_e19_excursion_tape.cpp`, `lab tv` tapes): where @@ -232,9 +232,9 @@ kernel and own these quirks, each at its site: (`select_exit_activation` src/compat/pine/exit_activation.cpp:42, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:10, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:46) 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:9591-9596`); the half-tick + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:9621-9626`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:333) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11683`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11713`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:454-460`, 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 @@ -249,8 +249,8 @@ kernel and own these quirks, each at its site: (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9). - **Margin.** The kernel owns the margin *mechanism* — the level solve, the check points, the kernel request, its re-pricing, the receipt — and the adapter answers its three policy hooks - with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14081`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14117`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14111`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:14147`) 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 @@ -261,7 +261,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:16816`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16846`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -282,7 +282,7 @@ 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:8282`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:8312`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:680`), and the two fills Pine refuses to recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5641`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and @@ -643,8 +643,8 @@ 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:342`), `source_level_on_price_grid` (`:328`), `nearest_tick` (`:266`) / `source_bar_fill_tick` (`:287`) / `directional_tick` (`:311`), 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`, `tests/test_native_price_grid.cpp` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13879`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14934`), `chart_day_key` (`pine_adapter.cpp:13776`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13238-13239`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13909`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14964`), `chart_day_key` (`pine_adapter.cpp:13806`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13268-13269`): 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:2360-2362`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index b38f0629..f32b4008 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,19 +47,19 @@ 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:2737-2748; `entry()` pine_adapter.cpp:5289, `order()` pine_adapter.cpp:10480) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | 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:8343 → `Flatten{}` pine_adapter.cpp:8372-8373; `Reduce{ExplicitUnits}` pine_adapter.cpp:7490-7499; `ReverseTo` pine_adapter.cpp:6324-6325) | **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 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2737-2748; `entry()` pine_adapter.cpp:5289, `order()` pine_adapter.cpp:10510) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | 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:8373 → `Flatten{}` pine_adapter.cpp:8402-8403; `Reduce{ExplicitUnits}` pine_adapter.cpp:7520-7529; `ReverseTo` pine_adapter.cpp:6354-6355) | **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:2707-3914 → `host.replace` pine_adapter.cpp:2723-2803) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4722-4768, `cancel_all` pine_adapter.cpp:18044-18059; `live_by_source_key_` pine_adapter.hpp:1985; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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:4189; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7175-7526) | **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:5513-5548, `cohort_for` pine_adapter.cpp:2501; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7473-7490) | **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:1270, `CancelReason::OwnerGone` native_order.hpp:949 | K | — | F:A6 O:A8 O:A9 S:O5 | FIFO *ordering* is host-computed (O) | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4722-4768, `cancel_all` pine_adapter.cpp:18074-18089; `live_by_source_key_` pine_adapter.hpp:1985; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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:4189; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7205-7556) | **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:5513-5548, `cohort_for` pine_adapter.cpp:2501; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7503-7520) | **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:1270, `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:2746-2752) | **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:1092-1093 | 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:13016; lowering `submit_one` pine_adapter.cpp:9040-9052, `BindCohort` pine_adapter.cpp:9541-9547, `submit_leg` pine_adapter.cpp:9565-9589; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8645-8648; `materialize_relative_exits` pine_adapter.cpp:17334-17375, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19095-19124) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10880-10904), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15643, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15877) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:651-653 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:13046; lowering `submit_one` pine_adapter.cpp:9070-9082, `BindCohort` pine_adapter.cpp:9571-9577, `submit_leg` pine_adapter.cpp:9595-9619; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8675-8678; `materialize_relative_exits` pine_adapter.cpp:17364-17405, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19125-19154) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10910-10934), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15673, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15907) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:651-653 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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,26 +68,26 @@ 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:5692-5714; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1080 | 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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11791-11803) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11704-11841) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11821-11833) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11734-11871) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4021-4067) | **partial** — the read exists `trail_state` native_host.hpp:1080; `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:287-289) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9591-9596, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11704-11841, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8615-8841, `trail_one_shot` pine_adapter.cpp:9680-9809 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9621-9626, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11734-11871, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8645-8871, `trail_one_shot` pine_adapter.cpp:9710-9839 | 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:5902-5906, pine_adapter.cpp:5902) | **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:10915-12119) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4922-4956 | 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. | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:10945-12149) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4922-4956 | 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:2595-2650) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4957-4959 **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: rounded equity `source_money_round` pine_adapter.cpp:2547, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2556-2558) | **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:2608, `typed_entry_units` pine_adapter.cpp:2626; `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:2587; `percent_commission_live_equity` pine_adapter.cpp:4328-4368) | **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. | -| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2513-2528, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6645-6671) | **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`. | +| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2513-2528, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6675-6701) | **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:591, 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:4973-4976; 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:12490-12518, `compute_exit_reservation` pine_adapter.cpp:4411-4532) | **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:4463-4786, `reserved_exit_qty` pine_adapter.cpp:8967-9277) | **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. | +| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:12520-12548, `compute_exit_reservation` pine_adapter.cpp:4411-4532) | **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:4463-4786, `reserved_exit_qty` pine_adapter.cpp:8997-9307) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4443-4458) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6706), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7913), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12747), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13343, pine_adapter.cpp:13396) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15691) | 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:2595), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2608, `typed_entry_units` pine_adapter.cpp:2626), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18690) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7175-7526; ledger `close_batch_bar_` pine_adapter.hpp:2038-2049; 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. | +| 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:6736), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7943), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12777), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13373, pine_adapter.cpp:13426) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15721) | 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:2595), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2608, `typed_entry_units` pine_adapter.cpp:2626), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18720) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7205-7556; ledger `close_batch_bar_` pine_adapter.hpp:2038-2049; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | ### 1.4 Margin, admission, liquidation, risk @@ -95,21 +95,21 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2361) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13238-13241) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6180-6226, cap left unset `max_open_lots` pine_adapter.cpp:2360-2362) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13292-13357 and leaves the kernel gate unset pine_adapter.cpp:13291-13293) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13947-13948, `margin_pct` pine_adapter.cpp:13355) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14047-14225, `submit_margin_call_units` pine_adapter.cpp:14186-14232, `schedule_margin_call_path` pine_adapter.cpp:18197-18274) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13978-14014) | **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:983 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. | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13268-13271) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6207-6256, cap left unset `max_open_lots` pine_adapter.cpp:2360-2362) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13322-13387 and leaves the kernel gate unset pine_adapter.cpp:13321-13323) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13977-13978, `margin_pct` pine_adapter.cpp:13385) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14077-14255, `submit_margin_call_units` pine_adapter.cpp:14216-14262, `schedule_margin_call_path` pine_adapter.cpp:18227-18304) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14008-14044) | **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:983 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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1308 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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16295-16311, `apply_fx_opening_margin_slice` pine_adapter.cpp:18275-18303) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16786-16825) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13327-13330) | **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:13908-13922, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14934-14964) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14977-15211) | **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:13718-13797) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:13936, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13995, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14703-14743, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14227-14354, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14227) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, the restore's `+1e-6` floor and the frozen signal-time units (M10); `margin_check_allowed` never admits the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low — a recorded divergence, open (M7). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3817-3884, `record_market_review` pine_adapter.cpp:15215-15273) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16325-16341, `apply_fx_opening_margin_slice` pine_adapter.cpp:18305-18333) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16816-16855) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13357-13360) | **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:13938-13952, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14964-14994) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15007-15241) | **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:13748-13827) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:13966, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14025, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14733-14773, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14257-14384, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14257) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, the restore's `+1e-6` floor and the frozen signal-time units (M10); `margin_check_allowed` never admits the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low — a recorded divergence, open (M7). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3817-3884, `record_market_review` pine_adapter.cpp:15245-15303) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -117,10 +117,10 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:881, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2318-2319) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7505-7509 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16130) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7062-7066; `execute_current` legal native_host.hpp:1092 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16160) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7062-7066; `execute_current` legal native_host.hpp:1092 | 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:7009 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7060 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1061 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:863, `NativeInputContext` native_host.hpp:761-766 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16273, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6916, native_execution_consumer.cpp:6895-6925; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16303, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6916, native_execution_consumer.cpp:6895-6925; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:866 fires per accepted realtime print native_execution_consumer.cpp:7144-7148; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | | 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:7499-7616 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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:2448-2504) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | @@ -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:5685-5732, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6268-6276; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11958-11967) is A | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11988-11997) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:472-479) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11974-11983, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:12004-12013, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:1190 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:8671). **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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9032-9037, `exit_limit_trigger` pine_adapter.cpp:9566-9587), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11683) | 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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9062-9067, `exit_limit_trigger` pine_adapter.cpp:9596-9617), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11713) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19095-19124 | 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:19125-19154 | 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:522, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:548) 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:308-585) 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:603. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1273 | 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) | @@ -522,11 +522,11 @@ line numbers are this tree's (683a82f); kernel semantics are | 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:2361); 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:13238-13239; 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:16786, `update_risk_state` pine_adapter.cpp:13879-13906): 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:13231), 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_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:13268-13269; 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:16816, `update_risk_state` pine_adapter.cpp:13909-13936): 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:13261), 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:1200-1208): +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:17724-17733); 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:16289); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16401), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14964-14973, `submit_intraday_loss_close` pine_adapter.cpp:14934-14962), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17143); 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:5961, `:7620`, `:8344`, `:8472`, `:11461`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18042-18045) | `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:13870-13872); 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:13776-13797, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16319); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16431), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14994-15003, `submit_intraday_loss_close` pine_adapter.cpp:14964-14992), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17173); 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:5961, `:7650`, `:8374`, `:8502`, `:11491`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18072-18075) | `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:13900-13902); 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:13806-13827, `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, @@ -929,9 +929,16 @@ this lane re-pins the affected v19 witnesses once. every path, and `ab9714be`'s 9 were its own error (`tests/test_magnified_aggregated_tape.cpp`, the variants the magnified run books exactly; the ones it does not are recorded divergences of their - own: a pyramiding-cap bypass under the magnifier with - `process_orders_on_close` and `calc_on_order_fills`, and a stop entry - already triggered at its bar's close filling a bar late). + own: a stop entry already triggered at its bar's close filling a bar late). + **The pyramiding-cap bypass is closed (R5 lane PAR-ORDERS).** Under + `process_orders_on_close` a pending opposite entry deferred the cap to the + close pass even when that pass would not fill it first -- a priced entry + resting from an earlier bar -- and nothing checked a fixed-size add at its + fill, so under the magnifier with `calc_on_order_fills` every fill's + recalculation added a lot: 86 at once with `pyramiding=1`. Such an add is + now held to the lots open (`entry()`); the row asserts the cap on all + eight variants and three paths, the v3 magnified run booking TradingView's + 12 rows and ending flat. - The per-bar broker hash of an aggregated run moves because the kernel now folds its script coordinate and named input coordinate. Those v19 witnesses are re-pinned once in `tests/test_native_point_checks.cpp`; no pre-release @@ -1011,10 +1018,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:8756). Full-table scans + (`has_full_entry_bracket` pine_adapter.cpp:8786). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1696, `revive_brackets_after_margin` pine_adapter.cpp:1918, - `preserved_by_close_all` pine_adapter.cpp:11492), and + `preserved_by_close_all` pine_adapter.cpp:11522), 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. @@ -1258,7 +1265,7 @@ unchanged after it. | 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5757) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290); 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:16130-16159) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:16160-16189) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | @@ -1303,7 +1310,7 @@ unchanged after it. | **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:16295-16311) 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:2645) 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:16325-16341) 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:2645) 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. | @@ -1379,7 +1386,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:13149-13170). 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:2575), 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:13179-13200). 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:2575), 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:4654-4667 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). @@ -1489,7 +1496,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:1317) (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:373), 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:13702-13800); 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:13732-13830); 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:4654-4667 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 65d859c4..7258bf8d 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1454,7 +1454,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:14081-14105`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:14111-14135`), 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/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index dfc6ca27..aad0a416 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -6114,6 +6114,33 @@ void PineExecutionAdapter::entry(const SourceId& id, bool is_long, double limit_ && existing->second.family == PineOrderFamily::Entry && existing->second.is_long != is_long; }); + // Under process_orders_on_close a pending opposite opening defers the + // pyramiding cap below to the close pass, where ab9714be counted the + // entries then filled (add_to_pyramid_market's position_entry_count_) and + // TradingView counts the trades then open. That pass fills the opposite + // opening BEFORE this entry only when it processes it -- a market entry, + // or one this script bar's own calculation placed -- and the add may be a + // reversal by then. A priced opposite entry resting from an earlier bar is + // not in that pass, so the add is held to the lots open now. (Deferred + // without that bound, every fill's recalculation under the magnifier and + // calc_on_order_fills re-issued such an add past pyramiding=1: 85 lots on + // one bar where TradingView's tape books none -- R5 lane PAR-ORDERS.) + const bool opposite_opening_in_close_pass = std::any_of( + live_handles_.begin(), live_handles_.end(), + [&](const native_order::RequestHandle& handle) { + const auto existing = placement_.find(handle.incarnation); + if (existing == placement_.end()) return false; + const auto& row = existing->second; + if (!row.opening || row.family != PineOrderFamily::Entry + || row.is_long == is_long) { + return false; + } + const bool market = !finite_positive(row.exit_levels.limit) + && !finite_positive(row.exit_levels.stop); + return market + || (source_point + && row.placement_script_open_ms == source_point->decision.script_bar_open_ms); + }); // ab9714be src/compat/pine/market_admission.cpp:33-37: the explicit // flat-pair scope (explicit_pair_scope) carries no commission term; // a commissioned pyramiding=2 pair keeps the gross transaction. @@ -6207,7 +6234,10 @@ void PineExecutionAdapter::entry(const SourceId& id, bool is_long, double limit_ && accepted_in_cycle >= static_cast(config_.pyramiding) && !short_seed_final_candidate && !paired_all_in_reentry && !default_gross_over_cap_candidate - && !(config_.process_orders_on_close && opposite_opening_pending)) { + && !(config_.process_orders_on_close && opposite_opening_pending + && (opposite_opening_in_close_pass + || detail::run_position(require_host()).lot_count + < static_cast(config_.pyramiding)))) { // Source replacement erases the older same-id priced entry before // judging the replacement's pyramiding admission. A rejected // over-cap reissue therefore leaves neither the old nor the new diff --git a/tests/fixtures/magnified_aggregated/README.md b/tests/fixtures/magnified_aggregated/README.md index 588db6f7..be820c8c 100644 --- a/tests/fixtures/magnified_aggregated/README.md +++ b/tests/fixtures/magnified_aggregated/README.md @@ -16,8 +16,13 @@ window 2025-07-01 .. 2025-07-08), byte-identical. | `hm-mag-diff-v0` | plain, magnifier on | 12 | `53c28aaf8caa6ed1708a9aa42f2965db37724c87d95691fb16bbb56dbb69906e` | `tv-tape-hm-mag-diff-v0-53c28aaf` | | `hm-mag-diff-v2` | calc_on_order_fills, magnifier on | 12 | `444e7bc4eaac5b16e224007225bc55ed1c36fecd4453b8eab95e6c1ea70c6656` | `tv-tape-hm-mag-diff-v2-444e7bc4` | | `hm-mag-diff-v4` | slippage 2, pyramiding 3, magnifier on | 14 | `d18ca4ad316311a4aad91503800402f1873c927c3cadc575b3b1a1ab669d196e` | `tv-tape-hm-mag-diff-v4-d18ca4ad` | +| `hm-mag-diff-v3` | process_orders_on_close, calc_on_order_fills, magnifier on | 12 | `23a25293ac35ce433214a7719e26cc58cf90471e5848bb7610e7485f1b3d079b` | `tv-tape-hm-mag-diff-v3-23a25293` | | `hm-chart-diff-v0` | plain, magnifier off | 12 | `b2652feb01b83236315a92112acb5b108626d11d959ed4b17effc6087454ae2c` | `tv-tape-hm-chart-diff-v0-b2652feb` | | `hm-chart-diff-v4` | slippage 2, pyramiding 3, magnifier off | 14 | `5293cc0c2bc88dabba65ff73a024d88b28292845c537f0f4ccf58922e4d502a1` | `tv-tape-hm-chart-diff-v4-5293cc0c` | TradingView dates every magnified fill at its chart bar's open; the rows are compared at chart-bar granularity. Excursions are not compared. + +`hm-mag-diff-v3` (added by R5 lane PAR-ORDERS from H-MEASURE's evidence, +`exec/H-MEASURE-scratch/agg/tapes`) witnesses the pyramiding cap: 12 rows and a +flat book, where the engine once opened 85 lots on one bar with pyramiding=1. diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v3/meta.json b/tests/fixtures/magnified_aggregated/hm-mag-diff-v3/meta.json new file mode 100644 index 00000000..7cc667f0 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v3/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM MAG diff v3 pooc1 coof1 slip0 pyr1", + "slug": "hm-mag-diff-v3", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:48:37.100Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "fdf261ac53a43f17b278605edca70c1a3ad4f3dcbc9db2a0efe8d773c76eb991", + "tv_trades_sha256": "23a25293ac35ce433214a7719e26cc58cf90471e5848bb7610e7485f1b3d079b", + "tv_trades_rows": 24 +} diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v3/metrics.json b/tests/fixtures/magnified_aggregated/hm-mag-diff-v3/metrics.json new file mode 100644 index 00000000..afcf63a2 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v3/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-mag-diff-v3", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 12, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "fdf261ac53a43f17b278605edca70c1a3ad4f3dcbc9db2a0efe8d773c76eb991", + "tvTradesCsvHash": "23a25293ac35ce433214a7719e26cc58cf90471e5848bb7610e7485f1b3d079b", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74394", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "ba82214d01514331680dc4fb126236c9f3a35a2e1e946270ee568f0ea98a033f", + "decodedReportSha256": "695274e50da876f779e8a03f10c866aabd193ec837f4e5951a0277fce5319d80", + "closedTrades": 12, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:48:37.098Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v3/strategy.pine b/tests/fixtures/magnified_aggregated/hm-mag-diff-v3/strategy.pine new file mode 100644 index 00000000..ec9ddfde --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v3/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 3: +// process_orders_on_close=1, calc_on_order_fills=1, slippage=0, pyramiding=1, +// with the bar magnifier ON. NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM MAG diff v3 pooc1 coof1 slip0 pyr1", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=true) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v3/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-mag-diff-v3/tv_trades.csv new file mode 100644 index 00000000..db49300d --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v3/tv_trades.csv @@ -0,0 +1,25 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,3 +1,Entry long,2025-07-02 22:15,PL,11.47,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,3 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1154,11.768,1.02,0.232,11.8846,1.03,-1.1154,-0.10,17.538,0.02,1 +2,Entry long,2025-07-02 23:00,ML,11.54,100,1154,11.768,1.02,0.232,11.8846,1.03,-1.1154,-0.10,17.538,0.02,1 +3,Exit long,2025-07-03 01:30,Close position order,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-2.1166,-0.18,17.3048,0.02,6 +3,Entry long,2025-07-03 00:00,PL,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-2.1166,-0.18,17.3048,0.02,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1167,4.7661,0.41,0.2339,4.8833,0.42,0,0.00,22.0709,0.02,0 +4,Entry long,2025-07-03 02:15,PL,11.67,100,1167,4.7661,0.41,0.2339,4.8833,0.42,0,0.00,22.0709,0.02,0 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1172,6.7649,0.58,0.2351,6.8828,0.59,-1.1172,-0.10,28.8358,0.03,7 +5,Entry long,2025-07-03 02:15,ML,11.72,100,1172,6.7649,0.58,0.2351,6.8828,0.59,-1.1172,-0.10,28.8358,0.03,7 +6,Exit long,2025-07-03 23:15,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,26.5988,0.03,2 +6,Entry long,2025-07-03 22:45,PL,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,26.5988,0.03,2 +7,Exit short,2025-07-04 00:30,SX,11.8,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,34.362,0.03,3 +7,Entry short,2025-07-03 23:45,PS,11.88,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,34.362,0.03,3 +8,Exit long,2025-07-07 21:30,PX,11.62,100,1180,-18.2342,-1.55,0.2342,3.882,0.33,-18.118,-1.54,16.1278,0.02,2 +8,Entry long,2025-07-04 00:30,PL,11.8,100,1180,-18.2342,-1.55,0.2342,3.882,0.33,-18.118,-1.54,16.1278,0.02,2 +9,Exit short,2025-07-07 22:00,SX,11.75,100,1170,-5.2345,-0.45,0.2345,4.883,0.42,-5.117,-0.44,10.8933,0.01,2 +9,Entry short,2025-07-07 21:30,PS,11.7,100,1170,-5.2345,-0.45,0.2345,4.883,0.42,-5.117,-0.44,10.8933,0.01,2 +10,Exit long,2025-07-07 23:45,MX,11.58,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,1.6608,0.00,4 +10,Entry long,2025-07-07 22:45,ML,11.67,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,1.6608,0.00,4 +11,Exit long,2025-07-08 02:15,PX,11.51,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-4.57,0.00,7 +11,Entry long,2025-07-08 00:30,PL,11.57,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-4.57,0.00,7 +12,Exit long,2025-07-08 02:45,PX,11.58,100,1154,3.7688,0.33,0.2312,3.8846,0.34,-3.1154,-0.27,-0.8012,0.00,2 +12,Entry long,2025-07-08 02:15,PL,11.54,100,1154,3.7688,0.33,0.2312,3.8846,0.34,-3.1154,-0.27,-0.8012,0.00,2 \ No newline at end of file diff --git a/tests/test_magnified_aggregated_tape.cpp b/tests/test_magnified_aggregated_tape.cpp index 6aeaaf3d..ce7fcec7 100644 --- a/tests/test_magnified_aggregated_tape.cpp +++ b/tests/test_magnified_aggregated_tape.cpp @@ -25,6 +25,13 @@ * Variants kept here are the ones the magnified run books exactly as TradingView: * v0 (plain), v2 (calc_on_order_fills), v4 (slippage 2, pyramiding 3); controls: * the chart and plain-aggregated paths against hm-chart-diff-v0 / v4. + * + * The pyramiding cap (R5 lane PAR-ORDERS): no path of any variant ever holds + * more lots than its pyramiding setting at a script call. Under the magnifier, + * process_orders_on_close and calc_on_order_fills (v3, pyramiding=1) a resting + * opposite limit entry used to lift the cap from every fill recalculation's + * market add: 85 lots on one bar, where TradingView's tape (hm-mag-diff-v3) + * books 12 rows and ends flat. */ #include @@ -155,6 +162,7 @@ class MagDiffHost final : public source::PineStrategyHost { void on_source_bar(const Bar& b) override { const int k = bar_index_; // the replay's first bar is the Pine counter's origin const double units = signed_position_size(); + max_lots = std::max(max_lots, physical_position().lot_count); if (k % 7 == 3 && units == 0.0) { strategy_entry("PL", true, kNaN, b.high - 0.02); strategy_exit("PX", "PL", b.close + 0.06, b.close - 0.06); @@ -198,6 +206,7 @@ class MagDiffHost final : public source::PineStrategyHost { return out; } double open_units() const { return physical_position().signed_units; } + std::size_t max_lots = 0; // the most lots open at any script call }; enum class Path { Chart, Aggregated, Magnified }; @@ -240,20 +249,24 @@ std::string exit_signal(const MagDiffHost::Row& r) { bool near(double a, double b, double tol) { return std::fabs(a - b) <= tol; } -void replay(const char* slug, const Variant& v, Path path) { +void run_path(MagDiffHost& host, Path path) { static const std::vector fifteen = feed(kFord15); static const std::vector one = feed(kFord1m); + if (path == Path::Chart) { + host.run(fifteen.data(), static_cast(fifteen.size()), "15", "15", false); + } else { + host.run(one.data(), static_cast(one.size()), "1", "15", path == Path::Magnified); + } +} + +void replay(const char* slug, const Variant& v, Path path) { std::printf("-- %s [%s, %s]\n", slug, v.tag, path_name(path)); std::vector tape; for (const TapeTrade& t : read_tape(slug)) if (t.exit_ms < kWindowEnd) tape.push_back(t); const bool after_window = read_tape(slug).size() != tape.size(); MagDiffHost host(v); - if (path == Path::Chart) { - host.run(fifteen.data(), static_cast(fifteen.size()), "15", "15", false); - } else { - host.run(one.data(), static_cast(one.size()), "1", "15", path == Path::Magnified); - } + run_path(host, path); CHECK(host.last_error().empty()); const auto rows = host.rows(); CHECK(!tape.empty()); @@ -290,12 +303,47 @@ void replay(const char* slug, const Variant& v, Path path) { } } +// The cap on every path of every variant, and the v3 magnified run against its +// tape: TradingView's 12 rows, the book flat at the end. +void pyramiding_cap_holds(const Variant (&variants)[8]) { + for (const Variant& v : variants) { + for (const Path path : {Path::Chart, Path::Aggregated, Path::Magnified}) { + MagDiffHost host(v); + run_path(host, path); + CHECK(host.last_error().empty()); + const bool within = host.max_lots <= static_cast(v.pyramiding); + CHECK(within); + if (!within) + std::printf(" %s, %s: %zu lots open at once, pyramiding %d\n", v.tag, + path_name(path), host.max_lots, v.pyramiding); + } + } + MagDiffHost host(variants[3]); + run_path(host, Path::Magnified); + std::vector tape; + for (const TapeTrade& t : read_tape("hm-mag-diff-v3")) + if (t.exit_ms < kWindowEnd) tape.push_back(t); + CHECK(tape.size() == 12); + CHECK(host.rows().size() == tape.size()); + CHECK(host.open_units() == 0.0); + std::printf("-- the pyramiding cap: v3 magnified books %zu rows (TradingView %zu), " + "%zu lots at most, %g units open at the end\n", host.rows().size(), + tape.size(), host.max_lots, host.open_units()); +} + } // namespace int main() { const Variant v0{"v0 plain", false, false, 0, 1}; const Variant v2{"v2 calc_on_order_fills", false, true, 0, 1}; const Variant v4{"v4 slippage 2, pyramiding 3", false, false, 2, 3}; + const Variant all[8] = { + v0, {"v1 process_orders_on_close", true, false, 0, 1}, v2, + {"v3 process_orders_on_close, calc_on_order_fills", true, true, 0, 1}, v4, + {"v5 process_orders_on_close, slippage 2, pyramiding 3", true, false, 2, 3}, + {"v6 calc_on_order_fills, slippage 2, pyramiding 3", false, true, 2, 3}, + {"v7 process_orders_on_close, calc_on_order_fills, slippage 2, pyramiding 3", + true, true, 2, 3}}; // 1. the magnified run against TradingView's magnifier tapes replay("hm-mag-diff-v0", v0, Path::Magnified); replay("hm-mag-diff-v2", v2, Path::Magnified); @@ -305,6 +353,8 @@ int main() { replay("hm-chart-diff-v0", v0, path); replay("hm-chart-diff-v4", v4, path); } + // 3. the pyramiding cap + pyramiding_cap_holds(all); std::printf("\n%s magnified aggregated tape: %d checks, %d failures\n", tests_failed == 0 ? "PASS" : "FAIL", tests_passed + tests_failed, tests_failed); return tests_failed == 0 ? 0 : 1; From e1d7a984ef0acb6d69ffc35eaa3d6d0f053040de Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 06:50:38 +0800 Subject: [PATCH 29/83] Adapter: a process_orders_on_close stop entry its placing close already reached fills at that close, on every path (R5 lane PAR-ORDERS, H-MEASURE Finding 6b) H-MEASURE's differential script places a buy stop entry (PL, stop = high - 0.02) at a process_orders_on_close close. On its five signal bars whose close's tick reaches that stop, TradingView fills it at that close (the close's tick, slipped like any stop fill); the engine left it resting and filled it a bar late at the next open -- on the chart, the plain aggregated and the magnified path, with or without calc_on_order_fills. On the bars whose close stays short of the stop (k = 17, 45, 59) both rest. Root cause, two parts: 1. flush_pooc_marketable_limit_entry_fills (the post-calculation close pass, ab9714be pine_fills.cpp:7604-7646) took pure LIMIT entries only, and none under calc_on_order_fills. 2. on_bar_close's quiet-bar gate for that pass (and for flush_pooc_marketable_exit_fills) tested row.projection_created_bar == context.coordinate.interval_index, while both passes test projection_bar_index(context) -- the named input slot (K-IDX, 3ed0276f). Since K-IDX the two differ on an aggregated or magnified run, so the gate declared every such bar quiet and neither pass ever ran there (PERF-L4 wrote the gate, 770f7b24, when they were equal). Fix, adapter only (ADR-0001 rule 2: TradingView's close-time fill of a crossed stop is Pine policy; no kernel line changes): - the close pass also takes a pure stop entry of this bar whose stop the close's tick is at or through, under calc_on_order_fills too (limits keep their non-COOF scope), booking nearest_tick(close) plus the slip; only as the bar's lone entry marketable at the close -- two such entries keep the next open's both-marketable arbitration (lane L9b's P0-B shapes, ab9714be literals no tape orders at a close); - the gate tests projection_bar_index(context), the passes' own index. Rows flipped to TradingView in this commit: - test_magnified_aggregated_tape now replays v1 and v5 on all three paths, v3 on all three and v7 on the chart and aggregated paths (tapes hm-{mag,chart}-diff-v1/-v5, hm-mag-diff-v3 already added, hm-chart-diff-v3/-v7 committed from H-MEASURE's evidence). Every POOC stop entry books TradingView's bar and price. The rows (b) does not touch are RECORDED divergences asserted to differ: v3/v7 row 5 (a fill recalculation's market entry after a fill at bar 19's high, H-MEASURE Finding 6d, next commit), v7 rows 8-9 and v3's magnified row 2 (pre-existing, outside this lane). Against the previous commit's library the row fails 50 of 373 checks; after, 0. - test_l10am_entry_bar_priced_exit, the XAUUSD probe: its buy stop 3327.38 is placed at a close of exactly 3327.38. The pin was the owner's (ab9714be) entry at the next bar, 2025-04-16 17:00 UTC; TradingView's own trade 18 of that population probe enters at 16:45 UTC, the signal bar (Duration 6 bars; R4-D's data-probes tape). The row now asserts TradingView's bar. Corpus: one probe moves, toward its TradingView tape -- validation/order-deferred-flip-pooc-cross-bar-01 (process_orders_on_close, "open-guaranteed" stop entries). Its entry placed from the flat book after each weekly reset now fills at the signal bar's close. Of the 82 rows that move, the old output matched TradingView's bar on 25 and price on 58, the new one on 82 and 82; the 57 moved entries go 0 -> 57 of TradingView's bar and price, the whole file 791 -> 848 of 1584 rows. (Flips placed while in position still fill at the next open here, as before; TradingView fills those at the signal bar's close too -- not measured by this lane's rule, left unchanged.) scripts/corpus_parity_baseline.txt re-pins that one line (60df427d.. -> 602febda.., the sha lab remote's x86 run produced and a local run reproduces); every other probe is byte-identical. Regression batteries: test_adapter_quiet_bar and test_publication_witness move nothing; test_adapter_live_state_equivalence moves 3 configurations (reversals seeds 1152019, 1675664, 2722954, all process_orders_on_close), each exactly a lone stop entry its close crossed now filling at that close (L2 stop 118.5 / close 119, L 104.75 / 105.5, S2 82.75 / 80.5). Re-pinned in the lane's named hash commit. Docs: the design's magnified-residual paragraph records the stop entry as closed, with the gate's history; digits re-anchored. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 767bb2b1c5bed8d14de768973e8c8e32b7c89278) INT26 pick note: the adapter, test, fixture and baseline hunks applied without conflict (the accepted re-pin of validation/order-deferred-flip-pooc-cross-bar-01, 60df427d -> 602febda, toward TradingView); the ADR and design pages merged on citation digits only. --- docs/adr/0001-kernel-adapter-boundary.md | 2 +- docs/design/native-feature-parity.md | 35 +++++++---- scripts/corpus_parity_baseline.txt | 2 +- src/source/pine_adapter.cpp | 61 ++++++++++++++----- tests/fixtures/magnified_aggregated/README.md | 10 +++ .../hm-chart-diff-v1/meta.json | 17 ++++++ .../hm-chart-diff-v1/metrics.json | 40 ++++++++++++ .../hm-chart-diff-v1/strategy.pine | 43 +++++++++++++ .../hm-chart-diff-v1/tv_trades.csv | 25 ++++++++ .../hm-chart-diff-v3/meta.json | 17 ++++++ .../hm-chart-diff-v3/metrics.json | 40 ++++++++++++ .../hm-chart-diff-v3/strategy.pine | 43 +++++++++++++ .../hm-chart-diff-v3/tv_trades.csv | 25 ++++++++ .../hm-chart-diff-v5/meta.json | 17 ++++++ .../hm-chart-diff-v5/metrics.json | 40 ++++++++++++ .../hm-chart-diff-v5/strategy.pine | 43 +++++++++++++ .../hm-chart-diff-v5/tv_trades.csv | 29 +++++++++ .../hm-chart-diff-v7/meta.json | 17 ++++++ .../hm-chart-diff-v7/metrics.json | 40 ++++++++++++ .../hm-chart-diff-v7/strategy.pine | 43 +++++++++++++ .../hm-chart-diff-v7/tv_trades.csv | 29 +++++++++ .../hm-mag-diff-v1/meta.json | 17 ++++++ .../hm-mag-diff-v1/metrics.json | 40 ++++++++++++ .../hm-mag-diff-v1/strategy.pine | 43 +++++++++++++ .../hm-mag-diff-v1/tv_trades.csv | 25 ++++++++ .../hm-mag-diff-v5/meta.json | 17 ++++++ .../hm-mag-diff-v5/metrics.json | 40 ++++++++++++ .../hm-mag-diff-v5/strategy.pine | 43 +++++++++++++ .../hm-mag-diff-v5/tv_trades.csv | 29 +++++++++ tests/test_l10am_entry_bar_priced_exit.cpp | 12 ++-- tests/test_magnified_aggregated_tape.cpp | 60 +++++++++++++++--- 31 files changed, 902 insertions(+), 42 deletions(-) create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v1/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v1/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v1/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v1/tv_trades.csv create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v3/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v3/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v3/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v3/tv_trades.csv create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v5/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v5/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v5/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v5/tv_trades.csv create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v7/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v7/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v7/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v7/tv_trades.csv create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v1/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v1/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v1/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v1/tv_trades.csv create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v5/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v5/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v5/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-mag-diff-v5/tv_trades.csv diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index afd3de9b..f99326a9 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -261,7 +261,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:16846`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16879`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index f32b4008..f66fec85 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -50,15 +50,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2737-2748; `entry()` pine_adapter.cpp:5289, `order()` pine_adapter.cpp:10510) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | 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:8373 → `Flatten{}` pine_adapter.cpp:8402-8403; `Reduce{ExplicitUnits}` pine_adapter.cpp:7520-7529; `ReverseTo` pine_adapter.cpp:6354-6355) | **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:2707-3914 → `host.replace` pine_adapter.cpp:2723-2803) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4722-4768, `cancel_all` pine_adapter.cpp:18074-18089; `live_by_source_key_` pine_adapter.hpp:1985; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4722-4768, `cancel_all` pine_adapter.cpp:18107-18122; `live_by_source_key_` pine_adapter.hpp:1985; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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:4189; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7205-7556) | **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:5513-5548, `cohort_for` pine_adapter.cpp:2501; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7503-7520) | **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:1270, `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:2746-2752) | **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:1092-1093 | 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:13046; lowering `submit_one` pine_adapter.cpp:9070-9082, `BindCohort` pine_adapter.cpp:9571-9577, `submit_leg` pine_adapter.cpp:9595-9619; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8675-8678; `materialize_relative_exits` pine_adapter.cpp:17364-17405, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19125-19154) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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). | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8675-8678; `materialize_relative_exits` pine_adapter.cpp:17397-17438, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19158-19187) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10910-10934), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15673, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15907) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:651-653 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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 | @@ -86,7 +86,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:12520-12548, `compute_exit_reservation` pine_adapter.cpp:4411-4532) | **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:4463-4786, `reserved_exit_qty` pine_adapter.cpp:8997-9307) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4443-4458) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6736), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7943), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12777), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13373, pine_adapter.cpp:13426) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15721) | 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:2595), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2608, `typed_entry_units` pine_adapter.cpp:2626), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18720) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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). | +| 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:6736), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7943), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12777), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13373, pine_adapter.cpp:13426) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15721) | 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:2595), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2608, `typed_entry_units` pine_adapter.cpp:2626), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18753) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7205-7556; ledger `close_batch_bar_` pine_adapter.hpp:2038-2049; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | @@ -99,11 +99,11 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6207-6256, cap left unset `max_open_lots` pine_adapter.cpp:2360-2362) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | | MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13322-13387 and leaves the kernel gate unset pine_adapter.cpp:13321-13323) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13977-13978, `margin_pct` pine_adapter.cpp:13385) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14077-14255, `submit_margin_call_units` pine_adapter.cpp:14216-14262, `schedule_margin_call_path` pine_adapter.cpp:18227-18304) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **Closed:** `maintenance_long` / `maintenance_short` with `NativeLiquidationCheck::{PathAdverseExtreme, PathAdverseExtremeMark, CalculationOnly}`; the kernel solves the level, rests its own `Reduce`/`Flatten` bound to the live book, re-prices it with `CancelReason::Superseded` and books a `MarginCallEvent` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | F:D2 O:D3 S:M3 | TV parts → MG15 | +| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:14077-14255, `submit_margin_call_units` pine_adapter.cpp:14216-14262, `schedule_margin_call_path` pine_adapter.cpp:18260-18337) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14008-14044) | **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:983 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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1308 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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16325-16341, `apply_fx_opening_margin_slice` pine_adapter.cpp:18305-18333) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16816-16855) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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 | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16325-16341, `apply_fx_opening_margin_slice` pine_adapter.cpp:18338-18366) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16849-16888) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13357-13360) | **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:13938-13952, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14964-14994) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15007-15241) | **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 | @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19125-19154 | 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:19158-19187 | 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:522, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:548) 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:308-585) 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:603. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1273 | 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) | @@ -523,9 +523,9 @@ line numbers are this tree's (683a82f); kernel semantics are |---|---|---|---|---|---| | `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2361); 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:13268-13269; 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:16816, `update_risk_state` pine_adapter.cpp:13909-13936): 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:13261), 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_drawdown` | `update_risk_state(bar.close)` once per script bar at the close mark (`update_risk_state(bar.close)` pine_adapter.cpp:16849, `update_risk_state` pine_adapter.cpp:13909-13936): 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:13261), 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:1200-1208): +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:17724-17733); 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:16319); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16431), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14994-15003, `submit_intraday_loss_close` pine_adapter.cpp:14964-14992), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17173); 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:5961, `:7650`, `:8374`, `:8502`, `:11491`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18072-18075) | `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_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16319); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16431), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14994-15003, `submit_intraday_loss_close` pine_adapter.cpp:14964-14992), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17206); 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:5961, `:7650`, `:8374`, `:8502`, `:11491`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18105-18108) | `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:13900-13902); 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:13806-13827, `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 @@ -928,8 +928,19 @@ this lane re-pins the affected v19 witnesses once. script: TradingView books the script's 12 rows as this engine does on every path, and `ab9714be`'s 9 were its own error (`tests/test_magnified_aggregated_tape.cpp`, the variants the magnified - run books exactly; the ones it does not are recorded divergences of their - own: a stop entry already triggered at its bar's close filling a bar late). + run books exactly). **A `process_orders_on_close` stop entry the placing + close already reached is closed too (R5 lane PAR-ORDERS):** TradingView + fills it at that close -- the close's tick, slipped like any stop fill -- + on every path and with or without `calc_on_order_fills`, five entries on + each `-v1` / `-v3` / `-v5` / `-v7` tape, where the resting stop filled a + bar late at the next open. The close pass that fills it + (`flush_pooc_marketable_limit_entry_fills`) now takes a pure stop entry + that is the bar's lone entry marketable at the close; two such entries keep + the next open's both-marketable arbitration, which no tape orders at a + close. That pass also never ran on an aggregated or magnified run: its + quiet-bar gate tested the script-bar `interval_index` where the pass tests + the input slot (`projection_bar_index`), so since K-IDX every such run + skipped it, limit entries and exit legs included. **The pyramiding-cap bypass is closed (R5 lane PAR-ORDERS).** Under `process_orders_on_close` a pending opposite entry deferred the cap to the close pass even when that pass would not fill it first -- a priced entry diff --git a/scripts/corpus_parity_baseline.txt b/scripts/corpus_parity_baseline.txt index e676c228..fbea2634 100644 --- a/scripts/corpus_parity_baseline.txt +++ b/scripts/corpus_parity_baseline.txt @@ -154,7 +154,7 @@ b1c6e2f74a263c3ed0402b20cca655b329326c68b9e91405bf874d74be0e8fe0 validation/ord c9d1428ff858ea15fb8634b4634e4f438e0ff38b39e19f27e94d552ed98b821c validation/order-cross-exit-close-same-pass-01/engine_trades.csv 5804e69d5646d827f6c300b54d02c063c54e8071e999db3e4cbe9458b9c5413e validation/order-crosscurrent-opposite-market-pair-01/engine_trades.csv b71d4a6a1d3c177473f041e56504820f9c06c3ba4588fc8b8827c44f24503ed6 validation/order-deferred-flip-guaranteed-gap-stops-01/engine_trades.csv -60df427d47535da3fd9525410c58503e33090068dfdd7d447ab627faaf23c27a validation/order-deferred-flip-pooc-cross-bar-01/engine_trades.csv +602febda64b40ac5b089ddc13b8810ee6ac11ea4f82b86725789496bf8b3d9a2 validation/order-deferred-flip-pooc-cross-bar-01/engine_trades.csv 12c029df1fbe1b2b071e6ee2b9f43039d0359ae02bf2ad0ed3260bb3adadda1e validation/order-dual-four-bar-stop-no-close-01/engine_trades.csv 560e75eadf284e4374e77479fa13edf0629cfad1bfa5d76f1cfb5dbe619eb8c4 validation/order-dual-side-same-id-stop-no-cancel-01/engine_trades.csv a03be8fcb680efdaaf09d4e38f7c7dc9c2f5a74948feb9db6cc57c7c9188a5db validation/order-dual-stop-both-touch-priority-01/engine_trades.csv diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index aad0a416..b7b5a82b 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -16510,13 +16510,22 @@ void PineExecutionAdapter::flush_pooc_marketable_limit_entry_fills( // at bar_fill_price(bar.close) with no slippage; otherwise it rests and // gets the ordinary touch evaluation from the next bar on. Scoped to the // ordinary (non-COOF, non-stream) route and a flat book. - if (config_.calc_on_order_fills || !config_.process_orders_on_close - || stream_mode_ || coof_recalc_active_) { - return; - } + // A pure STOP entry the close already reached fills there too, with or + // without calc_on_order_fills (R5 lane PAR-ORDERS): TradingView books it + // at that bar's close, the close's tick slipped like any stop fill, on + // every path -- the five such entries of each of H-MEASURE's + // hm-{chart,mag}-diff-v1 / -v3 / -v5 / -v7 tapes, which the resting stop + // filled a bar late at the next open. The stop is reached when the close's + // tick is at or through it (a raw close half a tick short prints it). Only + // a lone marketable entry: when a second entry of this bar is marketable at + // the same close, which fills first is unmeasured, and the stops keep the + // next open's both-marketable arbitration (lane L9b's P0-B shapes). + if (!config_.process_orders_on_close || stream_mode_ || coof_recalc_active_) return; if (detail::run_position(require_host()).signed_units != 0.0) return; const double raw_close = bar.close; if (!finite_positive(raw_close)) return; + const double tick = staged_.syminfo.mintick; + const double close_tick = source_bar_fill_tick(raw_close, tick); std::vector> marketable; for (const auto& handle : live_handles_) { const auto found = placement_.find(handle.incarnation); @@ -16527,14 +16536,28 @@ void PineExecutionAdapter::flush_pooc_marketable_limit_entry_fills( if (row.projection_created_bar != projection_bar_index(context)) continue; if (row.projection_position_side != static_cast(PositionSide::FLAT)) continue; - if (!finite_positive(row.exit_levels.limit) - || std::isfinite(row.exit_levels.stop)) continue; + const bool pure_limit = !config_.calc_on_order_fills + && finite_positive(row.exit_levels.limit) + && !std::isfinite(row.exit_levels.stop); + const bool pure_stop = finite_positive(row.exit_levels.stop) + && !std::isfinite(row.exit_levels.limit); + if (!pure_limit && !pure_stop) continue; if (row.direction_gate || row.terms_priced_reverse || row.paired_flat_market_candidate) continue; - const bool at_close = row.is_long ? raw_close <= row.exit_levels.limit - : raw_close >= row.exit_levels.limit; + const bool at_close = pure_limit + ? (row.is_long ? raw_close <= row.exit_levels.limit + : raw_close >= row.exit_levels.limit) + : (finite_positive(close_tick) + && (row.is_long ? close_tick >= row.exit_levels.stop + : close_tick <= row.exit_levels.stop)); if (at_close) marketable.emplace_back(handle, row); } + if (marketable.size() > 1) { + marketable.erase(std::remove_if(marketable.begin(), marketable.end(), + [](const std::pair& candidate) { + return finite_positive(candidate.second.exit_levels.stop); + }), marketable.end()); + } for (auto& candidate : marketable) { // An earlier fill of this pass leaves the book non-flat; the owner's // later rows are then ordinary pending orders again. @@ -16559,9 +16582,13 @@ void PineExecutionAdapter::flush_pooc_marketable_limit_entry_fills( immediate.cancellation = {}; immediate.market_admission = {}; // bar_fill_price(bar.close): the raw close, nearest-tick rounded - // (pine_fills.cpp:8038-8041); a limit fill is never slipped. - immediate.forced_execution_price = source_bar_fill_tick( - raw_close, staged_.syminfo.mintick); + // (pine_fills.cpp:8038-8041); a limit fill is never slipped, a stop + // fill is, after the rounding. + const bool stop_entry = finite_positive(row.exit_levels.stop); + immediate.forced_execution_price = stop_entry && config_.slippage != 0 + ? nearest_tick(close_tick + (row.is_long ? 1.0 : -1.0) + * config_.slippage * tick, tick) + : close_tick; const auto accepted = submit_or_replace( std::move(request), std::move(immediate), true, row.source_id); if (accepted) { @@ -16761,11 +16788,17 @@ void PineExecutionAdapter::on_bar_close( const bool bound = host_ != nullptr; // Quiet: the close fills are a process_orders_on_close pass outside fill // recalculation and streams, and fill only what this bar placed -- an - // opening entry, or a limit or stop exit leg. + // opening entry, or a limit or stop exit leg. "This bar" is the index + // both passes test, projection_bar_index (the named input slot): on an + // aggregated or magnified run it is not the script-bar interval_index, + // and a gate reading that one skipped both passes there (R5 lane + // PAR-ORDERS). const bool close_fill_pass = config_.process_orders_on_close && !config_.calc_on_order_fills && !stream_mode_ && !coof_recalc_active_; - const int bar_index = context.coordinate.interval_index; - if (!skip_quiet(QuietHook::PoocLimitEntryFills, bound && (!close_fill_pass + const bool entry_close_fill_pass = config_.process_orders_on_close + && !stream_mode_ && !coof_recalc_active_; + const int bar_index = projection_bar_index(context); + if (!skip_quiet(QuietHook::PoocLimitEntryFills, bound && (!entry_close_fill_pass || !any_live_row(live_handles_, placement_, [&](const PlacementSnapshot& row) { return row.family == PineOrderFamily::Entry && row.opening && row.projection_created_bar == bar_index; diff --git a/tests/fixtures/magnified_aggregated/README.md b/tests/fixtures/magnified_aggregated/README.md index be820c8c..69dd6b3b 100644 --- a/tests/fixtures/magnified_aggregated/README.md +++ b/tests/fixtures/magnified_aggregated/README.md @@ -14,11 +14,17 @@ window 2025-07-01 .. 2025-07-08), byte-identical. | probe | variant | trades | tvTradesSha256 | note | |---|---|---|---|---| | `hm-mag-diff-v0` | plain, magnifier on | 12 | `53c28aaf8caa6ed1708a9aa42f2965db37724c87d95691fb16bbb56dbb69906e` | `tv-tape-hm-mag-diff-v0-53c28aaf` | +| `hm-mag-diff-v1` | process_orders_on_close, magnifier on | 12 | `929f16389c8bcd20b60776757c0ceb2482b61ef82a53314ca2218649b14c7069` | `tv-tape-hm-mag-diff-v1-929f1638` | | `hm-mag-diff-v2` | calc_on_order_fills, magnifier on | 12 | `444e7bc4eaac5b16e224007225bc55ed1c36fecd4453b8eab95e6c1ea70c6656` | `tv-tape-hm-mag-diff-v2-444e7bc4` | | `hm-mag-diff-v4` | slippage 2, pyramiding 3, magnifier on | 14 | `d18ca4ad316311a4aad91503800402f1873c927c3cadc575b3b1a1ab669d196e` | `tv-tape-hm-mag-diff-v4-d18ca4ad` | +| `hm-mag-diff-v5` | process_orders_on_close, slippage 2, pyramiding 3, magnifier on | 14 | `d56124ea28d8747d3c4ce6b965bcea59f0edf42e43ffefa1484f5b36ee0eb20e` | `tv-tape-hm-mag-diff-v5-d56124ea` | | `hm-mag-diff-v3` | process_orders_on_close, calc_on_order_fills, magnifier on | 12 | `23a25293ac35ce433214a7719e26cc58cf90471e5848bb7610e7485f1b3d079b` | `tv-tape-hm-mag-diff-v3-23a25293` | | `hm-chart-diff-v0` | plain, magnifier off | 12 | `b2652feb01b83236315a92112acb5b108626d11d959ed4b17effc6087454ae2c` | `tv-tape-hm-chart-diff-v0-b2652feb` | +| `hm-chart-diff-v1` | process_orders_on_close, magnifier off | 12 | `27174a6b560b84c0c8f7ce541b66a6e0db648d4c1880d684059b6b050087d12c` | `tv-tape-hm-chart-diff-v1-27174a6b` | | `hm-chart-diff-v4` | slippage 2, pyramiding 3, magnifier off | 14 | `5293cc0c2bc88dabba65ff73a024d88b28292845c537f0f4ccf58922e4d502a1` | `tv-tape-hm-chart-diff-v4-5293cc0c` | +| `hm-chart-diff-v5` | process_orders_on_close, slippage 2, pyramiding 3, magnifier off | 14 | `0080877d820ae0df81ec7eedab24b7d3a20df9f819f1e6101ff51b6049a6b44d` | `tv-tape-hm-chart-diff-v5-0080877d` | +| `hm-chart-diff-v3` | process_orders_on_close, calc_on_order_fills, magnifier off | 12 | `4d45cbcd76d2949a706469307ecba90e5e5b878d84033ac8b704ec906152cdae` | `tv-tape-hm-chart-diff-v3-4d45cbcd` | +| `hm-chart-diff-v7` | process_orders_on_close, calc_on_order_fills, slippage 2, pyramiding 3, magnifier off | 14 | `f5a88e1e51e749a95eef40b094fe44b4637f4426e2b43796a9f7c5242457c581` | `tv-tape-hm-chart-diff-v7-f5a88e1e` | TradingView dates every magnified fill at its chart bar's open; the rows are compared at chart-bar granularity. Excursions are not compared. @@ -26,3 +32,7 @@ compared at chart-bar granularity. Excursions are not compared. `hm-mag-diff-v3` (added by R5 lane PAR-ORDERS from H-MEASURE's evidence, `exec/H-MEASURE-scratch/agg/tapes`) witnesses the pyramiding cap: 12 rows and a flat book, where the engine once opened 85 lots on one bar with pyramiding=1. +`hm-{mag,chart}-diff-v1` and `-v5`, `hm-chart-diff-v3` and `-v7` (the same lane, +the same source) witness the process_orders_on_close stop entry that its placing +close already reached: TradingView fills it at that close on every path, with or +without calc_on_order_fills. diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v1/meta.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v1/meta.json new file mode 100644 index 00000000..cee56be7 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v1/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM CHART diff v1 pooc1 coof0 slip0 pyr1", + "slug": "hm-chart-diff-v1", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:48:46.525Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "4e5f1548c3331e5bd332661468946204c5c0ef938df032576ef52e7045a81cea", + "tv_trades_sha256": "27174a6b560b84c0c8f7ce541b66a6e0db648d4c1880d684059b6b050087d12c", + "tv_trades_rows": 24 +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v1/metrics.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v1/metrics.json new file mode 100644 index 00000000..c80d8472 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v1/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-chart-diff-v1", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 12, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "4e5f1548c3331e5bd332661468946204c5c0ef938df032576ef52e7045a81cea", + "tvTradesCsvHash": "27174a6b560b84c0c8f7ce541b66a6e0db648d4c1880d684059b6b050087d12c", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74396", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "85a45cfc7c4a54ec06f1073aec2b9f4949651700353484b1ef895bf603f1c327", + "decodedReportSha256": "7e06a59a52a6fc36108d54466bb2e3eed4a26c1b4ce675ea6fd21e4d8d2eb8f9", + "closedTrades": 12, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:48:46.524Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v1/strategy.pine b/tests/fixtures/magnified_aggregated/hm-chart-diff-v1/strategy.pine new file mode 100644 index 00000000..c9461859 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v1/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 1: +// process_orders_on_close=1, calc_on_order_fills=0, slippage=0, pyramiding=1, +// with the bar magnifier OFF (the chart-path control). NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM CHART diff v1 pooc1 coof0 slip0 pyr1", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v1/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-chart-diff-v1/tv_trades.csv new file mode 100644 index 00000000..864ed0dd --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v1/tv_trades.csv @@ -0,0 +1,25 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,3 +1,Entry long,2025-07-02 22:15,PL,11.47,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,3 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1158,7.7676,0.67,0.2324,7.8842,0.68,-0.1158,-0.01,13.5376,0.01,1 +2,Entry long,2025-07-02 23:00,ML,11.58,100,1158,7.7676,0.67,0.2324,7.8842,0.68,-0.1158,-0.01,13.5376,0.01,1 +3,Exit long,2025-07-03 01:30,Close position order,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-2.1166,-0.18,13.3044,0.01,6 +3,Entry long,2025-07-03 00:00,PL,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-2.1166,-0.18,13.3044,0.01,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1167,4.7661,0.41,0.2339,4.8833,0.42,0,0.00,18.0705,0.02,0 +4,Entry long,2025-07-03 02:15,PL,11.67,100,1167,4.7661,0.41,0.2339,4.8833,0.42,0,0.00,18.0705,0.02,0 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1171,7.765,0.66,0.235,7.8829,0.67,-0.1171,-0.01,25.8355,0.03,7 +5,Entry long,2025-07-03 02:15,ML,11.71,100,1171,7.765,0.66,0.235,7.8829,0.67,-0.1171,-0.01,25.8355,0.03,7 +6,Exit long,2025-07-03 23:15,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,23.5985,0.02,2 +6,Entry long,2025-07-03 22:45,PL,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,23.5985,0.02,2 +7,Exit short,2025-07-04 00:30,SX,11.8,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,31.3617,0.03,3 +7,Entry short,2025-07-03 23:45,PS,11.88,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,31.3617,0.03,3 +8,Exit long,2025-07-07 21:30,PX,11.62,100,1181,-19.2343,-1.63,0.2343,2.8819,0.24,-19.1181,-1.62,12.1274,0.01,2 +8,Entry long,2025-07-04 00:30,PL,11.81,100,1181,-19.2343,-1.63,0.2343,2.8819,0.24,-19.1181,-1.62,12.1274,0.01,2 +9,Exit short,2025-07-07 22:00,SX,11.75,100,1170,-5.2345,-0.45,0.2345,0,0.00,-5.117,-0.44,6.8929,0.01,1 +9,Entry short,2025-07-07 21:45,PS,11.7,100,1170,-5.2345,-0.45,0.2345,0,0.00,-5.117,-0.44,6.8929,0.01,1 +10,Exit long,2025-07-07 23:45,MX,11.58,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,-2.3396,0.00,4 +10,Entry long,2025-07-07 22:45,ML,11.67,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,-2.3396,0.00,4 +11,Exit long,2025-07-08 02:15,PX,11.51,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-8.5704,-0.01,7 +11,Entry long,2025-07-08 00:30,PL,11.57,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-8.5704,-0.01,7 +12,Exit long,2025-07-08 02:45,PX,11.58,100,1154,3.7688,0.33,0.2312,3.8846,0.34,0,0.00,-4.8016,0.00,1 +12,Entry long,2025-07-08 02:30,PL,11.54,100,1154,3.7688,0.33,0.2312,3.8846,0.34,0,0.00,-4.8016,0.00,1 \ No newline at end of file diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v3/meta.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v3/meta.json new file mode 100644 index 00000000..a5fe97eb --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v3/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM CHART diff v3 pooc1 coof1 slip0 pyr1", + "slug": "hm-chart-diff-v3", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:48:55.455Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "78a85f1c2e9a388e9ac10cb84da45f131fd652b2f86b560a7544f3e69fe8ae72", + "tv_trades_sha256": "4d45cbcd76d2949a706469307ecba90e5e5b878d84033ac8b704ec906152cdae", + "tv_trades_rows": 24 +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v3/metrics.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v3/metrics.json new file mode 100644 index 00000000..a593a994 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v3/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-chart-diff-v3", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 12, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "78a85f1c2e9a388e9ac10cb84da45f131fd652b2f86b560a7544f3e69fe8ae72", + "tvTradesCsvHash": "4d45cbcd76d2949a706469307ecba90e5e5b878d84033ac8b704ec906152cdae", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74398", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "c8adac3d1255c04e58581555953613669352108816abc73c47b71bfaf5b394cb", + "decodedReportSha256": "6ca0b0dca4a60b737b0fb787994eff9e2c703b136572af595398cc88a23f5785", + "closedTrades": 12, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:48:55.455Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v3/strategy.pine b/tests/fixtures/magnified_aggregated/hm-chart-diff-v3/strategy.pine new file mode 100644 index 00000000..d88c85d9 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v3/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 3: +// process_orders_on_close=1, calc_on_order_fills=1, slippage=0, pyramiding=1, +// with the bar magnifier OFF (the chart-path control). NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM CHART diff v3 pooc1 coof1 slip0 pyr1", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v3/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-chart-diff-v3/tv_trades.csv new file mode 100644 index 00000000..449957ad --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v3/tv_trades.csv @@ -0,0 +1,25 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,3 +1,Entry long,2025-07-02 22:15,PL,11.47,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,3 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1158,7.7676,0.67,0.2324,7.8842,0.68,-0.1158,-0.01,13.5376,0.01,1 +2,Entry long,2025-07-02 23:00,ML,11.58,100,1158,7.7676,0.67,0.2324,7.8842,0.68,-0.1158,-0.01,13.5376,0.01,1 +3,Exit long,2025-07-03 01:30,Close position order,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-2.1166,-0.18,13.3044,0.01,6 +3,Entry long,2025-07-03 00:00,PL,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-2.1166,-0.18,13.3044,0.01,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1167,4.7661,0.41,0.2339,4.8833,0.42,0,0.00,18.0705,0.02,0 +4,Entry long,2025-07-03 02:15,PL,11.67,100,1167,4.7661,0.41,0.2339,4.8833,0.42,0,0.00,18.0705,0.02,0 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1172,6.7649,0.58,0.2351,6.8828,0.59,-1.1172,-0.10,24.8354,0.02,7 +5,Entry long,2025-07-03 02:15,ML,11.72,100,1172,6.7649,0.58,0.2351,6.8828,0.59,-1.1172,-0.10,24.8354,0.02,7 +6,Exit long,2025-07-03 23:15,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,22.5984,0.02,2 +6,Entry long,2025-07-03 22:45,PL,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,22.5984,0.02,2 +7,Exit short,2025-07-04 00:30,SX,11.8,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,30.3616,0.03,3 +7,Entry short,2025-07-03 23:45,PS,11.88,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,30.3616,0.03,3 +8,Exit long,2025-07-07 21:30,PX,11.62,100,1180,-18.2342,-1.55,0.2342,3.882,0.33,-18.118,-1.54,12.1274,0.01,2 +8,Entry long,2025-07-04 00:30,PL,11.8,100,1180,-18.2342,-1.55,0.2342,3.882,0.33,-18.118,-1.54,12.1274,0.01,2 +9,Exit short,2025-07-07 22:00,SX,11.75,100,1170,-5.2345,-0.45,0.2345,4.883,0.42,-5.117,-0.44,6.8929,0.01,2 +9,Entry short,2025-07-07 21:30,PS,11.7,100,1170,-5.2345,-0.45,0.2345,4.883,0.42,-5.117,-0.44,6.8929,0.01,2 +10,Exit long,2025-07-07 23:45,MX,11.58,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,-2.3396,0.00,4 +10,Entry long,2025-07-07 22:45,ML,11.67,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,-2.3396,0.00,4 +11,Exit long,2025-07-08 02:15,PX,11.51,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-8.5704,-0.01,7 +11,Entry long,2025-07-08 00:30,PL,11.57,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-8.5704,-0.01,7 +12,Exit long,2025-07-08 02:45,PX,11.58,100,1154,3.7688,0.33,0.2312,3.8846,0.34,0,0.00,-4.8016,0.00,1 +12,Entry long,2025-07-08 02:30,PL,11.54,100,1154,3.7688,0.33,0.2312,3.8846,0.34,0,0.00,-4.8016,0.00,1 \ No newline at end of file diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v5/meta.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v5/meta.json new file mode 100644 index 00000000..d35d2f6a --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v5/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM CHART diff v5 pooc1 coof0 slip2 pyr3", + "slug": "hm-chart-diff-v5", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:49:32.820Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "342ccd8fb5fea1d3c48035a6ba47cfd30c507b5b2c5cf6c11dbf269214048e34", + "tv_trades_sha256": "0080877d820ae0df81ec7eedab24b7d3a20df9f819f1e6101ff51b6049a6b44d", + "tv_trades_rows": 28 +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v5/metrics.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v5/metrics.json new file mode 100644 index 00000000..0c8169fa --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v5/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-chart-diff-v5", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 14, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "342ccd8fb5fea1d3c48035a6ba47cfd30c507b5b2c5cf6c11dbf269214048e34", + "tvTradesCsvHash": "0080877d820ae0df81ec7eedab24b7d3a20df9f819f1e6101ff51b6049a6b44d", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74404", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "ec3ae8f4169bd5383adf7eb20bf398059b4dc9226973f42fcd7f5b6244b29c0a", + "decodedReportSha256": "7a562fd90c49d84a71942f115676493b7b9998dae1d5f27b8295dd38465500b5", + "closedTrades": 14, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:49:32.819Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v5/strategy.pine b/tests/fixtures/magnified_aggregated/hm-chart-diff-v5/strategy.pine new file mode 100644 index 00000000..6773b647 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v5/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 5: +// process_orders_on_close=1, calc_on_order_fills=0, slippage=2, pyramiding=3, +// with the bar magnifier OFF (the chart-path control). NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM CHART diff v5 pooc1 coof0 slip2 pyr3", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=3, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=2, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v5/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-chart-diff-v5/tv_trades.csv new file mode 100644 index 00000000..c28008d5 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v5/tv_trades.csv @@ -0,0 +1,29 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1149,3.7698,0.33,0.2302,3.8851,0.34,-5.1149,-0.45,3.7698,0.00,3 +1,Entry long,2025-07-02 22:15,PL,11.49,100,1149,3.7698,0.33,0.2302,3.8851,0.34,-5.1149,-0.45,3.7698,0.00,3 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1160,5.7674,0.50,0.2326,5.884,0.51,-2.116,-0.18,9.5372,0.01,1 +2,Entry long,2025-07-02 23:00,ML,11.6,100,1160,5.7674,0.50,0.2326,5.884,0.51,-2.116,-0.18,9.5372,0.01,1 +3,Exit long,2025-07-03 01:30,Close position order,11.64,100,1168,-4.2332,-0.36,0.2332,0,0.00,-4.1168,-0.35,5.304,0.01,6 +3,Entry long,2025-07-03 00:00,PL,11.68,100,1168,-4.2332,-0.36,0.2332,0,0.00,-4.1168,-0.35,5.304,0.01,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1169,2.7659,0.24,0.2341,2.8831,0.25,0,0.00,8.0699,0.01,0 +4,Entry long,2025-07-03 02:15,PL,11.69,100,1169,2.7659,0.24,0.2341,2.8831,0.25,0,0.00,8.0699,0.01,0 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1173,5.7648,0.49,0.2352,5.8827,0.50,-2.1173,-0.18,13.7172,0.01,7 +5,Entry long,2025-07-03 02:15,ML,11.73,100,1173,5.7648,0.49,0.2352,5.8827,0.50,-2.1173,-0.18,13.7172,0.01,7 +6,Exit long,2025-07-03 21:30,MX,11.79,100,1175,3.7646,0.32,0.2354,3.8825,0.33,-3.1175,-0.27,17.5993,0.02,6 +6,Entry long,2025-07-03 02:30,ML,11.75,100,1175,3.7646,0.32,0.2354,3.8825,0.33,-3.1175,-0.27,17.5993,0.02,6 +7,Exit long,2025-07-03 23:15,Close position order,11.82,100,1188,-6.237,-0.52,0.237,0,0.00,-8.1188,-0.68,11.2437,0.01,2 +7,Entry long,2025-07-03 22:45,PL,11.88,100,1188,-6.237,-0.52,0.237,0,0.00,-8.1188,-0.68,11.2437,0.01,2 +8,Exit long,2025-07-03 23:45,PS,11.88,100,1186,1.7626,0.15,0.2374,1.8814,0.16,-4.1186,-0.35,13.1249,0.01,2 +8,Entry long,2025-07-03 23:15,ML,11.86,100,1186,1.7626,0.15,0.2374,1.8814,0.16,-4.1186,-0.35,13.1249,0.01,2 +9,Exit long,2025-07-07 21:30,PX,11.6,100,1183,-23.2343,-1.96,0.2343,0.8817,0.07,-23.1183,-1.95,-10.1094,-0.01,2 +9,Entry long,2025-07-04 00:30,PL,11.83,100,1183,-23.2343,-1.96,0.2343,0.8817,0.07,-23.1183,-1.95,-10.1094,-0.01,2 +10,Exit short,2025-07-07 22:00,SX,11.77,100,1170,-7.2347,-0.62,0.2347,0,0.00,-7.117,-0.61,-17.3441,-0.02,1 +10,Entry short,2025-07-07 21:45,PS,11.7,100,1170,-7.2347,-0.62,0.2347,0,0.00,-7.117,-0.61,-17.3441,-0.02,1 +11,Exit long,2025-07-07 23:45,MX,11.56,100,1169,-13.2325,-1.13,0.2325,0,0.00,-13.1169,-1.12,-30.693,-0.03,4 +11,Entry long,2025-07-07 22:45,ML,11.69,100,1169,-13.2325,-1.13,0.2325,0,0.00,-13.1169,-1.12,-30.693,-0.03,4 +12,Exit long,2025-07-07 23:45,MX,11.56,100,1164,-8.232,-0.71,0.232,0,0.00,-8.1164,-0.70,-38.8086,-0.04,3 +12,Entry long,2025-07-07 23:00,ML,11.64,100,1164,-8.232,-0.71,0.232,0,0.00,-8.1164,-0.70,-38.8086,-0.04,3 +13,Exit long,2025-07-08 02:15,PX,11.49,100,1159,-10.2308,-0.88,0.2308,1.8841,0.16,-10.1159,-0.87,-49.0394,-0.05,7 +13,Entry long,2025-07-08 00:30,PL,11.59,100,1159,-10.2308,-0.88,0.2308,1.8841,0.16,-10.1159,-0.87,-49.0394,-0.05,7 +14,Exit long,2025-07-08 02:45,PX,11.58,100,1156,1.7686,0.15,0.2314,1.8844,0.16,-1.1156,-0.10,-47.2708,-0.05,1 +14,Entry long,2025-07-08 02:30,PL,11.56,100,1156,1.7686,0.15,0.2314,1.8844,0.16,-1.1156,-0.10,-47.2708,-0.05,1 \ No newline at end of file diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v7/meta.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v7/meta.json new file mode 100644 index 00000000..7501db13 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v7/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM CHART diff v7 pooc1 coof1 slip2 pyr3", + "slug": "hm-chart-diff-v7", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:49:41.613Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "e6d45b169566e5f21451f7158e7452ab60f8d58401d656c88bba680f49589e9b", + "tv_trades_sha256": "f5a88e1e51e749a95eef40b094fe44b4637f4426e2b43796a9f7c5242457c581", + "tv_trades_rows": 28 +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v7/metrics.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v7/metrics.json new file mode 100644 index 00000000..8bacff9d --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v7/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-chart-diff-v7", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 14, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "e6d45b169566e5f21451f7158e7452ab60f8d58401d656c88bba680f49589e9b", + "tvTradesCsvHash": "f5a88e1e51e749a95eef40b094fe44b4637f4426e2b43796a9f7c5242457c581", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74406", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "937b89e35458b455de597f7c6add1d2d84d3fb9af3fcbcbd37678e8f684acc19", + "decodedReportSha256": "d5b68eb9de5c217a9944cc223aa53aa7eb76f7109c7da001d809dce9917384f8", + "closedTrades": 14, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:49:41.607Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v7/strategy.pine b/tests/fixtures/magnified_aggregated/hm-chart-diff-v7/strategy.pine new file mode 100644 index 00000000..5133ceb6 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v7/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 7: +// process_orders_on_close=1, calc_on_order_fills=1, slippage=2, pyramiding=3, +// with the bar magnifier OFF (the chart-path control). NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM CHART diff v7 pooc1 coof1 slip2 pyr3", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=3, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=2, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v7/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-chart-diff-v7/tv_trades.csv new file mode 100644 index 00000000..820dc0b4 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v7/tv_trades.csv @@ -0,0 +1,29 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1149,3.7698,0.33,0.2302,3.8851,0.34,-5.1149,-0.45,3.7698,0.00,3 +1,Entry long,2025-07-02 22:15,PL,11.49,100,1149,3.7698,0.33,0.2302,3.8851,0.34,-5.1149,-0.45,3.7698,0.00,3 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1160,5.7674,0.50,0.2326,5.884,0.51,-2.116,-0.18,9.5372,0.01,1 +2,Entry long,2025-07-02 23:00,ML,11.6,100,1160,5.7674,0.50,0.2326,5.884,0.51,-2.116,-0.18,9.5372,0.01,1 +3,Exit long,2025-07-03 01:30,Close position order,11.64,100,1168,-4.2332,-0.36,0.2332,0,0.00,-4.1168,-0.35,5.304,0.01,6 +3,Entry long,2025-07-03 00:00,PL,11.68,100,1168,-4.2332,-0.36,0.2332,0,0.00,-4.1168,-0.35,5.304,0.01,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1169,2.7659,0.24,0.2341,2.8831,0.25,0,0.00,8.0699,0.01,0 +4,Entry long,2025-07-03 02:15,PL,11.69,100,1169,2.7659,0.24,0.2341,2.8831,0.25,0,0.00,8.0699,0.01,0 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1174,4.7647,0.41,0.2353,4.8826,0.42,-3.1174,-0.27,12.7171,0.01,7 +5,Entry long,2025-07-03 02:15,ML,11.74,100,1174,4.7647,0.41,0.2353,4.8826,0.42,-3.1174,-0.27,12.7171,0.01,7 +6,Exit long,2025-07-03 21:30,MX,11.79,100,1175,3.7646,0.32,0.2354,3.8825,0.33,-3.1175,-0.27,16.5992,0.02,6 +6,Entry long,2025-07-03 02:30,ML,11.75,100,1175,3.7646,0.32,0.2354,3.8825,0.33,-3.1175,-0.27,16.5992,0.02,6 +7,Exit long,2025-07-03 23:15,Close position order,11.82,100,1188,-6.237,-0.52,0.237,0,0.00,-8.1188,-0.68,10.2436,0.01,2 +7,Entry long,2025-07-03 22:45,PL,11.88,100,1188,-6.237,-0.52,0.237,0,0.00,-8.1188,-0.68,10.2436,0.01,2 +8,Exit long,2025-07-03 23:45,PS,11.88,100,1186,1.7626,0.15,0.2374,1.8814,0.16,-4.1186,-0.35,12.1248,0.01,2 +8,Entry long,2025-07-03 23:15,ML,11.86,100,1186,1.7626,0.15,0.2374,1.8814,0.16,-4.1186,-0.35,12.1248,0.01,2 +9,Exit long,2025-07-07 21:30,PX,11.6,100,1183,-23.2343,-1.96,0.2343,0.8817,0.07,-23.1183,-1.95,-11.1095,-0.01,2 +9,Entry long,2025-07-04 00:30,PL,11.83,100,1183,-23.2343,-1.96,0.2343,0.8817,0.07,-23.1183,-1.95,-11.1095,-0.01,2 +10,Exit short,2025-07-07 22:00,SX,11.77,100,1170,-7.2347,-0.62,0.2347,4.883,0.42,-7.117,-0.61,-18.3442,-0.02,2 +10,Entry short,2025-07-07 21:30,PS,11.7,100,1170,-7.2347,-0.62,0.2347,4.883,0.42,-7.117,-0.61,-18.3442,-0.02,2 +11,Exit long,2025-07-07 23:45,MX,11.56,100,1169,-13.2325,-1.13,0.2325,0,0.00,-13.1169,-1.12,-31.6931,-0.03,4 +11,Entry long,2025-07-07 22:45,ML,11.69,100,1169,-13.2325,-1.13,0.2325,0,0.00,-13.1169,-1.12,-31.6931,-0.03,4 +12,Exit long,2025-07-07 23:45,MX,11.56,100,1164,-8.232,-0.71,0.232,0,0.00,-8.1164,-0.70,-39.8087,-0.04,3 +12,Entry long,2025-07-07 23:00,ML,11.64,100,1164,-8.232,-0.71,0.232,0,0.00,-8.1164,-0.70,-39.8087,-0.04,3 +13,Exit long,2025-07-08 02:15,PX,11.49,100,1159,-10.2308,-0.88,0.2308,1.8841,0.16,-10.1159,-0.87,-50.0395,-0.05,7 +13,Entry long,2025-07-08 00:30,PL,11.59,100,1159,-10.2308,-0.88,0.2308,1.8841,0.16,-10.1159,-0.87,-50.0395,-0.05,7 +14,Exit long,2025-07-08 02:45,PX,11.58,100,1156,1.7686,0.15,0.2314,1.8844,0.16,-1.1156,-0.10,-48.2709,-0.05,1 +14,Entry long,2025-07-08 02:30,PL,11.56,100,1156,1.7686,0.15,0.2314,1.8844,0.16,-1.1156,-0.10,-48.2709,-0.05,1 \ No newline at end of file diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v1/meta.json b/tests/fixtures/magnified_aggregated/hm-mag-diff-v1/meta.json new file mode 100644 index 00000000..92177846 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v1/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM MAG diff v1 pooc1 coof0 slip0 pyr1", + "slug": "hm-mag-diff-v1", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:48:27.121Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "3069baba1814417a66ea8d0d1ccda919db8e7c827180c1ce91ab13273d81d57e", + "tv_trades_sha256": "929f16389c8bcd20b60776757c0ceb2482b61ef82a53314ca2218649b14c7069", + "tv_trades_rows": 24 +} diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v1/metrics.json b/tests/fixtures/magnified_aggregated/hm-mag-diff-v1/metrics.json new file mode 100644 index 00000000..0ab3d5e6 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v1/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-mag-diff-v1", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 12, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "3069baba1814417a66ea8d0d1ccda919db8e7c827180c1ce91ab13273d81d57e", + "tvTradesCsvHash": "929f16389c8bcd20b60776757c0ceb2482b61ef82a53314ca2218649b14c7069", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74392", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "b7e67eff90f1caa79394a1b5198065a612b57be5c1c857b24b9e060529aec76a", + "decodedReportSha256": "b3be9289a01780fc754055577fa435929586171ff965ffaafb6bacd7aa7ccae0", + "closedTrades": 12, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:48:27.120Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v1/strategy.pine b/tests/fixtures/magnified_aggregated/hm-mag-diff-v1/strategy.pine new file mode 100644 index 00000000..124b12de --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v1/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 1: +// process_orders_on_close=1, calc_on_order_fills=0, slippage=0, pyramiding=1, +// with the bar magnifier ON. NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM MAG diff v1 pooc1 coof0 slip0 pyr1", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=true) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v1/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-mag-diff-v1/tv_trades.csv new file mode 100644 index 00000000..fd8ef139 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v1/tv_trades.csv @@ -0,0 +1,25 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,3 +1,Entry long,2025-07-02 22:15,PL,11.47,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,3 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1158,7.7676,0.67,0.2324,7.8842,0.68,-1.1158,-0.10,13.5376,0.01,1 +2,Entry long,2025-07-02 23:00,ML,11.58,100,1158,7.7676,0.67,0.2324,7.8842,0.68,-1.1158,-0.10,13.5376,0.01,1 +3,Exit long,2025-07-03 01:30,Close position order,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-2.1166,-0.18,13.3044,0.01,6 +3,Entry long,2025-07-03 00:00,PL,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-2.1166,-0.18,13.3044,0.01,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1167,4.7661,0.41,0.2339,4.8833,0.42,0,0.00,18.0705,0.02,0 +4,Entry long,2025-07-03 02:15,PL,11.67,100,1167,4.7661,0.41,0.2339,4.8833,0.42,0,0.00,18.0705,0.02,0 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1171,7.765,0.66,0.235,7.8829,0.67,-0.1171,-0.01,25.8355,0.03,7 +5,Entry long,2025-07-03 02:15,ML,11.71,100,1171,7.765,0.66,0.235,7.8829,0.67,-0.1171,-0.01,25.8355,0.03,7 +6,Exit long,2025-07-03 23:15,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,23.5985,0.02,2 +6,Entry long,2025-07-03 22:45,PL,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,23.5985,0.02,2 +7,Exit short,2025-07-04 00:30,SX,11.8,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,31.3617,0.03,3 +7,Entry short,2025-07-03 23:45,PS,11.88,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,31.3617,0.03,3 +8,Exit long,2025-07-07 21:30,PX,11.62,100,1181,-19.2343,-1.63,0.2343,2.8819,0.24,-19.1181,-1.62,12.1274,0.01,2 +8,Entry long,2025-07-04 00:30,PL,11.81,100,1181,-19.2343,-1.63,0.2343,2.8819,0.24,-19.1181,-1.62,12.1274,0.01,2 +9,Exit short,2025-07-07 22:00,SX,11.75,100,1170,-5.2345,-0.45,0.2345,2.883,0.25,-5.117,-0.44,6.8929,0.01,1 +9,Entry short,2025-07-07 21:45,PS,11.7,100,1170,-5.2345,-0.45,0.2345,2.883,0.25,-5.117,-0.44,6.8929,0.01,1 +10,Exit long,2025-07-07 23:45,MX,11.58,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,-2.3396,0.00,4 +10,Entry long,2025-07-07 22:45,ML,11.67,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,-2.3396,0.00,4 +11,Exit long,2025-07-08 02:15,PX,11.51,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-8.5704,-0.01,7 +11,Entry long,2025-07-08 00:30,PL,11.57,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-8.5704,-0.01,7 +12,Exit long,2025-07-08 02:45,PX,11.58,100,1154,3.7688,0.33,0.2312,3.8846,0.34,-0.1154,-0.01,-4.8016,0.00,1 +12,Entry long,2025-07-08 02:30,PL,11.54,100,1154,3.7688,0.33,0.2312,3.8846,0.34,-0.1154,-0.01,-4.8016,0.00,1 \ No newline at end of file diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v5/meta.json b/tests/fixtures/magnified_aggregated/hm-mag-diff-v5/meta.json new file mode 100644 index 00000000..580b682d --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v5/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM MAG diff v5 pooc1 coof0 slip2 pyr3", + "slug": "hm-mag-diff-v5", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:49:11.981Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "81df7b931d4e497da72afc7cf8d1204e217474aa6a7b5e46f8881f0ac28d0c43", + "tv_trades_sha256": "d56124ea28d8747d3c4ce6b965bcea59f0edf42e43ffefa1484f5b36ee0eb20e", + "tv_trades_rows": 28 +} diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v5/metrics.json b/tests/fixtures/magnified_aggregated/hm-mag-diff-v5/metrics.json new file mode 100644 index 00000000..6cecea47 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v5/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-mag-diff-v5", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 14, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "81df7b931d4e497da72afc7cf8d1204e217474aa6a7b5e46f8881f0ac28d0c43", + "tvTradesCsvHash": "d56124ea28d8747d3c4ce6b965bcea59f0edf42e43ffefa1484f5b36ee0eb20e", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74400", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "9ee86551d87f429712d27f88731a52272a68fa931bd774ceeb825df8578f5e85", + "decodedReportSha256": "c21cdb34f4a95036721230553b0fca88c4d57b2b6ecc011362d661f37a0e05db", + "closedTrades": 14, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:49:11.980Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v5/strategy.pine b/tests/fixtures/magnified_aggregated/hm-mag-diff-v5/strategy.pine new file mode 100644 index 00000000..db675655 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v5/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 5: +// process_orders_on_close=1, calc_on_order_fills=0, slippage=2, pyramiding=3, +// with the bar magnifier ON. NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM MAG diff v5 pooc1 coof0 slip2 pyr3", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=3, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=2, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=true) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-mag-diff-v5/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-mag-diff-v5/tv_trades.csv new file mode 100644 index 00000000..7a0be95a --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-mag-diff-v5/tv_trades.csv @@ -0,0 +1,29 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1149,3.7698,0.33,0.2302,3.8851,0.34,-5.1149,-0.45,3.7698,0.00,3 +1,Entry long,2025-07-02 22:15,PL,11.49,100,1149,3.7698,0.33,0.2302,3.8851,0.34,-5.1149,-0.45,3.7698,0.00,3 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1160,5.7674,0.50,0.2326,5.884,0.51,-3.116,-0.27,9.5372,0.01,1 +2,Entry long,2025-07-02 23:00,ML,11.6,100,1160,5.7674,0.50,0.2326,5.884,0.51,-3.116,-0.27,9.5372,0.01,1 +3,Exit long,2025-07-03 01:30,Close position order,11.64,100,1168,-4.2332,-0.36,0.2332,0,0.00,-4.1168,-0.35,5.304,0.01,6 +3,Entry long,2025-07-03 00:00,PL,11.68,100,1168,-4.2332,-0.36,0.2332,0,0.00,-4.1168,-0.35,5.304,0.01,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1169,2.7659,0.24,0.2341,2.8831,0.25,-1.1169,-0.10,8.0699,0.01,0 +4,Entry long,2025-07-03 02:15,PL,11.69,100,1169,2.7659,0.24,0.2341,2.8831,0.25,-1.1169,-0.10,8.0699,0.01,0 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1173,5.7648,0.49,0.2352,5.8827,0.50,-2.1173,-0.18,13.7172,0.01,7 +5,Entry long,2025-07-03 02:15,ML,11.73,100,1173,5.7648,0.49,0.2352,5.8827,0.50,-2.1173,-0.18,13.7172,0.01,7 +6,Exit long,2025-07-03 21:30,MX,11.79,100,1175,3.7646,0.32,0.2354,3.8825,0.33,-3.1175,-0.27,17.5993,0.02,6 +6,Entry long,2025-07-03 02:30,ML,11.75,100,1175,3.7646,0.32,0.2354,3.8825,0.33,-3.1175,-0.27,17.5993,0.02,6 +7,Exit long,2025-07-03 23:15,Close position order,11.82,100,1188,-6.237,-0.52,0.237,0,0.00,-8.1188,-0.68,11.2437,0.01,2 +7,Entry long,2025-07-03 22:45,PL,11.88,100,1188,-6.237,-0.52,0.237,0,0.00,-8.1188,-0.68,11.2437,0.01,2 +8,Exit long,2025-07-03 23:45,PS,11.88,100,1186,1.7626,0.15,0.2374,1.8814,0.16,-4.1186,-0.35,13.1249,0.01,2 +8,Entry long,2025-07-03 23:15,ML,11.86,100,1186,1.7626,0.15,0.2374,1.8814,0.16,-4.1186,-0.35,13.1249,0.01,2 +9,Exit long,2025-07-07 21:30,PX,11.6,100,1183,-23.2343,-1.96,0.2343,0.8817,0.07,-23.1183,-1.95,-10.1094,-0.01,2 +9,Entry long,2025-07-04 00:30,PL,11.83,100,1183,-23.2343,-1.96,0.2343,0.8817,0.07,-23.1183,-1.95,-10.1094,-0.01,2 +10,Exit short,2025-07-07 22:00,SX,11.77,100,1170,-7.2347,-0.62,0.2347,2.883,0.25,-7.117,-0.61,-17.3441,-0.02,1 +10,Entry short,2025-07-07 21:45,PS,11.7,100,1170,-7.2347,-0.62,0.2347,2.883,0.25,-7.117,-0.61,-17.3441,-0.02,1 +11,Exit long,2025-07-07 23:45,MX,11.56,100,1169,-13.2325,-1.13,0.2325,0,0.00,-13.1169,-1.12,-30.693,-0.03,4 +11,Entry long,2025-07-07 22:45,ML,11.69,100,1169,-13.2325,-1.13,0.2325,0,0.00,-13.1169,-1.12,-30.693,-0.03,4 +12,Exit long,2025-07-07 23:45,MX,11.56,100,1164,-8.232,-0.71,0.232,0,0.00,-8.1164,-0.70,-38.8086,-0.04,3 +12,Entry long,2025-07-07 23:00,ML,11.64,100,1164,-8.232,-0.71,0.232,0,0.00,-8.1164,-0.70,-38.8086,-0.04,3 +13,Exit long,2025-07-08 02:15,PX,11.49,100,1159,-10.2308,-0.88,0.2308,1.8841,0.16,-10.1159,-0.87,-49.0394,-0.05,7 +13,Entry long,2025-07-08 00:30,PL,11.59,100,1159,-10.2308,-0.88,0.2308,1.8841,0.16,-10.1159,-0.87,-49.0394,-0.05,7 +14,Exit long,2025-07-08 02:45,PX,11.58,100,1156,1.7686,0.15,0.2314,1.8844,0.16,-2.1156,-0.18,-47.2708,-0.05,1 +14,Entry long,2025-07-08 02:30,PL,11.56,100,1156,1.7686,0.15,0.2314,1.8844,0.16,-2.1156,-0.18,-47.2708,-0.05,1 \ No newline at end of file diff --git a/tests/test_l10am_entry_bar_priced_exit.cpp b/tests/test_l10am_entry_bar_priced_exit.cpp index f633558a..86793b4a 100644 --- a/tests/test_l10am_entry_bar_priced_exit.cpp +++ b/tests/test_l10am_entry_bar_priced_exit.cpp @@ -107,7 +107,11 @@ void test_traderhayz_entry_bar_stop() { // Trade #18: // On bar 0 (2025-04-16 16:45 UTC), strategy places stop buy entry @ 3327.38 // and exit limit @ 3337.46, stop @ 3320.00. -// On bar 1 (2025-04-16 17:00 UTC), buy stop fills @ 3327.38. +// The buy stop fills @ 3327.38 at bar 0's close: under process_orders_on_close +// the placing close (3327.38) already reaches it. TradingView's own trade 18 +// enters at 2025-04-16 16:45 UTC, Duration 6 bars; the owner (ab9714be) filled +// it a bar later, on bar 1 (2025-04-16 17:00 UTC), and so did this engine until +// R5 lane PAR-ORDERS, which moved the pin to TradingView's bar. // On bar 6 (2025-04-16 18:15 UTC): O 3332.235, H 3338.895, L 3331.735, C 3338.375. // Exit long limit fills @ 3337.46. // Owner literals: entry 3327.38, exit 3337.46, qty 1.0, pnl 10.08. @@ -144,9 +148,9 @@ class VasudevshenoyHost : public source::PineStrategyHost { std::vector vasudevshenoy_bars() { return { - // 0: 2025-04-16 16:45 UTC (signal bar) + // 0: 2025-04-16 16:45 UTC (signal bar; the stop fills at its close) mk(1744821900000LL, 3322.59, 3327.38, 3322.34, 3327.38), - // 1: 2025-04-16 17:00 UTC (entry bar, stop fills at 3327.38) + // 1: 2025-04-16 17:00 UTC (the owner's entry bar) mk(1744822800000LL, 3327.355, 3328.865, 3325.475, 3326.87), // 2: 2025-04-16 17:15 UTC mk(1744823700000LL, 3326.855, 3330.53, 3326.675, 3330.105), @@ -171,7 +175,7 @@ void test_vasudevshenoy_priced_exit() { if (host.trade_count() == 1) { const auto& t = host.get_trade(0); CHECK(t.is_long); - CHECK(t.entry_time == 1744822800000LL); + CHECK(t.entry_time == 1744821900000LL); CHECK(near(t.entry_price, 3327.38)); CHECK(t.exit_time == 1744827300000LL); CHECK(near(t.exit_price, 3337.46)); diff --git a/tests/test_magnified_aggregated_tape.cpp b/tests/test_magnified_aggregated_tape.cpp index ce7fcec7..e8d7a09e 100644 --- a/tests/test_magnified_aggregated_tape.cpp +++ b/tests/test_magnified_aggregated_tape.cpp @@ -23,8 +23,32 @@ * since K-IDX the kernel books lots and rows in script-bar space. * * Variants kept here are the ones the magnified run books exactly as TradingView: - * v0 (plain), v2 (calc_on_order_fills), v4 (slippage 2, pyramiding 3); controls: - * the chart and plain-aggregated paths against hm-chart-diff-v0 / v4. + * v0 (plain), v1 (process_orders_on_close), v2 (calc_on_order_fills), v4 + * (slippage 2, pyramiding 3), v5 (process_orders_on_close, slippage 2, + * pyramiding 3); controls: the chart and plain-aggregated paths against + * hm-chart-diff-v0 / v1 / v4 / v5. + * + * v1 and v5 (R5 lane PAR-ORDERS): under process_orders_on_close a stop entry + * whose stop the placing close already reached fills at that close, the + * close's tick slipped like any stop fill -- five entries per tape, which the + * engine used to fill a bar late at the next open, on every path. The + * aggregated and magnified paths also needed the close pass itself: its + * quiet-bar gate read the script-bar index where the pass reads the input + * slot, and skipped it on every such run. The same stop entries under + * calc_on_order_fills fill there too: v3 (all three paths) and v7 (chart and + * aggregated) book them at TradingView's bar and price. Their other rows are + * RECORDED divergences, each asserted to differ, so a fix flips them + * deliberately: + * - v3 / v7 row 5 (chart, aggregated): the calc_on_order_fills market entry + * the fill at bar 19's high re-issues books the close (11.71 / 11.73) + * where TradingView books that high (11.72 / 11.74); + * - v7 rows 8 and 9 (chart, aggregated): after bar 33's close_all and + * same-bar add TradingView keeps the add (ML 11.86) and fills bar 38's + * stop entry at 11.83, the engine drops the add and books 11.82; + * - v3 row 2 (magnified): a fill recalculation's market entry books 11.58 + * where TradingView books 11.54. + * v7's magnified run is not replayed: TradingView books two more ML rows at + * bar 59 there (16 against 14). * * The pyramiding cap (R5 lane PAR-ORDERS): no path of any variant ever holds * more lots than its pyramiding setting at a script call. Under the magnifier, @@ -42,6 +66,7 @@ #include #include #include +#include #include #include #include @@ -259,7 +284,7 @@ void run_path(MagDiffHost& host, Path path) { } } -void replay(const char* slug, const Variant& v, Path path) { +void replay(const char* slug, const Variant& v, Path path, unsigned recorded = 0u) { std::printf("-- %s [%s, %s]\n", slug, v.tag, path_name(path)); std::vector tape; for (const TapeTrade& t : read_tape(slug)) @@ -287,7 +312,9 @@ void replay(const char* slug, const Variant& v, Path path) { && r.entry_bar == chart_bar_of(tv.entry_ms) && r.exit_bar == chart_bar_of(tv.exit_ms) && r.exit_bar - r.entry_bar == tv.duration_bars; - CHECK(ok); + const bool divergence = (recorded & (1u << (i + 1))) != 0; + CHECK(ok != divergence); + if (divergence) std::printf(" row %zu: RECORDED divergence\n", i + 1); if (!ok) { std::printf(" row %zu: TradingView %s %s bar %d @%.2f -> bar %d @%.2f %s dur %d pnl %.4f\n" " engine %s %s bar %d @%.2f -> bar %d @%.2f %s dur %d pnl %.4f" @@ -335,23 +362,36 @@ void pyramiding_cap_holds(const Variant (&variants)[8]) { int main() { const Variant v0{"v0 plain", false, false, 0, 1}; + const Variant v1{"v1 process_orders_on_close", true, false, 0, 1}; const Variant v2{"v2 calc_on_order_fills", false, true, 0, 1}; const Variant v4{"v4 slippage 2, pyramiding 3", false, false, 2, 3}; + const Variant v3{"v3 process_orders_on_close, calc_on_order_fills", true, true, 0, 1}; + const Variant v5{"v5 process_orders_on_close, slippage 2, pyramiding 3", true, false, 2, 3}; + const Variant v7{"v7 process_orders_on_close, calc_on_order_fills, slippage 2, pyramiding 3", + true, true, 2, 3}; const Variant all[8] = { - v0, {"v1 process_orders_on_close", true, false, 0, 1}, v2, - {"v3 process_orders_on_close, calc_on_order_fills", true, true, 0, 1}, v4, - {"v5 process_orders_on_close, slippage 2, pyramiding 3", true, false, 2, 3}, - {"v6 calc_on_order_fills, slippage 2, pyramiding 3", false, true, 2, 3}, - {"v7 process_orders_on_close, calc_on_order_fills, slippage 2, pyramiding 3", - true, true, 2, 3}}; + v0, v1, v2, v3, v4, v5, + {"v6 calc_on_order_fills, slippage 2, pyramiding 3", false, true, 2, 3}, v7}; + const auto rows = [](std::initializer_list list) { + unsigned mask = 0; + for (const int row : list) mask |= 1u << row; + return mask; + }; // 1. the magnified run against TradingView's magnifier tapes replay("hm-mag-diff-v0", v0, Path::Magnified); + replay("hm-mag-diff-v1", v1, Path::Magnified); replay("hm-mag-diff-v2", v2, Path::Magnified); replay("hm-mag-diff-v4", v4, Path::Magnified); + replay("hm-mag-diff-v5", v5, Path::Magnified); + replay("hm-mag-diff-v3", v3, Path::Magnified, rows({2})); // 2. controls: the chart and plain-aggregated paths against the magnifier-off tapes for (const Path path : {Path::Chart, Path::Aggregated}) { replay("hm-chart-diff-v0", v0, path); + replay("hm-chart-diff-v1", v1, path); replay("hm-chart-diff-v4", v4, path); + replay("hm-chart-diff-v5", v5, path); + replay("hm-chart-diff-v3", v3, path, rows({5})); + replay("hm-chart-diff-v7", v7, path, rows({5, 8, 9})); } // 3. the pyramiding cap pyramiding_cap_holds(all); From 16938b50441b839fbbaa18a929b45b5b3c4848c5 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 07:21:02 +0800 Subject: [PATCH 30/83] Host: an aggregated magnified run dates every fill at its chart bar's open, as TradingView does (R5 lane PAR-ORDERS, H-MEASURE Finding 6a) TradingView dates every fill of a chart bar at the bar's open, a magnified one too: 218 of 218 fills on H-MEASURE's sixteen lab tv tapes (hm-{mag,chart}-diff-v0..v7). The engine dated a fill of an aggregated magnified run (1m input, 15m chart, use_bar_magnifier) at its sub-bar's instant -- 71 of the 152 instants the magnified replays of the tape row book sat inside their chart bar (15:01, 15:25, 18:16, ...). Root cause. PineStrategyHost::on_native_applied (the AG2 re-dating, lane F1 / E27) re-dates a lot's time and a row's exit time to context.script_bar_open_ms for every modeled point of a plain aggregated chart, but under the magnifier for the close points alone ("its sub-bar fills keep their sub-bar's instant, as ab9714be dated them"). A sub-bar's open, path and ModeledOHLCClose point were left at the kernel's instant. Fix, host presentation only (the kernel's own instant, which its FX lookup, decision floor and ordering read, is untouched; ADR-0001 rule 2: TradingView's dating is presentation policy on the Pine host): on an aggregated run every modeled point -- ModeledOHLCClose included, which only a magnified run presents -- is dated at the chart bar's open, magnifier or not. A chart-timeframe run (magnified or not) already dates every point at its bar's label (0 of 1,386 magnified quiet-bar instants off a bar boundary) and does not change; a realtime print and a stream's quiet carried interval keep their own instant as before. Rows flipped in this commit: - test_magnified_aggregated_tape asserts every row's instants exactly on every path (it compared the bar each instant opens). Against the previous commit's library: FAIL 373 checks, 53 failures; after, 0. - Three rows pinned ab9714be's sub-bar instants on aggregated magnified runs; only instants move, every price, quantity, P&L and book digest stays: - test_adapter_brackets_relower: the nine Mag* shapes re-harvested with the file's own -DPINEFORGE_R4D_HARVEST build on this tree; 20 instants of 16 rows move onto their 2-minute bar's open. - test_adapter_coof_relower: the refill's l5 lot 960000 -> 900000 (its b0cec54 cadence text notes it); the waypoint-deferral check now proves the deferral by the lots' prices (100, 100, 99, 101, 100.25, 100.1 against the birth rule's 100.0 .. 100.5), since every lot is dated at the bar's open. - test_native_calc_timing_twin: the adapter's sixth lot 960000 -> 900000 (the native route keeps the kernel's instants). The corpus runs no aggregated magnified probe, so nothing there can move. One v19 hash witness moves with the dating -- test_adapter_continuation_view's aggregated_magnifier reads -- re-pinned in the lane's named hash commit; the quiet-bar, publication and live-state batteries run no aggregated magnified configuration and do not move. Docs: the design's AG2 row; digits re-anchored. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 1fc67988cb7df98c9b4978379cfdc5bcfb881177) INT26 pick note: the host and test hunks applied without conflict; the ADR and design pages merged on citation digits only. --- docs/adr/0001-kernel-adapter-boundary.md | 12 ++++---- docs/design/native-feature-parity.md | 26 ++++++++--------- src/source/pine_strategy_host.cpp | 25 ++++++++-------- tests/test_adapter_brackets_relower.cpp | 37 ++++++++++++++---------- tests/test_adapter_coof_relower.cpp | 29 ++++++++++--------- tests/test_magnified_aggregated_tape.cpp | 15 +++++----- tests/test_native_calc_timing_twin.cpp | 8 +++-- 7 files changed, 83 insertions(+), 69 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index f99326a9..2bb70d1e 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -207,11 +207,11 @@ into kernel driving — never by special-casing the kernel *for a specific scrip kernel and own these quirks, each at its site: - **Seams the adapter overrides.** The kernel is driven, not patched. `PineStrategyHost` overrides - `resolve_execution_terms` (`pine_strategy_host.cpp:676`) for TV sizing and fill spelling; - `validate_execution_precommit` (`pine_strategy_host.cpp:701`), whose + `resolve_execution_terms` (`pine_strategy_host.cpp:677`) for TV sizing and fill spelling; + `validate_execution_precommit` (`pine_strategy_host.cpp:702`), whose `validate_precommit` (`pine_adapter.cpp:13076`) returns `AdmitWithHostMargin` (`pine_adapter.cpp:13321`) to own the opening margin decision; `owns_lot_excursions() = true` - (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:786`), so + (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:787`), so the Pine host keeps its own per-lot MFE/MAE, `ab9714be`'s model (RULING A48). Measured against TradingView by R5 lane H-MEASURE (`tests/test_e19_excursion_tape.cpp`, `lab tv` tapes): where the host's model and the kernel sampler differ, the kernel's number is TradingView's — the host @@ -289,7 +289,7 @@ kernel and own these quirks, each at its site: position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the three session flags, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` `pine_strategy_host.hpp:857`), which `scheduler_update_session_state` - (`pine_strategy_host.cpp:1764`) selects from the kernel's session-day facts before each source + (`pine_strategy_host.cpp:1765`) selects from the kernel's session-day facts before each source callback — the host computes no session-day rule of its own. Generated code reads two of them; a generated `session.ismarket` still calls the time-of-day predicate (`pine_session_ismarket` `pine_strategy_host.hpp:606`), which on six `lab tv` tapes misses every @@ -347,7 +347,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files lot flags: `skip_entry_bar_high` / `skip_entry_bar_low` (`engine.hpp:153`), hashed (`skip_entry_bar_high` `src/engine_state_hash.cpp:66`) and, since R5 lane E6, set by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` - `pine_strategy_host.cpp:548`) and the kernel derives the pair — the intrabar-fill excursion + `pine_strategy_host.cpp:549`) and the kernel derives the pair — the intrabar-fill excursion mask. Rule 5 is now scoped to that pair's storage; the comment residue it also covered was deleted by R5 lane E6 (Section A below). - **The wider coupling inventory, re-derived.** `docs/design/native-feature-parity.md` §2.ii @@ -649,7 +649,7 @@ Every line number in this section names its symbol on this tree, which | `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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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:2325-2327`) | 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:1583`): 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:1565`) 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 the registry's `sec_id`s are not dense from 0; and when the kernel refuses the declaration. R5 lane H-MEASURE pins each reachable condition with what it changes (`tests/test_adapter_security_route_conditions.cpp`, a site through the evaluator against the same site through a bare kernel host): on the feeds measured the magnifier and an aggregated chart change no delivered value, while the range-start cut, the projection's incomplete tail, `lookahead_on` and the OTC pins do; the four lower-timeframe flags, the publish gate and the calling-bar latches are subsumed by other clauses, and the empty or undetected timeframe checks are unreachable after begin-time validation | `examples/native/native_htf_strategy.cpp`, `tests/test_native_htf_subscriptions.cpp` | +| `subscriptions` | **adapter-hook** | `declare_timeframe_subscriptions` from the begin-time hook (`pine_strategy_host.cpp:1584`): 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:1565`) 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 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 close ending within it of an interior lot boundary is one selected `Flatten` of whole lots (`source_fifo_prefix_openings`, `src/source/pine_adapter.cpp`), 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 | `tests/test_native_quantity_tolerance.cpp` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index f66fec85..6da9d62d 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -131,8 +131,8 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1483-1486, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1391-1547; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1484 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1124 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1107 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | -| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI `strategy_set_native_security_feed` c_abi.cpp:839) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1429-1445 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1516; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1447-1452 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:487 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8276 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:46-56). | +| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1484-1487, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1392-1548; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1485 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1124 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1107 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | +| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI `strategy_set_native_security_feed` c_abi.cpp:839) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1429-1445 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1517; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1447-1452 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:487 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8276 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:46-56). | | HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:637) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:916 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | | HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:21-220, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:241-413; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:514) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:255-336) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | @@ -159,7 +159,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | RP2 | Closed-trade list, trade statistics, report struct | **yes** — `fill_report` engine_report.cpp:39-66, `get_trade` engine.hpp:1791-1828, `compute_trade_stats` engine_metrics.cpp:138 | K | — | F:H2 O:I3 S:X2 | — | | RP3 | Equity curve | **no** — `update_equity_extremes` engine.hpp:1362 / `record_equity_point` engine.hpp:1380 are protected and were called only by the adapter **Closed:** `NativeReportPolicy::KernelRecorded` native_run_spec.hpp:63 has the consumer fold the extremes and append one point per script calculation, labelled with the script interval's open; `KernelRecordedAtHostMarks` records the same series at the host's own marks (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I4 S:X2 | S's "partial" folds RP2 in | | RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:219; `fill_metrics_section` engine_report.cpp:119-139 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | -| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1683) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1408) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | +| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1684) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1409) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | | RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9236, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | | RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1358, native_execution_consumer.cpp:10622; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | @@ -176,7 +176,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OT4 | Cooperative abort | **yes** — `request_abort` engine.hpp:2158, `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:896, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:940, probe `strategy_execution_contract` c_abi.cpp:873, stream `strategy_stream_begin` c_abi.cpp:624 **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:548 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:907 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 58 runtime exports (and eight generated per-strategy exports) (`tests/test_native_c_api.c`, `tests/test_native_c_api_frozen_header.cpp`). | K | L13 | F:J6 O:J5 | R5-7: post-v1. S covers it in its §2.3 without a row. | -| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1454 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7266), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1764 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:857; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | +| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1454 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7266), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1765 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:857; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | | OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:288-290) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | --- @@ -211,7 +211,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19158-19187 | 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:522, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:548) 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:308-585) 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 | +| 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:522, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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:603. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1273 | 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) | | m | `request.security` machinery | `register_security_eval` engine_security.cpp:27, `set_native_security_feed` engine_aux_security.cpp:76-127, `SecurityEvalState` engine.hpp:1156-1360 carry Pine semantics (lookahead, gaps, Heikin-Ashi, KI-55 range start) and are driven only by the source layer | HASH-VISIBLE / measured path | F planned to move both TUs and the state into the source host after native HTF. **R5-6 narrows this:** L6 reuses the feed store and routing, so those stay kernel; only the Pine-only evaluator semantics remain candidates. Do last. **Status (lane L12b): done** — the evaluator semantics are `source::PineSecurityEvalState` + `src/source/pine_security_eval.cpp`; the kernel keeps the registry, aggregator, feed store and one generic step | F S | @@ -288,7 +288,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the 1. **Spec:** `std::vector subscriptions` (Appendix A.5), hashed only when non-empty (digest like `native_intrabar_path_digest`). 2. **Wiring**, all inside the native consumer at Ready → begin and only when `subscriptions` is non-empty (zero work for the adapter): register one evaluator state per subscription through the existing `register_security_eval` (engine.hpp:1295-1297, *fresh*); install `authoritative_bars` into the existing store (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129); call `prepare_native_security_feeds` (engine.hpp:1639, defined `BacktestEngine::prepare_native_security_feeds` engine_aux_security.cpp:128, *fresh*) — the "made native-callable" step: reachable from the native consumer, still refused from host code in-run; pump `feed_security_eval_state` (engine.hpp:1312-1314, *fresh*) from the accepted-input path; deliver each completed bucket through `on_native_timeframe_bar`. -3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1304-1388 and its call order. The per-run evaluator init is today a *source-layer* member (`PineStrategyHost::init_security_eval_states_for_run` pine_scheduler.cpp:9-39, declared `init_security_eval_states_for_run` pine_strategy_host.hpp:694, *fresh*), so L6 adds a kernel-side twin instead of moving it. +3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1305-1389 and its call order. The per-run evaluator init is today a *source-layer* member (`PineStrategyHost::init_security_eval_states_for_run` pine_scheduler.cpp:9-39, declared `init_security_eval_states_for_run` pine_strategy_host.hpp:694, *fresh*), so L6 adds a kernel-side twin instead of moving it. 4. **Delivery rule (F):** a subscription bar is delivered on the input bar that completes it — its last contributing input bar, or, for a `LazyComplete` bucket, the next period's first input bar — immediately before that input's script calculation, never earlier — and never before an earlier script interval's lazily sealed calculation has run: the pump is ordered against the script interval, `seal(k) → deliver(i+1) → calc(i+1)` (L6d). 5. **Interim, documented in L0:** `on_native_input` + `TimeframeAggregator` (HT6). Doc notes (O): `set_native_security_feed` is public but inert without evaluators and throws in-run (`guard_native_mutation` engine_aux_security.cpp:78, which latches `UnsupportedSource` native_execution_consumer.cpp:1455); `set_aux_security_feed` returns false in the kernel engine_consumer.cpp:158-160. 6. **Entanglement to put in the lane brief** (*fresh* read of `set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:46-78): the feed machinery is TV-calibrated. "W" / "M" buckets are built from the installed native daily bars, and the native stamps define the period partition (a holiday session folds into the next trade date's bar). A bare host that supplies `authoritative_bars` inherits those rules. Document them as the contract or gate them behind a partition policy — decide in the lane, not silently (§4 E8). @@ -324,7 +324,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:1730, `guard_native_mutation` engine_aux_security.cpp:78 | 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). No kernel TU includes a source header | `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13 | 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:739 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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:548; the ledger `close_logical_units_` pine_adapter.hpp:2013-2024; `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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549; the ledger `close_logical_units_` pine_adapter.hpp:2013-2024; `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) | 58 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 58 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:13933f70987f6bfe37d3015ae065a8d5ee5937c37431d48e2ecd1f17a4c29958` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:862, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:590-709, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:515-567, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:494-511, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:802-833, c_abi.cpp:929 | 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 | @@ -412,7 +412,7 @@ closure markers and `scripts/check_design_inventory.py`). | 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:5611-5615) and `drain_applied_notifications` (native_execution_consumer.cpp:6272-6285) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | -| L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1304-1388 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | +| L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1305-1389 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | | L7 | `native_working_requests()`, `cancel_all()`, `cancel_where(label)`, `TriggerAnchor`, `TrailTicks`, zero offset, `ReplaceOptions`; toolkit `submit_bracket`, `OrderBook` → A.6 | new kinds unused by the adapter; anchor defaults to `Absolute`; the adapter's `tick * 0.5` sentinel keeps working | bracket + trailing probe through the builder: exact where TR5 / FP7 are inactive (on-grid levels) (F); the zero-offset trail test asserts the run no longer fails (O); parent-limit + stop sibling, parent rejection, replacement, parent-cycle revival (S) | | L8 | `NativePriceGrid { None, QuantizeFills, QuantizeFillsAndTriggers }`, `NativeGridRounding` → A.7 | `None` for the adapter, permanently — **native-only by ruling, audit lane P6: §3.6.2** | a host with `QuantizeFills` books on-grid prices; a second test pins that `QuantizeFillsAndTriggers` still differs from TV's half-tick threshold (FP5) (O) | | L9 | `NativeRiskLimits`, `NativeRiskEvent`, `MatchRejectReason::RiskLimit` → A.8 | `risk` unset for the adapter; its ledger (`SourceDayLedger` pine_adapter.hpp:1200) untouched — **retained after measurement, audit lane N12; native-only by ruling, audit lane P6: §3.6.1** | risk probes: same halt bar (F); each limit + the day-boundary basis (O); breach, forced close, cancellation, next-day reset (S) | @@ -499,7 +499,7 @@ inside the per-bar body (corpus 442d497, `set_pine_risk_direction(1)` / `set_pine_risk_max_position_size(2)` / `set_pine_risk_max_intraday_filled_orders(3)`), so the adapter first sees a limit on script bar 0, from `PineStrategyHost::set_pine_risk_*` -(pine_strategy_host.cpp:952-974) — after `prepare_native_begin` has already +(pine_strategy_host.cpp:953-975) — after `prepare_native_begin` has already projected the spec (pine_strategy_host.cpp:245) and `configure_native` has folded it into the continuation digest. Measured: an env-gated probe printing the adapter's risk configuration inside `project()` on the corpus reads @@ -848,8 +848,8 @@ so the gross open profit is 800. | **the equity re-mark** | `last.open_profit = 0; last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl` | none — the kernel leaves the curve exactly as the run marked it | `RE1` (no fee): the adapter reports the curve's last point as `eq=100800 op=0`, the kernel as `eq=100800 op=800` — same equity, the open profit moved into the realized side. `RE2` (0.1 %): `eq=100758.8 op=0` against `eq=100800 op=800`; the adapter's equity is **41.2 lower**, exactly the round-trip commission of both lots (20.5 + 20.7), because the re-mark carries the NET row P&L where the run's own mark carried the GROSS open profit | retained: a report that restates the last equity point off net row P&L is TradingView's report shape. Re-marking the curve from the kernel would move a figure every bare host already reads | | **the extreme re-fold** | `max_equity_`/`min_equity_` reset to `initial_capital_`, `max_drawdown_`/`max_runup_` to 0, then `fold_equity_extreme` over the whole curve | none — the extremes stay as `update_equity_extremes` sampled them | `RE3` (a dip to 90 at bar 1, a peak of 108 at bar 3, 0.1 %): both sides agree on `maxeq=101400 mineq=99000 maxdd=2400`, and the run-up diverges — adapter `maxru=1758.8000000000029`, kernel `maxru=1800`. The re-fold is a no-op **except** where the re-marked last point differs, so the divergence is again the 41.2 round trip, modulo the binary fold order | retained: it is a consequence of the re-mark above, not a mechanism of its own; it cannot be re-lowered while the re-mark is adapter policy | | **the same-bar exit re-sort** | `sort_same_bar_exit_trades(trades_, adapter_)` — same-bar bracket exits ordered by script command sequence | none — the kernel never reorders `trades_` | not exercised by the four scenarios (it reorders CLOSED rows, not range-end rows); it is TradingView command-sequence ordering by construction (`git show ab9714be:src/source/pine_fills.cpp`, lines 664-670) | retained: script command sequence is a source-language fact | -| **the mark cadence** | three call sites, each clearing and rebuilding: the terminal sub-bar of the last source bar (`scheduler_record_range_end` pine_strategy_host.cpp:460), after every applied execution once terminal (`record_applied_range_end`, :649), and the scheduler's terminal-source-bar hook (`PineScheduler::applied` pine_scheduler_native.cpp:549, before the bar's broker hash) | once, after the last point of the run (`run_batch` ×3, `stream_end`) | structural, not a number: the adapter's rows are re-derived at each mark so the report reflects the book as of the last event on the terminal bar, and its re-marked equity is visible to that bar's broker hash. A one-shot run-end producer cannot occupy those three points | retained: three marks on a terminal bar is a TradingView report cadence, not a generic run-end fact | -| **the excursion projection gate** | `excursion_range_end_projection_` suppresses the owner's exit-bar path fold for the projection (`excursion_range_end_projection_` pine_strategy_host.cpp:856) | none — the generic builder folds what it folds | `RE1`: identical run-up/draw-down on both halves (`ru=500 dd=0`, `ru=300 dd=0`) because the adapter's own owner hook returns the carried magnitudes for a projection; the gate exists to keep it that way when a host owns lot excursions | retained: it is the host's excursion contract (`owns_lot_excursions`), which the kernel deliberately does not model | +| **the mark cadence** | three call sites, each clearing and rebuilding: the terminal sub-bar of the last source bar (`scheduler_record_range_end` pine_strategy_host.cpp:460), after every applied execution once terminal (`record_applied_range_end`, :650), and the scheduler's terminal-source-bar hook (`PineScheduler::applied` pine_scheduler_native.cpp:549, before the bar's broker hash) | once, after the last point of the run (`run_batch` ×3, `stream_end`) | structural, not a number: the adapter's rows are re-derived at each mark so the report reflects the book as of the last event on the terminal bar, and its re-marked equity is visible to that bar's broker hash. A one-shot run-end producer cannot occupy those three points | retained: three marks on a terminal bar is a TradingView report cadence, not a generic run-end fact | +| **the excursion projection gate** | `excursion_range_end_projection_` suppresses the owner's exit-bar path fold for the projection (`excursion_range_end_projection_` pine_strategy_host.cpp:857) | none — the generic builder folds what it folds | `RE1`: identical run-up/draw-down on both halves (`ru=500 dd=0`, `ru=300 dd=0`) because the adapter's own owner hook returns the carried magnitudes for a projection; the gate exists to keep it that way when a host owns lot excursions | retained: it is the host's excursion contract (`owns_lot_excursions`), which the kernel deliberately does not model | | **the tail / warm-up suppression** | `if (stream_warmup_mode_ \|\| realtime_tail_ \|\| equity_curve_.empty()) return;` | none — the kernel emits at `stream_end` whatever the run left open | structural: the kernel has no warm-up or realtime-tail notion, and `run_corpus`'s probe-tail suppression (`suppress_probe_tail`) is a harness fact | retained: a tail is a source-layer replay concept | **Control and witness.** `RE4` closes the lot at bar 2: neither side emits a @@ -909,7 +909,7 @@ this lane re-pins the affected v19 witnesses once. | ID | What TradingView and `ab9714be` do | What R4 slice C did | Owner and fix | Proof | |---|---|---|---|---| | AG1 | A trade row counts **chart** bars: its entry and exit bar are the `bar_index` the script read there, so `bar_index - strategy.opentrades.entry_bar_index(0)` is the bars held, and a tape row one chart bar long says "Duration (bars)" 1 | The pre-K-IDX kernel used the first contributing INPUT slot as its public interval index on an aggregated run. That made a two-script-bar trade report input indices and made `avg_bars_in_trade` count input slots. | **Kernel (Option A).** `NativeExecutionConsumer::ScriptBucket::script_index` assigns the sequential script index when a bucket seals. `NativeCoordinate::interval_index`, `PyramidEntry::entry_bar_index`, `Trade::exit_bar_index`, open-lot rows and the metrics calculation all consume that script index. `NativeCoordinate::input_interval_index` preserves the first input slot. `PineStrategyHost::on_native_applied` no longer rewrites lots or rows; Pine's excursion and cohort sampling still pass their explicit chart source index to the source-layer sampler. | `kc_a310` / `kc_a310_c` in the K-IDX scratch: batch, stream and both magnified modes report `0/1/2`, entry/exit `1/2`, avg `2`; `tests/test_aggregated_path_regressions.cpp` and `tests/test_pooc_fill_stamp_aggregation.cpp` remain green. TradingView: `tests/test_aggregated_entry_bar_index_tape.cpp` (R5 lane H-MEASURE), a tape whose exits read the indices, booked field for field on all three paths. | -| AG2 | Every fill of a chart bar is dated at the bar's **open**: the e25-f-islastbar tape dates all 510 of its `process_orders_on_close` rows there, and `ab9714be`'s aggregated rows equal its chart rows | The kernel dates a close point (the calculation, the close after it, a modeled bar's close leg) at `calculation_time()`, the bucket's close, and a fill the host executes at the current point (a `calc_on_order_fills` first-open chain, a same-bar close-then-entry, the KI-62 cover) at its decision floor, which on the no-path route the bucket's inputs have already carried to the close. The chart timeframe's zero-width interval hides both, since every point of a bar sits at its label | **Adapter, at projection time.** `on_native_applied` dates the lot a fill opened (`PyramidEntry::time`, hence `Trade::entry_time`) and the rows it closed (`Trade::exit_time`) at the chart bar's open: on a plain aggregated chart every point of a confirmed bar's modeled path (its open, the segments, the calculation, the close after it), where the chart timeframe's own points all sit; under the magnifier the close points alone (lane E27's measured cut), so its sub-bar fills keep their sub-bar instant as `ab9714be` dated them. A realtime print keeps its own instant, as the chart-timeframe stream keeps it, and so does a stream's quiet carried interval | the audit's pooc probe rows (`ab9714be`'s 19:45Z / 13:30Z on all three paths), row 4, the KI-62 row and the stream row (a chart-timeframe stream and the same stream fed 1m agree on every row, a 10:30:01 print fill and an 11:00 carried fill included); E27's stamp rows flipped. Measured: the chart path unchanged byte for byte, the magnified run moves only its POOC close-fill instants. TradingView itself dates a magnified fill at its chart bar's open too (218 of 218 fills on sixteen `lab tv` tapes, R5 lane H-MEASURE), where the magnified run keeps a sub-bar fill's own instant: a recorded presentation divergence; `tests/test_magnified_aggregated_tape.cpp` compares those rows at chart-bar granularity | +| AG2 | Every fill of a chart bar is dated at the bar's **open**: the e25-f-islastbar tape dates all 510 of its `process_orders_on_close` rows there, and `ab9714be`'s aggregated rows equal its chart rows | The kernel dates a close point (the calculation, the close after it, a modeled bar's close leg) at `calculation_time()`, the bucket's close, and a fill the host executes at the current point (a `calc_on_order_fills` first-open chain, a same-bar close-then-entry, the KI-62 cover) at its decision floor, which on the no-path route the bucket's inputs have already carried to the close. The chart timeframe's zero-width interval hides both, since every point of a bar sits at its label | **Adapter, at projection time.** `on_native_applied` dates the lot a fill opened (`PyramidEntry::time`, hence `Trade::entry_time`) and the rows it closed (`Trade::exit_time`) at the chart bar's open: on an aggregated chart every point of a confirmed bar's modeled path (its open, the segments, the calculation, the close after it), where the chart timeframe's own points all sit, and under the magnifier every sub-bar point too (each sub-bar's open, path and `ModeledOHLCClose`; lane E27 had cut it to the close points, R5 lane PAR-ORDERS widened it, TradingView's dating). A realtime print keeps its own instant, as the chart-timeframe stream keeps it, and so does a stream's quiet carried interval | the audit's pooc probe rows (`ab9714be`'s 19:45Z / 13:30Z on all three paths), row 4, the KI-62 row and the stream row (a chart-timeframe stream and the same stream fed 1m agree on every row, a 10:30:01 print fill and an 11:00 carried fill included); E27's stamp rows flipped. Measured: the chart path unchanged byte for byte, the magnified run moves only its POOC close-fill instants. TradingView itself dates a magnified fill at its chart bar's open too (218 of 218 fills on sixteen `lab tv` tapes, R5 lane H-MEASURE); the magnified run kept a sub-bar fill's own instant (71 of the 152 instants `tests/test_magnified_aggregated_tape.cpp` replays sat inside their bar) until R5 lane PAR-ORDERS, and the row now asserts every instant exactly | | AG3 | A `calc_on_order_fills` recalculation runs the script **on the bar the fill is on** and reads that bar's `session.ismarket` / `isfirstbar` / `islastbar`; `ab9714be` set them before any fill of the bar | The scheduler set the flags in the close callback only, so a recalculation published the PREVIOUS bar's: a fill at the next session day's 09:30 read `islastbar`. On the chart timeframe the close then read its lookahead from a count the recalculation had already advanced, one retained bar too far: a fill on 15:30 flagged 15:30 `islastbar` and, through E26's dual, 15:45 `isfirstbar` | **Adapter** (Pine language state, OT7). `scheduler_update_session_state` stays the one writer and runs once per script bar, at the bar's first publication: `PineScheduler::recalculate` asks before the script runs, `PineScheduler::bar` pine_scheduler_native.cpp:446 only when no recalculation published the bar. Since R5 lane F5 it selects the kernel's session-day facts of that bar, which every callback of the bar carries alike, so the scheduler's lookahead is gone. E26's session-day rule and its `isfirstbar` dual are unchanged | the audit's coof_session probes and the 15:30 case in `tests/test_aggregated_path_regressions.cpp`; section 7 of `tests/test_session_islastbar_aggregation.cpp` (TradingView's NYSE:F days with `calc_on_order_fills`, chart, aggregated and magnified: every callback of a bar reads the tape's flags) | **Recorded, not changed here.** @@ -1184,7 +1184,7 @@ engine.hpp:1666) is not. The other is a contract that the six folded fields of a booked row are never rewritten. In the tree they are not; the only writer after booking, the Pine host, rewrites `close_cause`, `exit_from_bracket`, `exit_bar_index`, `exit_comment` and `entry_incarnation` only; its -writes are `close_cause` pine_strategy_host.cpp:489, `exit_bar_index` :826, `exit_comment` :617, `close_cause` :631-636, `exit_from_bracket` :1247 and `entry_incarnation` :1428-1436. +writes are `close_cause` pine_strategy_host.cpp:489, `exit_bar_index` :827, `exit_comment` :618, `close_cause` :632-637, `exit_from_bracket` :1248 and `entry_incarnation` :1429-1437. But `trades_` engine.hpp:546 is a protected member any host can write, so a lane that takes the consumer-side digest has to make that contract explicit. Folding `(count, digest)` in place of the rows saves the 256-entry table but diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index 182acfc9..2e2991fc 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -502,23 +502,24 @@ void source::PineStrategyHost::on_native_applied( // calc_on_order_fills first-open chain, a same-bar cover) at its decision // floor, which the bucket's inputs have already carried to the close on // the no-path route. The adapter dates the rows it presents at the chart - // bar's open: every modeled point of a plain aggregated chart, as - // ab9714be's aggregation loop did, and under the magnifier, whose sub-bar - // fills keep their sub-bar's instant, the close points alone. A realtime - // print keeps its own instant, as it does on the chart timeframe, and so - // does a stream's quiet carried interval, whose context still names the - // bar before it. The kernel's own instant, which its FX lookup, decision - // floor and ordering read, is untouched (lane F1). + // bar's open: every modeled point of an aggregated chart, as ab9714be's + // aggregation loop did, and under the magnifier too -- each sub-bar's + // open, path and close (ModeledOHLCClose, which only a magnified run + // presents). TradingView dates every magnified fill at its chart bar's + // open, 218 of 218 on H-MEASURE's sixteen tapes; ab9714be kept a sub-bar + // fill's own instant, and so did this adapter for all but the close + // points until R5 lane PAR-ORDERS. A realtime print keeps its own + // instant, as it does on the chart timeframe, and so does a stream's + // quiet carried interval, whose context still names the bar before it. + // The kernel's own instant, which its FX lookup, decision floor and + // ordering read, is untouched (lane F1). const auto provenance = context.coordinate.provenance; const bool modeled_point = provenance == NativePriceProvenance::Confirmed || provenance == NativePriceProvenance::ModeledOHLCOpen + || provenance == NativePriceProvenance::ModeledOHLCClose || provenance == NativePriceProvenance::Calculation || provenance == NativePriceProvenance::AfterCalculationClose; - const bool close_point = provenance == NativePriceProvenance::Calculation - || provenance == NativePriceProvenance::AfterCalculationClose - || (provenance == NativePriceProvenance::Confirmed - && context.coordinate.path_phase == NativePathPhase::Close); - if (aggregated && (scheduler_.bar_magnifier_enabled() ? close_point : modeled_point)) { + if (aggregated && modeled_point) { const std::int64_t script_open = context.script_bar_open_ms; if (event.opened_units != 0.0) { for (auto& lot : pyramid_entries_) { diff --git a/tests/test_adapter_brackets_relower.cpp b/tests/test_adapter_brackets_relower.cpp index 6b0b55fe..59756118 100644 --- a/tests/test_adapter_brackets_relower.cpp +++ b/tests/test_adapter_brackets_relower.cpp @@ -1156,6 +1156,11 @@ struct Pinned { }; // R4D_PINNED_DATA_BEGIN — harvested on 577315a, unchanged adapter. +// R5 lane PAR-ORDERS re-harvested the magnified shapes' rows with this same +// -DPINEFORGE_R4D_HARVEST build on its tree: every fill of an aggregated +// magnified run is dated at its chart bar's open, as TradingView dates one, so +// 20 instants of 16 rows (nine Mag* shapes) moved onto their bar's open; no +// price, quantity, P&L, flag or digest moved. constexpr Row kRelBracketTp_rows[] = { {1700000060000LL, 1700000240000LL, 100, 102.5, 2, 5, 1, 0}, }; @@ -1347,50 +1352,50 @@ constexpr Row kRelBreakoutPairSetOnce_rows[] = { constexpr Pinned kRelBreakoutPairSetOnce = {kRelBreakoutPairSetOnce_rows, 2, 10006, 0, 0, 0, 0x157cc0001ede7376ULL}; constexpr Row kMagRelBreakoutPair_rows[] = { - {420000LL, 540000LL, 101.25, 102.75, 2, 3, 1, 0}, + {360000LL, 480000LL, 101.25, 102.75, 2, 3, 1, 0}, {600000LL, 720000LL, 102.75, 104.25, 2, 3, 1, 0}, - {840000LL, 900000LL, 103.25, 102.25, 2, -2, 1, 0}, - {960000LL, 1020000LL, 101.25, 100.25, 2, -2, 1, 0}, - {1080000LL, 1380000LL, 98.25, 99.25, 2, -2, 0, 0}, - {1440000LL, 1500000LL, 99.25, 100.25, 2, -2, 0, 0}, - {1620000LL, 1620000LL, 101.25, 102.75, 2, 3, 1, 0}, + {840000LL, 840000LL, 103.25, 102.25, 2, -2, 1, 0}, + {960000LL, 960000LL, 101.25, 100.25, 2, -2, 1, 0}, + {1080000LL, 1320000LL, 98.25, 99.25, 2, -2, 0, 0}, + {1440000LL, 1440000LL, 99.25, 100.25, 2, -2, 0, 0}, + {1560000LL, 1560000LL, 101.25, 102.75, 2, 3, 1, 0}, {1680000LL, 1680000LL, 103, 103, 2, 0, 1, 1}, }; constexpr Pinned kMagRelBreakoutPair = {kMagRelBreakoutPair_rows, 8, 10001, 8, 3, 2, 0x03474c49dca8bc04ULL}; constexpr Row kMagRelBracketTp_rows[] = { - {120000LL, 540000LL, 100, 102.5, 2, 5, 1, 0}, + {120000LL, 480000LL, 100, 102.5, 2, 5, 1, 0}, }; constexpr Pinned kMagRelBracketTp = {kMagRelBracketTp_rows, 1, 10005, 0, 0, 0, 0x577159c6dba2ac05ULL}; constexpr Row kMagRelBracketSl_rows[] = { - {120000LL, 1020000LL, 100, 98.5, 2, -3, 1, 0}, + {120000LL, 960000LL, 100, 98.5, 2, -3, 1, 0}, }; constexpr Pinned kMagRelBracketSl = {kMagRelBracketSl_rows, 1, 9997, 10.5, 0, 0, 0xa65938a0103f1ccdULL}; constexpr Row kMagRelTrailOffset_rows[] = { - {120000LL, 780000LL, 100, 103.5, 2, 7, 1, 0}, + {120000LL, 720000LL, 100, 103.5, 2, 7, 1, 0}, }; constexpr Pinned kMagRelTrailOffset = {kMagRelTrailOffset_rows, 1, 10007, 0.5, 0, 0, 0x61c23d6a28f0f142ULL}; constexpr Row kMagRelTrailOneShot_rows[] = { - {120000LL, 540000LL, 100, 102.5, 2, 5, 1, 0}, + {120000LL, 480000LL, 100, 102.5, 2, 5, 1, 0}, }; constexpr Pinned kMagRelTrailOneShot = {kMagRelTrailOneShot_rows, 1, 10005, 0, 0, 0, 0x83150528a91d732fULL}; constexpr Row kMagRelLimitParent_rows[] = { - {120000LL, 420000LL, 99.75, 101.75, 2, 4, 1, 0}, - {1020000LL, 1020000LL, 99.75, 98.75, 2, -2, 1, 0}, + {120000LL, 360000LL, 99.75, 101.75, 2, 4, 1, 0}, + {960000LL, 960000LL, 99.75, 98.75, 2, -2, 1, 0}, }; constexpr Pinned kMagRelLimitParent = {kMagRelLimitParent_rows, 2, 10002, 2, 0, 0, 0x24e46fd2675cf69fULL}; constexpr Row kMagRelStopParent_rows[] = { - {300000LL, 540000LL, 100.75, 102.25, 2, 3, 1, 0}, + {240000LL, 480000LL, 100.75, 102.25, 2, 3, 1, 0}, }; constexpr Pinned kMagRelStopParent = {kMagRelStopParent_rows, 1, 10003, 0, 0, 0, 0x9288d52f3e3a8aabULL}; constexpr Row kMagRelShort_rows[] = { - {120000LL, 420000LL, 100, 101.5, 2, -3, 0, 0}, + {120000LL, 360000LL, 100, 101.5, 2, -3, 0, 0}, }; constexpr Pinned kMagRelShort = {kMagRelShort_rows, 1, 9997, 3, 0, 0, 0x5ee3f00d56388cacULL}; @@ -1426,8 +1431,8 @@ constexpr Row kRelPyramidSetOnce_rows[] = { constexpr Pinned kRelPyramidSetOnce = {kRelPyramidSetOnce_rows, 2, 10014.5, 0, 0, 0, 0x3ff244519fd858e8ULL}; constexpr Row kMagRelPyramidSetOnce_rows[] = { - {120000LL, 660000LL, 100, 104, 2, 8, 1, 0}, - {360000LL, 660000LL, 100.75, 104, 2, 6.5, 1, 0}, + {120000LL, 600000LL, 100, 104, 2, 8, 1, 0}, + {360000LL, 600000LL, 100.75, 104, 2, 6.5, 1, 0}, }; constexpr Pinned kMagRelPyramidSetOnce = {kMagRelPyramidSetOnce_rows, 2, 10014.5, 0, 0, 0, 0xa608f3773ef1c340ULL}; // R4D_PINNED_DATA_END diff --git a/tests/test_adapter_coof_relower.cpp b/tests/test_adapter_coof_relower.cpp index ab74be2d..e18d6af8 100644 --- a/tests/test_adapter_coof_relower.cpp +++ b/tests/test_adapter_coof_relower.cpp @@ -296,7 +296,9 @@ const Bar kCascadeBars[] = {{100, 102, 98, 101, 1, 900000}, // --------------------------------------------------------------------------- // Harvested from the PRE-lowering library (engine main b0cec54). See the file // header: these are the adapter's own cadence and book, produced when the -// adapter still drove the recalculation itself. +// adapter still drove the recalculation itself. One instant since: R5 lane +// PAR-ORDERS dates l5 at its chart bar's open (900000), as TradingView dates a +// magnified fill; b0cec54 kept its sub-bar's instant (960000). // --------------------------------------------------------------------------- const char kExpectRefill[] = @@ -315,7 +317,7 @@ const char kExpectRefill[] = " l2 L2@99@900000\n" " l3 L3@101@900000\n" " l4 L4@100.25@900000\n" - " l5 L5@100.1@960000\n" + " l5 L5@100.1@900000\n" "U6 C8\n"; const char kExpectChronology[] = "#0:b0:u0:l0:o100:h101:l99:c100:f1\n" @@ -561,9 +563,11 @@ void test_pine_refusals_compose_with_the_kernel_cadence() { // 4. The waypoint deferral is still needed: the kernel's birth rule alone does // not reproduce TradingView's eligibility. Lane L5 measured this against a // bare native twin (tests/test_native_calc_timing.cpp, CT11); here it is -// measured against the adapter itself — the six refilled lots sit on the -// chart bar's own O/L/H/C and the next chart open, NOT on the successive -// sub-bar opens the unqualified birth rule would produce. +// measured against the adapter itself — the six refilled lots fill on the +// chart bar's own O/L/H/C and the next sub-bar's open, NOT at the +// successive sub-bar opens the unqualified birth rule would produce. Every +// fill is dated at the chart bar's open, as TradingView dates a magnified +// fill (R5 lane PAR-ORDERS), so the prices carry the difference. // --------------------------------------------------------------------------- void test_the_waypoint_deferral_is_still_load_bearing() { @@ -575,15 +579,14 @@ void test_the_waypoint_deferral_is_still_load_bearing() { CHECK(refill.last_error().empty()); const auto position = refill.physical_position(); CHECK(position.lot_count == 6); - const std::vector waypoint_times = {900000, 900000, 900000, - 900000, 900000, 960000}; - const std::vector birth_rule_times = {900000, 960000, 1020000, - 1080000, 1140000, 1200000}; - for (std::size_t i = 0; i < position.lot_count && i < waypoint_times.size(); ++i) { + const std::vector waypoint_prices = {100.0, 100.0, 99.0, 101.0, 100.25, 100.1}; + const std::vector birth_rule_prices = {100.0, 100.1, 100.2, 100.3, 100.4, 100.5}; + for (std::size_t i = 0; i < position.lot_count && i < waypoint_prices.size(); ++i) { const int index = static_cast(i); - CHECK(refill.lot_time(index) == waypoint_times[i]); - CHECK(refill.lot_time(index) != birth_rule_times[i] - || waypoint_times[i] == birth_rule_times[i]); + CHECK(refill.lot_time(index) == 900000); + CHECK(std::abs(refill.lot_price(index) - waypoint_prices[i]) < 1e-9); + CHECK(std::abs(refill.lot_price(index) - birth_rule_prices[i]) > 1e-9 + || waypoint_prices[i] == birth_rule_prices[i]); } } #endif // PF_COOF_HARVEST diff --git a/tests/test_magnified_aggregated_tape.cpp b/tests/test_magnified_aggregated_tape.cpp index e8d7a09e..783ffb00 100644 --- a/tests/test_magnified_aggregated_tape.cpp +++ b/tests/test_magnified_aggregated_tape.cpp @@ -13,11 +13,13 @@ * tests/fixtures/session_islastbar, use_bar_magnifier=true (hm-mag-*) and false * (hm-chart-*). TradingView books 12 rows (v0, v2) and 14 (v4), as this engine * does on every path: ab9714be's 9 was its own divergence. - * - The rows are asserted at chart-bar granularity: TradingView dates a magnified - * fill at its chart bar's open, where the adapter keeps a magnified sub-bar fill's - * own instant (design AG2); that presentation difference is a separate finding. - * Excursions are not asserted (the E19 excursion model differs on every path, - * the chart timeframe's included). + * - TradingView dates every fill at its chart bar's open, a magnified one too + * (218 of 218 fills on the sixteen tapes), and so does the engine on every path + * since R5 lane PAR-ORDERS: each row's instants are asserted exactly. (The + * adapter kept a magnified sub-bar fill's own instant, design AG2, and the row + * compared the bar each instant opens: 71 of the 152 instants the magnified + * replays book sat inside their bar.) Excursions are not asserted (the E19 + * excursion model differs on every path, the chart timeframe's included). * - Each row's bar indices are the chart bars its instants open, and its "Duration * (bars)" is exit bar - entry bar, on the magnified path as on the chart path: * since K-IDX the kernel books lots and rows in script-bar space. @@ -265,7 +267,6 @@ int chart_bar_of(std::int64_t ms) { return -1; } -std::int64_t bar_open(std::int64_t ms) { return ms - ms % kBar; } // TradingView's exit signal for strategy.close_all is "Close position order". std::string exit_signal(const MagDiffHost::Row& r) { @@ -304,7 +305,7 @@ void replay(const char* slug, const Variant& v, Path path, unsigned recorded = 0 const TapeTrade& tv = tape[i]; const MagDiffHost::Row& r = rows[i]; const bool ok = r.is_long == tv.is_long - && bar_open(r.entry_ms) == tv.entry_ms && bar_open(r.exit_ms) == tv.exit_ms + && r.entry_ms == tv.entry_ms && r.exit_ms == tv.exit_ms && near(r.entry_price, tv.entry_price, 1e-9) && near(r.exit_price, tv.exit_price, 1e-9) && r.qty == tv.qty && near(r.profit, tv.net_pnl, 0.00006) diff --git a/tests/test_native_calc_timing_twin.cpp b/tests/test_native_calc_timing_twin.cpp index 39059670..5015a386 100644 --- a/tests/test_native_calc_timing_twin.cpp +++ b/tests/test_native_calc_timing_twin.cpp @@ -165,10 +165,14 @@ void test_twin_against_the_adapter() { // has no waypoint rule: a newborn market request is eligible at the next // discrete matching point of the delivered path, which under a retained // lower feed is the next sub-bar's opening. Nothing else differs: same - // rule, same six ids, same order, same resulting book. + // rule, same six ids, same order, same resulting book. The adapter dates + // every one of its fills at the chart bar's open, the sixth (filled at + // the next sub-bar's open, 960000) included, as TradingView dates a + // magnified fill (R5 lane PAR-ORDERS); the native route keeps the + // kernel's instants. const std::vector adapter_prices = {100.0, 100.0, 99.0, 101.0, 100.25, 100.10}; const std::vector adapter_times = {900000, 900000, 900000, 900000, - 900000, 960000}; + 900000, 900000}; const std::vector native_prices = {100.0, 100.1, 100.2, 100.3, 100.4, 100.5}; const std::vector native_times = {900000, 960000, 1020000, 1080000, 1140000, 1200000}; From 1518fa8f55c023c641cce2bf0470055d88a4db55 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 07:25:47 +0800 Subject: [PATCH 31/83] PAR-ORDERS v19 hash re-pin The lane's four fixes move v19 hash values (and, where a fix moves a trade, a trade digest) in four witnesses. This commit re-harvests each once, on the lane's final tree, inside the unreleased v19 epoch; no epoch token moves. Every other witness row passes unchanged in the full suite (report_relower, recording_hash_witness, receipts_in_place, command_allocations, lookup_index_witness, host_view_memo, calendar_hash_witness). How: each pinned TU compiled with its own harvest switch (PINEFORGE_QUIET_BAR_HARVEST, PINEFORGE_D2D_PUBLICATION_HARVEST, PINEFORGE_V19E_HARVEST, PINEFORGE_P1_HARVEST) against the tree's library, its table replaced by the harvest; a second harvest after writing reproduces every pin byte for byte. Why each moves (attributed by harvesting after every fix commit): - E14 (d474d728): a pending entry's trailing exit is no longer placed as an anchored kernel child, so the kernel records other command events, the source fold changes and later requests take other numbers. Hash inputs and Trade::entry_incarnation only: across the 272 quiet-bar runs and the 10,122 live-state trades no trade's times, prices, quantity, P&L, excursions or bars move, and the publication witness without its three hash inputs prints INT25's table. - cap (8d312d81): a same-side process_orders_on_close add is refused when the lots then open already reach pyramiding and no opposite opening is in the close pass. Real trades move: quiet-bar Random10, Random14, Random21; publication Storm11/Storm11M; nine live-state configurations. Traced: every command it drops is such an add (lots open >= pyramiding). - POOC stop (767bb2b1): three live-state configurations (reversals seeds 1152019, 1675664, 2722954) each fill a lone stop entry its close crossed at that close. - magnified dating (1fc67988): continuation_view's aggregated_magnifier reads (the lots' dated times are Pine hash inputs). By commit: quiet_bar 62 of 272 runs (E14 50, E14+cap 12); publication 21 of 156 (E14 17, cap 4); live_state 29 of 108 (E14 20, E14+cap 2, cap 4, cap+POOC stop 3); continuation_view k_aggregated_magnifier 3 of 5 values (magnified dating). One live-state configuration (reversals seed 3246599) still ends in the kernel's "native current evaluated allowance mismatch", as it did on INT25. Moved (old -> new; each pinned file carries the same list): test_adapter_quiet_bar: 62 moved Trail/0/1: boundary 0x5211ba6f22808f15 -> 0xa09d959161ab83b5, rows 0x5fd4f2baecf12c67 -> 0x6db293f4d9ba407e, final_hash 0x1ca55654b8ff3a6e -> 0xf9e526fb6a079950 Trail/0/0: boundary 0x5211ba6f22808f15 -> 0xa09d959161ab83b5, final_hash 0x1ca55654b8ff3a6e -> 0xf9e526fb6a079950 Trail/1/1: boundary 0x596611dc371ff159 -> 0x2a39cc8c834a5287, rows 0x5ebc78f3f36b3da8 -> 0xe23d175bdbe6d254, final_hash 0x2d3ea22c1cd861e4 -> 0xffac0df8a2dbc19e Trail/1/0: boundary 0x596611dc371ff159 -> 0x2a39cc8c834a5287, final_hash 0x2d3ea22c1cd861e4 -> 0xffac0df8a2dbc19e Random01/0/1: boundary 0xc538d8fca421c703 -> 0xfa3864e9f5f3a619, rows 0xc035295ef347b140 -> 0x22da84303f34b121, trades 0x5f25a36c2031c262 -> 0x71cab0c731b4168d, final_hash 0x166e28e2277135d2 -> 0xa142013345fb25dd Random01/0/0: boundary 0xc538d8fca421c703 -> 0xfa3864e9f5f3a619, trades 0x5f25a36c2031c262 -> 0x71cab0c731b4168d, final_hash 0x166e28e2277135d2 -> 0xa142013345fb25dd Random01/1/1: boundary 0x5b8b5e29d258d71c -> 0xfdafd368c22a3bb4, rows 0x16a72508f5fdd125 -> 0x30ace005c5ea2e1b, trades 0x81d6c3334c3a4ed0 -> 0xee55d992ca5fbe97, final_hash 0xa0becd4ec596146e -> 0xb44a05ed46f5109d Random01/1/0: boundary 0x5b8b5e29d258d71c -> 0xfdafd368c22a3bb4, trades 0x81d6c3334c3a4ed0 -> 0xee55d992ca5fbe97, final_hash 0xa0becd4ec596146e -> 0xb44a05ed46f5109d Random05/0/1: boundary 0x38db302327d01e80 -> 0x3e8e6495fe2e6dc1, rows 0x2461425b8e878f0d -> 0x2703dbc59d3516a6, trades 0xae3b2415c28fc5cb -> 0x5da77f8971db3d7a, final_hash 0xe32002fbd2d6ea3e -> 0xb72557b7f6894eaf Random05/0/0: boundary 0x38db302327d01e80 -> 0x3e8e6495fe2e6dc1, trades 0xae3b2415c28fc5cb -> 0x5da77f8971db3d7a, final_hash 0xe32002fbd2d6ea3e -> 0xb72557b7f6894eaf Random05/1/1: boundary 0xe3ae54dee0ba6db5 -> 0x4be132773d14c0e0, rows 0xc1c6474702bc75e0 -> 0xa6803dead66d3343, trades 0x6b60d1d77a27186f -> 0xdf770d465571b020, final_hash 0x70343cf2d03a75eb -> 0xb3561169b361d8bf Random05/1/0: boundary 0xe3ae54dee0ba6db5 -> 0x4be132773d14c0e0, trades 0x6b60d1d77a27186f -> 0xdf770d465571b020, final_hash 0x70343cf2d03a75eb -> 0xb3561169b361d8bf Random10/0/1: boundary 0xe529db3277bf6dd9 -> 0x998ee74ea73334c6, rows 0x1a655455a6bd3b6f -> 0x5a3104007433abc0, trades 0x70ed423d22403976 -> 0x274b98362186a62f, final_hash 0xffdc77023cb52b44 -> 0x0a7888fb9b890fb4, boundary_count 71 -> 70, trade_count 24 -> 23 Random10/0/0: boundary 0xe529db3277bf6dd9 -> 0x998ee74ea73334c6, trades 0x70ed423d22403976 -> 0x274b98362186a62f, final_hash 0xffdc77023cb52b44 -> 0x0a7888fb9b890fb4, boundary_count 71 -> 70, trade_count 24 -> 23 Random10/1/1: boundary 0xb3a8db733c3f30bd -> 0xde7dbd52ac33a7ff, rows 0xa2424fa6550c3ef1 -> 0xaf8f0ca763b06949, trades 0x2d930b1932bc89bd -> 0x5dcb9b87576b70ed, final_hash 0x61b614a68f564bf0 -> 0xfbe70c357ac296cf, boundary_count 68 -> 67, trade_count 23 -> 22 Random10/1/0: boundary 0xb3a8db733c3f30bd -> 0xde7dbd52ac33a7ff, trades 0x2d930b1932bc89bd -> 0x5dcb9b87576b70ed, final_hash 0x61b614a68f564bf0 -> 0xfbe70c357ac296cf, boundary_count 68 -> 67, trade_count 23 -> 22 Random13/0/1: boundary 0xf255fd63895e7bad -> 0x39caae56088c1db8, rows 0x41470781ab6bef64 -> 0x4028b545fb2c3ad9, trades 0xb5e34ee7b3268a86 -> 0x7f720ad1034d2991, final_hash 0x57de0e46784b0f54 -> 0xc19e5f5bbefe0e7f Random13/0/0: boundary 0xf255fd63895e7bad -> 0x39caae56088c1db8, trades 0xb5e34ee7b3268a86 -> 0x7f720ad1034d2991, final_hash 0x57de0e46784b0f54 -> 0xc19e5f5bbefe0e7f Random13/1/1: boundary 0x7abd074c56aca8c6 -> 0x07b714c492b5ae85, rows 0x41de5734424c47ed -> 0xa03b3f8bed95186d, trades 0xab774513033e4b33 -> 0xfe702e1eb2f469c0, final_hash 0xa6590b3d3950e7bb -> 0xa9f1d1a463925b52 Random13/1/0: boundary 0x7abd074c56aca8c6 -> 0x07b714c492b5ae85, trades 0xab774513033e4b33 -> 0xfe702e1eb2f469c0, final_hash 0xa6590b3d3950e7bb -> 0xa9f1d1a463925b52 Random14/0/1: boundary 0x6b59b79be4590a72 -> 0x21085d70db4c336b, rows 0x6d4cbed6a7d3b423 -> 0x394ebfbc2c15522a, trades 0xbcda0382be163781 -> 0x4e8d14373274d558, final_hash 0x918405ca8ede2f52 -> 0xad1f71b1afdd37f8, trade_count 17 -> 16 Random14/0/0: boundary 0x6b59b79be4590a72 -> 0x21085d70db4c336b, trades 0xbcda0382be163781 -> 0x4e8d14373274d558, final_hash 0x918405ca8ede2f52 -> 0xad1f71b1afdd37f8, trade_count 17 -> 16 Random14/1/1: boundary 0x8dfaddf560dc80ca -> 0x5a8dbff598244e8c, rows 0x13e689009402d1e0 -> 0x4c48857b5cd25816, trades 0x8bca41c28a85c67a -> 0xbec287230f684e1d, final_hash 0x70415e0fc326b13c -> 0x7f45221fb1111aa3, trade_count 17 -> 16 Random14/1/0: boundary 0x8dfaddf560dc80ca -> 0x5a8dbff598244e8c, trades 0x8bca41c28a85c67a -> 0xbec287230f684e1d, final_hash 0x70415e0fc326b13c -> 0x7f45221fb1111aa3, trade_count 17 -> 16 Random21/0/1: boundary 0xe1da26acd373548a -> 0x18e96beb584671fc, rows 0xad828c8396454565 -> 0x6c5b4e5a67209f94, trades 0x7d29f9137d1bc337 -> 0x0571605cf9e50eb1, final_hash 0xd6a6f90f609ca64a -> 0xca719be0f46f42ac Random21/0/0: boundary 0xe1da26acd373548a -> 0x18e96beb584671fc, trades 0x7d29f9137d1bc337 -> 0x0571605cf9e50eb1, final_hash 0xd6a6f90f609ca64a -> 0xca719be0f46f42ac Random21/1/1: boundary 0x1b2388f0f789a7d1 -> 0xd32a038cc5b6ea6e, rows 0xc1ca64a0715ca8b4 -> 0x9074957bf890f8e6, trades 0x32f61d041209dc84 -> 0x20e23c0060589e5e, final_hash 0x08da0b92c166b77c -> 0xb6387745e72b0075 Random21/1/0: boundary 0x1b2388f0f789a7d1 -> 0xd32a038cc5b6ea6e, trades 0x32f61d041209dc84 -> 0x20e23c0060589e5e, final_hash 0x08da0b92c166b77c -> 0xb6387745e72b0075 Random25/0/1: boundary 0x873e291d0017c5dc -> 0x1091baf336b7e96a, rows 0xd20a941e2361c0df -> 0x5836120ce0f3d783, trades 0x5ff4203ec9ac379e -> 0xf92cddf4fc024c49, final_hash 0x7573fe39c01fb659 -> 0x3f0d68bfc74cf35f Random25/0/0: boundary 0x873e291d0017c5dc -> 0x1091baf336b7e96a, trades 0x5ff4203ec9ac379e -> 0xf92cddf4fc024c49, final_hash 0x7573fe39c01fb659 -> 0x3f0d68bfc74cf35f Random25/1/1: boundary 0x9006cf963c9d2db4 -> 0x4c59eab8929878ab, rows 0x39a1e5e11f193964 -> 0x90bf03c0ed26cd99, trades 0x5ff4203ec9ac379e -> 0xf92cddf4fc024c49, final_hash 0xb372cc6219683abd -> 0xeb9d9c36364972df Random25/1/0: boundary 0x9006cf963c9d2db4 -> 0x4c59eab8929878ab, trades 0x5ff4203ec9ac379e -> 0xf92cddf4fc024c49, final_hash 0xb372cc6219683abd -> 0xeb9d9c36364972df Random26/0/1: rows 0xd559a888703734a3 -> 0xc4ceaa2f46b3b1c7, final_hash 0x758182130fa5a986 -> 0x6d9f1512cb3c63ef Random26/0/0: final_hash 0x758182130fa5a986 -> 0x6d9f1512cb3c63ef Random26/1/1: rows 0x3d8702945df8ba26 -> 0x383f34833e879b1b, final_hash 0xeeb06d8c2a0c4772 -> 0x500ef2279c277ba0 Random26/1/0: final_hash 0xeeb06d8c2a0c4772 -> 0x500ef2279c277ba0 Random27/0/1: boundary 0x51e3ce5734d5737c -> 0xeafcaf72c5794d88, rows 0x35105942d4fa267f -> 0xfbb77c6f11f0944f, final_hash 0x030065ad81ba4087 -> 0xf2d844781c8e9819 Random27/0/0: boundary 0x51e3ce5734d5737c -> 0xeafcaf72c5794d88, final_hash 0x030065ad81ba4087 -> 0xf2d844781c8e9819 Random27/1/1: boundary 0x81fa7403c9f754c1 -> 0x433fe5d0340138d8, rows 0x4f43080f0a6c1974 -> 0x80e409be19d6131d, final_hash 0x900de7e225926b9b -> 0x2a459b69d95400b1 Random27/1/0: boundary 0x81fa7403c9f754c1 -> 0x433fe5d0340138d8, final_hash 0x900de7e225926b9b -> 0x2a459b69d95400b1 Random31/0/1: boundary 0x3b5f096d5bca7d49 -> 0x09b545a0067e8c26, rows 0xd72b574c253a553d -> 0x3c6ae0150bb63629, final_hash 0x466ccec496cced70 -> 0xfaf6a143c8b0d038 Random31/0/0: boundary 0x3b5f096d5bca7d49 -> 0x09b545a0067e8c26, final_hash 0x466ccec496cced70 -> 0xfaf6a143c8b0d038 Random31/1/1: boundary 0xc14041a700ffb6c1 -> 0xb42bd9cfbe060a73, rows 0xe3f3a394089513b1 -> 0x928f7cfde5bdb105, final_hash 0xc8e0343f71af4c85 -> 0xf602f077ecb2b7cf Random31/1/0: boundary 0xc14041a700ffb6c1 -> 0xb42bd9cfbe060a73, final_hash 0xc8e0343f71af4c85 -> 0xf602f077ecb2b7cf Random32/0/1: boundary 0x7bb420637675ac73 -> 0x220cd02053dbc40a, rows 0x7a70d1e9f78179c9 -> 0x9afe2ada13350695, final_hash 0x0e03ccad16dc63fd -> 0xe577534dd3033b52 Random32/0/0: boundary 0x7bb420637675ac73 -> 0x220cd02053dbc40a, final_hash 0x0e03ccad16dc63fd -> 0xe577534dd3033b52 Random32/1/1: boundary 0x8bd26473f6dc3b55 -> 0xc0ca43094334f008, rows 0xeb260eee22e520f0 -> 0xe85eb4136b8c435d, final_hash 0x597150e94c1a4012 -> 0xba233c9fc46444a4 Random32/1/0: boundary 0x8bd26473f6dc3b55 -> 0xc0ca43094334f008, final_hash 0x597150e94c1a4012 -> 0xba233c9fc46444a4 Random36/0/1: boundary 0x3e3529f5adea5e8a -> 0xd4b5588948d4b755, rows 0x16d9ee97193355f5 -> 0x6af2dc7dd208ef5d, trades 0x8ee686191c3d0539 -> 0x24bf064fe8416ffd, final_hash 0x32a2c44c6f794a8c -> 0x1980e25b047dea78 Random36/0/0: boundary 0x3e3529f5adea5e8a -> 0xd4b5588948d4b755, trades 0x8ee686191c3d0539 -> 0x24bf064fe8416ffd, final_hash 0x32a2c44c6f794a8c -> 0x1980e25b047dea78 Random36/1/1: boundary 0x99c3ad89ead2d6e0 -> 0xd1532dd9d52ae5f4, rows 0x144bd2f3c5b9c0f8 -> 0x0e1d68d8d6c4c5e2, trades 0x10b01a5c8873f5d5 -> 0x86d1b68c9a0453a2, final_hash 0xe6c283fd13b0660c -> 0xe93d9e6c724efef6 Random36/1/0: boundary 0x99c3ad89ead2d6e0 -> 0xd1532dd9d52ae5f4, trades 0x10b01a5c8873f5d5 -> 0x86d1b68c9a0453a2, final_hash 0xe6c283fd13b0660c -> 0xe93d9e6c724efef6 Random40/0/1: boundary 0x9f1a94ed263d51df -> 0x869959795f89f7af, rows 0x6f59acfa20c1f416 -> 0xe9919376f3542ea7, final_hash 0x95af814f9835cb65 -> 0xd91533e6639e22f9 Random40/0/0: boundary 0x9f1a94ed263d51df -> 0x869959795f89f7af, final_hash 0x95af814f9835cb65 -> 0xd91533e6639e22f9 Random40/1/1: boundary 0xf0451d9a693e54e3 -> 0xf47407130500a9bd, rows 0x01721733b1316376 -> 0xda9b4928c023ca07, final_hash 0x8fdb15e706d2b994 -> 0x5a8f1ec5a18f3731 Random40/1/0: boundary 0xf0451d9a693e54e3 -> 0xf47407130500a9bd, final_hash 0x8fdb15e706d2b994 -> 0x5a8f1ec5a18f3731 Random43/0/1: boundary 0x83e2926a6d6dec9c -> 0x7fb8967d6d6d24d2, rows 0x784d62a813c85f48 -> 0x98cae92178fe91eb, final_hash 0x5d4d3ed83aa4608c -> 0xdc29a310e9c7d06b Random43/0/0: boundary 0x83e2926a6d6dec9c -> 0x7fb8967d6d6d24d2, final_hash 0x5d4d3ed83aa4608c -> 0xdc29a310e9c7d06b Random43/1/1: boundary 0xb9a953e47994593f -> 0xffd1c55ce888ef72, rows 0x879899b5598309ee -> 0x8ac3c2c1053a4819, final_hash 0xb6e20a498ce1b25a -> 0x45393018e93b32be Random43/1/0: boundary 0xb9a953e47994593f -> 0xffd1c55ce888ef72, final_hash 0xb6e20a498ce1b25a -> 0x45393018e93b32be Random45/1/1: boundary 0x81ba9360ff25c670 -> 0x32a1d85e85b93f0a, rows 0xe345a7e76585b1de -> 0xebcfcdf78cd848a5, final_hash 0x6c13a376f0a1a840 -> 0x56b0e295081820c0 Random45/1/0: boundary 0x81ba9360ff25c670 -> 0x32a1d85e85b93f0a, final_hash 0x6c13a376f0a1a840 -> 0x56b0e295081820c0 test_publication_witness: 21 moved ConfigFlags113: digest 0x2a4b7804a0a3e27e -> 0xb0d7186d612e1063 ConfigFlags113M: digest 0x88c14690db445933 -> 0x066c20032094d9bf Storm01: digest 0x75c523aefe67f41a -> 0x2940f65451a3189f Storm01M: digest 0x5ea8235c86419d28 -> 0xbfc8780fbe24d51c Storm06: digest 0x52fe595453aed8f6 -> 0x4053ebf00e230b53 Storm06M: digest 0x570cecdbe9d2d0c0 -> 0x4d4c9f8c75e1901b Storm09: digest 0xe0004089dbf5f4ae -> 0x09195d4f14ec28dd Storm09M: digest 0x977b07e1c905757a -> 0x5a6cd7deec2033ff Storm10: digest 0xe7a0f75a29b2bd10 -> 0x7ae3261a9cbdeff3 Storm10M: digest 0xf3739b7278c799f2 -> 0x03595faccb3309cd Storm11: digest 0x144d68a22896e3bf -> 0xf46684d86365208a, trades 24 -> 22 Storm11M: digest 0xcbb7769e3a9bf7c6 -> 0xb5205907e08e01bf, trades 24 -> 22 Storm13: digest 0x327d8e77ac1de02c -> 0xde3f322bf0c59a98 Storm13M: digest 0x6d1a8b0e3f3199b3 -> 0xf7d902eba9385f60 Storm17M: digest 0x00de1819d109b846 -> 0x46e2d77c04f174e3 Storm18: digest 0xcb801349463a0b9a -> 0xe7e874c21bf9c5fc Storm18M: digest 0xc661ff254ed1d0b0 -> 0x75b39ccdb9b4542d Storm19: digest 0x33d7bba6e955b14a -> 0x8c49334fb012a382 Storm19M: digest 0x6e51f913b73007d5 -> 0x83ea12d967fea7e7 Storm22: digest 0xce56d5badb05638c -> 0xda5de455f464504d Storm22M: digest 0xa8b971a9116eb495 -> 0xfdaa191a97b50614 test_adapter_live_state_equivalence: 29 moved reversals seed 104729: 12303832286788334682 -> 8320704943709246891 (131 trades, 396 commands, 420 rows placed -> 131 trades, 397 commands, 426 rows placed) reversals seed 1152019: 15845567806271913335 -> 6104491581818825501 (148 trades, 386 commands, 415 rows placed -> 145 trades, 386 commands, 411 rows placed) reversals seed 1675664: 8598226526726960377 -> 9695271777348687666 (132 trades, 397 commands, 406 rows placed -> 125 trades, 398 commands, 400 rows placed) reversals seed 2199309: 17071186865396902026 -> 12243088919143375019 (114 trades, 375 commands, 341 rows placed -> 126 trades, 398 commands, 370 rows placed) reversals seed 2722954: 16169955000323314060 -> 1694449538189143220 (153 trades, 390 commands, 423 rows placed -> 150 trades, 390 commands, 419 rows placed) reversals seed 3770244: 11999293368236543422 -> 17742628010123667157 (53 trades, 339 commands, 240 rows placed -> 53 trades, 339 commands, 239 rows placed) chains seed 209460: 1482558061860139386 -> 11413128619516782387 (85 trades, 390 commands, 393 rows placed -> 85 trades, 390 commands, 386 rows placed) chains seed 314189: 12647248509338936453 -> 10134000431367652603 (107 trades, 526 commands, 446 rows placed -> 107 trades, 526 commands, 444 rows placed) chains seed 628376: 18133723332411023357 -> 10122741367483366791 (95 trades, 364 commands, 391 rows placed -> 90 trades, 358 commands, 415 rows placed) chains seed 733105: 15591750256092081664 -> 13574785125744972298 (88 trades, 351 commands, 319 rows placed -> 88 trades, 351 commands, 318 rows placed) chains seed 942563: 11616873062904594822 -> 12836024126514275333 chains seed 1047292: 2647666278133997762 -> 16444976724508807594 (64 trades, 343 commands, 344 rows placed -> 64 trades, 343 commands, 341 rows placed) chains seed 1152021: 10659847217212312660 -> 6715343122935757718 (70 trades, 342 commands, 418 rows placed -> 70 trades, 342 commands, 384 rows placed) chains seed 1466208: 13767422546817055791 -> 9973593188851025006 (73 trades, 382 commands, 345 rows placed -> 73 trades, 382 commands, 338 rows placed) chains seed 1570937: 10446321169177622098 -> 15204985627708005050 (90 trades, 366 commands, 419 rows placed -> 90 trades, 366 commands, 370 rows placed) chains seed 1675666: 16738581318629067020 -> 4161494051921551918 (64 trades, 321 commands, 389 rows placed -> 64 trades, 321 commands, 339 rows placed) chains seed 1780395: 12765469987058303812 -> 632278533107602324 (82 trades, 480 commands, 461 rows placed -> 82 trades, 480 commands, 426 rows placed) chains seed 2094582: 6926029722960384835 -> 10675979410645833515 (75 trades, 321 commands, 365 rows placed -> 75 trades, 321 commands, 355 rows placed) chains seed 2199311: 2330688028628663175 -> 11831052386180063129 (56 trades, 327 commands, 306 rows placed -> 56 trades, 327 commands, 278 rows placed) chains seed 2304040: 6167262563711331013 -> 9463870180619581974 (81 trades, 342 commands, 354 rows placed -> 81 trades, 342 commands, 353 rows placed) chains seed 2513498: 13079047485388200520 -> 1646636678506043505 (66 trades, 336 commands, 338 rows placed -> 66 trades, 336 commands, 332 rows placed) chains seed 2618227: 17296102854052744512 -> 15055331467095475975 (65 trades, 336 commands, 355 rows placed -> 65 trades, 336 commands, 331 rows placed) chains seed 2722956: 5539584074139432609 -> 18266136563684836716 (87 trades, 363 commands, 366 rows placed -> 62 trades, 345 commands, 344 rows placed) chains seed 2932414: 10884597100613452852 -> 6451466112212038981 (65 trades, 350 commands, 331 rows placed -> 65 trades, 350 commands, 330 rows placed) chains seed 3037143: 8164707371872766708 -> 6296605148496367564 (81 trades, 324 commands, 358 rows placed -> 81 trades, 324 commands, 328 rows placed) chains seed 3246601: 5433696715667756825 -> 17544117140908072910 (100 trades, 485 commands, 473 rows placed -> 98 trades, 485 commands, 468 rows placed) chains seed 3351330: 10570656349418425385 -> 9531902743311875506 (60 trades, 311 commands, 317 rows placed -> 60 trades, 311 commands, 310 rows placed) chains seed 3456059: 10524873942376059207 -> 1817828500136052373 (59 trades, 292 commands, 298 rows placed -> 59 trades, 292 commands, 289 rows placed) chains seed 3560788: 12441518321478214338 -> 5397962162622300398 (73 trades, 359 commands, 338 rows placed -> 73 trades, 359 commands, 337 rows placed) test_adapter_continuation_view: 1 moved k_aggregated_magnifier (3 of 5): 10607093485200722581 -> 7363580706773703120; 22607172627576334 -> 5380311451522306220; 22607172627576334 -> 5380311451522306220 Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 6945fc19fdfec5b99763197401272aa5a1596b18) --- tests/test_adapter_continuation_view.cpp | 10 +- ...st_adapter_live_state_equivalence_data.hpp | 60 +++--- tests/test_adapter_quiet_bar_pinned.inc | 188 ++++++++++++------ tests/test_publication_witness.cpp | 9 +- tests/test_publication_witness_pinned.inc | 65 ++++-- 5 files changed, 216 insertions(+), 116 deletions(-) diff --git a/tests/test_adapter_continuation_view.cpp b/tests/test_adapter_continuation_view.cpp index b6c74606..7f1ce403 100644 --- a/tests/test_adapter_continuation_view.cpp +++ b/tests/test_adapter_continuation_view.cpp @@ -449,12 +449,16 @@ constexpr std::uint64_t k_aggregated[] = { // 6106527879897255797ull -> 22607172627576334ull // 14842205570428090150ull -> 12265832737220737843ull // 6106527879897255797ull -> 22607172627576334ull +// expectation corrected (PAR-ORDERS, k_aggregated_magnifier, 3 of 5 values), because R5 lane PAR-ORDERS dates every fill of an aggregated magnified run at its chart bar's open, as TradingView does, and the lots' dated times are Pine hash inputs; harvested with this TU's harvest switch on the lane's tree (a second harvest reproduces it): +// 10607093485200722581ull -> 7363580706773703120ull +// 22607172627576334ull -> 5380311451522306220ull +// 22607172627576334ull -> 5380311451522306220ull constexpr std::uint64_t k_aggregated_magnifier[] = { 14031106247497767919ull, - 10607093485200722581ull, - 22607172627576334ull, + 7363580706773703120ull, + 5380311451522306220ull, 12265832737220737843ull, - 22607172627576334ull, + 5380311451522306220ull, }; // expectation corrected (k_coof, 10 of 10 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19): // 10454171698854540065ull -> 8160477046829279565ull diff --git a/tests/test_adapter_live_state_equivalence_data.hpp b/tests/test_adapter_live_state_equivalence_data.hpp index 89cc3e9b..7831cf5e 100644 --- a/tests/test_adapter_live_state_equivalence_data.hpp +++ b/tests/test_adapter_live_state_equivalence_data.hpp @@ -19,8 +19,10 @@ // K-IDX Option A re-pins the v19 transcript digests once for the script/input coordinate fold. +// R5 lane PAR-ORDERS re-harvested it the same way on its tree (29 of 108 digests +// move; see the lane's hash commit for why each moves). constexpr std::uint64_t kTranscriptDigests[] = { - 12303832286788334682ull, // reversals seed 104729, 131 trades, 396 commands, 420 rows placed + 8320704943709246891ull, // reversals seed 104729, 131 trades, 397 commands, 426 rows placed 3937583110602879389ull, // reversals seed 209458, 126 trades, 382 commands, 409 rows placed 9660742953292018077ull, // reversals seed 314187, 208 trades, 684 commands, 653 rows placed 14691229822702022375ull, // reversals seed 418916, 105 trades, 378 commands, 361 rows placed @@ -30,22 +32,22 @@ constexpr std::uint64_t kTranscriptDigests[] = { 15494092140752158088ull, // reversals seed 837832, 91 trades, 401 commands, 332 rows placed 5702736951683229543ull, // reversals seed 942561, 98 trades, 408 commands, 356 rows placed 10200415260680638236ull, // reversals seed 1047290, 134 trades, 402 commands, 393 rows placed - 15845567806271913335ull, // reversals seed 1152019, 148 trades, 386 commands, 415 rows placed + 6104491581818825501ull, // reversals seed 1152019, 145 trades, 386 commands, 411 rows placed 9826242374101611623ull, // reversals seed 1256748, 118 trades, 402 commands, 362 rows placed 4883135135199369800ull, // reversals seed 1361477, 105 trades, 413 commands, 349 rows placed 6980972530369447172ull, // reversals seed 1466206, 151 trades, 372 commands, 393 rows placed 419860461863827083ull, // reversals seed 1570935, 77 trades, 357 commands, 297 rows placed - 8598226526726960377ull, // reversals seed 1675664, 132 trades, 397 commands, 406 rows placed + 9695271777348687666ull, // reversals seed 1675664, 125 trades, 398 commands, 400 rows placed 3952138674949420146ull, // reversals seed 1780393, 197 trades, 704 commands, 645 rows placed 4513324373166417315ull, // reversals seed 1885122, 113 trades, 393 commands, 388 rows placed 18393465585117555058ull, // reversals seed 1989851, 180 trades, 430 commands, 448 rows placed 6120235323624511436ull, // reversals seed 2094580, 92 trades, 384 commands, 340 rows placed - 17071186865396902026ull, // reversals seed 2199309, 114 trades, 375 commands, 341 rows placed + 12243088919143375019ull, // reversals seed 2199309, 126 trades, 398 commands, 370 rows placed 7979846979225294549ull, // reversals seed 2304038, 149 trades, 412 commands, 418 rows placed 16092912242308174488ull, // reversals seed 2408767, 139 trades, 415 commands, 404 rows placed 1517737495207721277ull, // reversals seed 2513496, 110 trades, 390 commands, 394 rows placed 1610718425964291349ull, // reversals seed 2618225, 105 trades, 411 commands, 371 rows placed - 16169955000323314060ull, // reversals seed 2722954, 153 trades, 390 commands, 423 rows placed + 1694449538189143220ull, // reversals seed 2722954, 150 trades, 390 commands, 419 rows placed 8640229404818843386ull, // reversals seed 2827683, 72 trades, 339 commands, 277 rows placed 8135938306046315211ull, // reversals seed 2932412, 96 trades, 360 commands, 318 rows placed 9738428349677663372ull, // reversals seed 3037141, 114 trades, 405 commands, 377 rows placed @@ -55,7 +57,7 @@ constexpr std::uint64_t kTranscriptDigests[] = { 13691690510990974075ull, // reversals seed 3456057, 88 trades, 373 commands, 334 rows placed 14092552094636276530ull, // reversals seed 3560786, 147 trades, 413 commands, 444 rows placed 4203447471686931733ull, // reversals seed 3665515, 133 trades, 394 commands, 397 rows placed - 11999293368236543422ull, // reversals seed 3770244, 53 trades, 339 commands, 240 rows placed + 17742628010123667157ull, // reversals seed 3770244, 53 trades, 339 commands, 239 rows placed 5125743962394059998ull, // brackets seed 104730, 82 trades, 521 commands, 568 rows placed 3732927941814356572ull, // brackets seed 209459, 77 trades, 487 commands, 551 rows placed 10835407349126156117ull, // brackets seed 314188, 147 trades, 866 commands, 1036 rows placed @@ -93,39 +95,39 @@ constexpr std::uint64_t kTranscriptDigests[] = { 11302959202215131177ull, // brackets seed 3665516, 86 trades, 536 commands, 566 rows placed 5122510577651000745ull, // brackets seed 3770245, 78 trades, 503 commands, 560 rows placed 11244999202114714137ull, // chains seed 104731, 59 trades, 301 commands, 286 rows placed - 1482558061860139386ull, // chains seed 209460, 85 trades, 390 commands, 393 rows placed - 12647248509338936453ull, // chains seed 314189, 107 trades, 526 commands, 446 rows placed + 11413128619516782387ull, // chains seed 209460, 85 trades, 390 commands, 386 rows placed + 10134000431367652603ull, // chains seed 314189, 107 trades, 526 commands, 444 rows placed 3121930092179333450ull, // chains seed 418918, 45 trades, 312 commands, 299 rows placed 165784926829741990ull, // chains seed 523647, 61 trades, 322 commands, 329 rows placed - 18133723332411023357ull, // chains seed 628376, 95 trades, 364 commands, 391 rows placed - 15591750256092081664ull, // chains seed 733105, 88 trades, 351 commands, 319 rows placed + 10122741367483366791ull, // chains seed 628376, 90 trades, 358 commands, 415 rows placed + 13574785125744972298ull, // chains seed 733105, 88 trades, 351 commands, 318 rows placed 10108332716913767868ull, // chains seed 837834, 64 trades, 359 commands, 358 rows placed - 11616873062904594822ull, // chains seed 942563, 59 trades, 341 commands, 322 rows placed - 2647666278133997762ull, // chains seed 1047292, 64 trades, 343 commands, 344 rows placed - 10659847217212312660ull, // chains seed 1152021, 70 trades, 342 commands, 418 rows placed + 12836024126514275333ull, // chains seed 942563, 59 trades, 341 commands, 322 rows placed + 16444976724508807594ull, // chains seed 1047292, 64 trades, 343 commands, 341 rows placed + 6715343122935757718ull, // chains seed 1152021, 70 trades, 342 commands, 384 rows placed 12725802557554099964ull, // chains seed 1256750, 65 trades, 319 commands, 302 rows placed 1798455623603086886ull, // chains seed 1361479, 64 trades, 312 commands, 323 rows placed - 13767422546817055791ull, // chains seed 1466208, 73 trades, 382 commands, 345 rows placed - 10446321169177622098ull, // chains seed 1570937, 90 trades, 366 commands, 419 rows placed - 16738581318629067020ull, // chains seed 1675666, 64 trades, 321 commands, 389 rows placed - 12765469987058303812ull, // chains seed 1780395, 82 trades, 480 commands, 461 rows placed + 9973593188851025006ull, // chains seed 1466208, 73 trades, 382 commands, 338 rows placed + 15204985627708005050ull, // chains seed 1570937, 90 trades, 366 commands, 370 rows placed + 4161494051921551918ull, // chains seed 1675666, 64 trades, 321 commands, 339 rows placed + 632278533107602324ull, // chains seed 1780395, 82 trades, 480 commands, 426 rows placed 8033768331525115464ull, // chains seed 1885124, 66 trades, 360 commands, 304 rows placed 8033691744657508662ull, // chains seed 1989853, 73 trades, 353 commands, 373 rows placed - 6926029722960384835ull, // chains seed 2094582, 75 trades, 321 commands, 365 rows placed - 2330688028628663175ull, // chains seed 2199311, 56 trades, 327 commands, 306 rows placed - 6167262563711331013ull, // chains seed 2304040, 81 trades, 342 commands, 354 rows placed + 10675979410645833515ull, // chains seed 2094582, 75 trades, 321 commands, 355 rows placed + 11831052386180063129ull, // chains seed 2199311, 56 trades, 327 commands, 278 rows placed + 9463870180619581974ull, // chains seed 2304040, 81 trades, 342 commands, 353 rows placed 1057624106564667840ull, // chains seed 2408769, 67 trades, 347 commands, 374 rows placed - 13079047485388200520ull, // chains seed 2513498, 66 trades, 336 commands, 338 rows placed - 17296102854052744512ull, // chains seed 2618227, 65 trades, 336 commands, 355 rows placed - 5539584074139432609ull, // chains seed 2722956, 87 trades, 363 commands, 366 rows placed + 1646636678506043505ull, // chains seed 2513498, 66 trades, 336 commands, 332 rows placed + 15055331467095475975ull, // chains seed 2618227, 65 trades, 336 commands, 331 rows placed + 18266136563684836716ull, // chains seed 2722956, 62 trades, 345 commands, 344 rows placed 16845310385882942743ull, // chains seed 2827685, 57 trades, 337 commands, 325 rows placed - 10884597100613452852ull, // chains seed 2932414, 65 trades, 350 commands, 331 rows placed - 8164707371872766708ull, // chains seed 3037143, 81 trades, 324 commands, 358 rows placed + 6451466112212038981ull, // chains seed 2932414, 65 trades, 350 commands, 330 rows placed + 6296605148496367564ull, // chains seed 3037143, 81 trades, 324 commands, 328 rows placed 18120582394174365029ull, // chains seed 3141872, 82 trades, 355 commands, 349 rows placed - 5433696715667756825ull, // chains seed 3246601, 100 trades, 485 commands, 473 rows placed - 10570656349418425385ull, // chains seed 3351330, 60 trades, 311 commands, 317 rows placed - 10524873942376059207ull, // chains seed 3456059, 59 trades, 292 commands, 298 rows placed - 12441518321478214338ull, // chains seed 3560788, 73 trades, 359 commands, 338 rows placed + 17544117140908072910ull, // chains seed 3246601, 98 trades, 485 commands, 468 rows placed + 9531902743311875506ull, // chains seed 3351330, 60 trades, 311 commands, 310 rows placed + 1817828500136052373ull, // chains seed 3456059, 59 trades, 292 commands, 289 rows placed + 5397962162622300398ull, // chains seed 3560788, 73 trades, 359 commands, 337 rows placed 3084783482698433102ull, // chains seed 3665517, 70 trades, 347 commands, 326 rows placed 15996277680102595200ull, // chains seed 3770246, 76 trades, 362 commands, 298 rows placed }; diff --git a/tests/test_adapter_quiet_bar_pinned.inc b/tests/test_adapter_quiet_bar_pinned.inc index bf85b883..5b387fc3 100644 --- a/tests/test_adapter_quiet_bar_pinned.inc +++ b/tests/test_adapter_quiet_bar_pinned.inc @@ -898,6 +898,70 @@ // Random48/0/0: boundary 0xd0c92fe6fc290e0bull -> 0xaa6b18e4254e2f68ull, final_hash 0x840184ac798a5de2ull -> 0xaff43b38e6bc02acull // Random48/1/1: boundary 0xd39f4122a0d0a91dull -> 0x655a5637ae415d60ull, rows 0xd439da8c48c337ddull -> 0x13866e45be14eaeeull, final_hash 0x1969d9a52bdd51c2ull -> 0xa0e205aa768a0bc9ull // Random48/1/0: boundary 0xd39f4122a0d0a91dull -> 0x655a5637ae415d60ull, final_hash 0x1969d9a52bdd51c2ull -> 0xa0e205aa768a0bc9ull +// R5 lane PAR-ORDERS re-harvested it the same way on its tree (62 of 272 runs move; +// see the lane's hash commit for why each moves): +// Trail/0/1: boundary 0x5211ba6f22808f15 -> 0xa09d959161ab83b5, rows 0x5fd4f2baecf12c67 -> 0x6db293f4d9ba407e, final_hash 0x1ca55654b8ff3a6e -> 0xf9e526fb6a079950 +// Trail/0/0: boundary 0x5211ba6f22808f15 -> 0xa09d959161ab83b5, final_hash 0x1ca55654b8ff3a6e -> 0xf9e526fb6a079950 +// Trail/1/1: boundary 0x596611dc371ff159 -> 0x2a39cc8c834a5287, rows 0x5ebc78f3f36b3da8 -> 0xe23d175bdbe6d254, final_hash 0x2d3ea22c1cd861e4 -> 0xffac0df8a2dbc19e +// Trail/1/0: boundary 0x596611dc371ff159 -> 0x2a39cc8c834a5287, final_hash 0x2d3ea22c1cd861e4 -> 0xffac0df8a2dbc19e +// Random01/0/1: boundary 0xc538d8fca421c703 -> 0xfa3864e9f5f3a619, rows 0xc035295ef347b140 -> 0x22da84303f34b121, trades 0x5f25a36c2031c262 -> 0x71cab0c731b4168d, final_hash 0x166e28e2277135d2 -> 0xa142013345fb25dd +// Random01/0/0: boundary 0xc538d8fca421c703 -> 0xfa3864e9f5f3a619, trades 0x5f25a36c2031c262 -> 0x71cab0c731b4168d, final_hash 0x166e28e2277135d2 -> 0xa142013345fb25dd +// Random01/1/1: boundary 0x5b8b5e29d258d71c -> 0xfdafd368c22a3bb4, rows 0x16a72508f5fdd125 -> 0x30ace005c5ea2e1b, trades 0x81d6c3334c3a4ed0 -> 0xee55d992ca5fbe97, final_hash 0xa0becd4ec596146e -> 0xb44a05ed46f5109d +// Random01/1/0: boundary 0x5b8b5e29d258d71c -> 0xfdafd368c22a3bb4, trades 0x81d6c3334c3a4ed0 -> 0xee55d992ca5fbe97, final_hash 0xa0becd4ec596146e -> 0xb44a05ed46f5109d +// Random05/0/1: boundary 0x38db302327d01e80 -> 0x3e8e6495fe2e6dc1, rows 0x2461425b8e878f0d -> 0x2703dbc59d3516a6, trades 0xae3b2415c28fc5cb -> 0x5da77f8971db3d7a, final_hash 0xe32002fbd2d6ea3e -> 0xb72557b7f6894eaf +// Random05/0/0: boundary 0x38db302327d01e80 -> 0x3e8e6495fe2e6dc1, trades 0xae3b2415c28fc5cb -> 0x5da77f8971db3d7a, final_hash 0xe32002fbd2d6ea3e -> 0xb72557b7f6894eaf +// Random05/1/1: boundary 0xe3ae54dee0ba6db5 -> 0x4be132773d14c0e0, rows 0xc1c6474702bc75e0 -> 0xa6803dead66d3343, trades 0x6b60d1d77a27186f -> 0xdf770d465571b020, final_hash 0x70343cf2d03a75eb -> 0xb3561169b361d8bf +// Random05/1/0: boundary 0xe3ae54dee0ba6db5 -> 0x4be132773d14c0e0, trades 0x6b60d1d77a27186f -> 0xdf770d465571b020, final_hash 0x70343cf2d03a75eb -> 0xb3561169b361d8bf +// Random10/0/1: boundary 0xe529db3277bf6dd9 -> 0x998ee74ea73334c6, rows 0x1a655455a6bd3b6f -> 0x5a3104007433abc0, trades 0x70ed423d22403976 -> 0x274b98362186a62f, final_hash 0xffdc77023cb52b44 -> 0x0a7888fb9b890fb4, boundary_count 71 -> 70, trade_count 24 -> 23 +// Random10/0/0: boundary 0xe529db3277bf6dd9 -> 0x998ee74ea73334c6, trades 0x70ed423d22403976 -> 0x274b98362186a62f, final_hash 0xffdc77023cb52b44 -> 0x0a7888fb9b890fb4, boundary_count 71 -> 70, trade_count 24 -> 23 +// Random10/1/1: boundary 0xb3a8db733c3f30bd -> 0xde7dbd52ac33a7ff, rows 0xa2424fa6550c3ef1 -> 0xaf8f0ca763b06949, trades 0x2d930b1932bc89bd -> 0x5dcb9b87576b70ed, final_hash 0x61b614a68f564bf0 -> 0xfbe70c357ac296cf, boundary_count 68 -> 67, trade_count 23 -> 22 +// Random10/1/0: boundary 0xb3a8db733c3f30bd -> 0xde7dbd52ac33a7ff, trades 0x2d930b1932bc89bd -> 0x5dcb9b87576b70ed, final_hash 0x61b614a68f564bf0 -> 0xfbe70c357ac296cf, boundary_count 68 -> 67, trade_count 23 -> 22 +// Random13/0/1: boundary 0xf255fd63895e7bad -> 0x39caae56088c1db8, rows 0x41470781ab6bef64 -> 0x4028b545fb2c3ad9, trades 0xb5e34ee7b3268a86 -> 0x7f720ad1034d2991, final_hash 0x57de0e46784b0f54 -> 0xc19e5f5bbefe0e7f +// Random13/0/0: boundary 0xf255fd63895e7bad -> 0x39caae56088c1db8, trades 0xb5e34ee7b3268a86 -> 0x7f720ad1034d2991, final_hash 0x57de0e46784b0f54 -> 0xc19e5f5bbefe0e7f +// Random13/1/1: boundary 0x7abd074c56aca8c6 -> 0x07b714c492b5ae85, rows 0x41de5734424c47ed -> 0xa03b3f8bed95186d, trades 0xab774513033e4b33 -> 0xfe702e1eb2f469c0, final_hash 0xa6590b3d3950e7bb -> 0xa9f1d1a463925b52 +// Random13/1/0: boundary 0x7abd074c56aca8c6 -> 0x07b714c492b5ae85, trades 0xab774513033e4b33 -> 0xfe702e1eb2f469c0, final_hash 0xa6590b3d3950e7bb -> 0xa9f1d1a463925b52 +// Random14/0/1: boundary 0x6b59b79be4590a72 -> 0x21085d70db4c336b, rows 0x6d4cbed6a7d3b423 -> 0x394ebfbc2c15522a, trades 0xbcda0382be163781 -> 0x4e8d14373274d558, final_hash 0x918405ca8ede2f52 -> 0xad1f71b1afdd37f8, trade_count 17 -> 16 +// Random14/0/0: boundary 0x6b59b79be4590a72 -> 0x21085d70db4c336b, trades 0xbcda0382be163781 -> 0x4e8d14373274d558, final_hash 0x918405ca8ede2f52 -> 0xad1f71b1afdd37f8, trade_count 17 -> 16 +// Random14/1/1: boundary 0x8dfaddf560dc80ca -> 0x5a8dbff598244e8c, rows 0x13e689009402d1e0 -> 0x4c48857b5cd25816, trades 0x8bca41c28a85c67a -> 0xbec287230f684e1d, final_hash 0x70415e0fc326b13c -> 0x7f45221fb1111aa3, trade_count 17 -> 16 +// Random14/1/0: boundary 0x8dfaddf560dc80ca -> 0x5a8dbff598244e8c, trades 0x8bca41c28a85c67a -> 0xbec287230f684e1d, final_hash 0x70415e0fc326b13c -> 0x7f45221fb1111aa3, trade_count 17 -> 16 +// Random21/0/1: boundary 0xe1da26acd373548a -> 0x18e96beb584671fc, rows 0xad828c8396454565 -> 0x6c5b4e5a67209f94, trades 0x7d29f9137d1bc337 -> 0x0571605cf9e50eb1, final_hash 0xd6a6f90f609ca64a -> 0xca719be0f46f42ac +// Random21/0/0: boundary 0xe1da26acd373548a -> 0x18e96beb584671fc, trades 0x7d29f9137d1bc337 -> 0x0571605cf9e50eb1, final_hash 0xd6a6f90f609ca64a -> 0xca719be0f46f42ac +// Random21/1/1: boundary 0x1b2388f0f789a7d1 -> 0xd32a038cc5b6ea6e, rows 0xc1ca64a0715ca8b4 -> 0x9074957bf890f8e6, trades 0x32f61d041209dc84 -> 0x20e23c0060589e5e, final_hash 0x08da0b92c166b77c -> 0xb6387745e72b0075 +// Random21/1/0: boundary 0x1b2388f0f789a7d1 -> 0xd32a038cc5b6ea6e, trades 0x32f61d041209dc84 -> 0x20e23c0060589e5e, final_hash 0x08da0b92c166b77c -> 0xb6387745e72b0075 +// Random25/0/1: boundary 0x873e291d0017c5dc -> 0x1091baf336b7e96a, rows 0xd20a941e2361c0df -> 0x5836120ce0f3d783, trades 0x5ff4203ec9ac379e -> 0xf92cddf4fc024c49, final_hash 0x7573fe39c01fb659 -> 0x3f0d68bfc74cf35f +// Random25/0/0: boundary 0x873e291d0017c5dc -> 0x1091baf336b7e96a, trades 0x5ff4203ec9ac379e -> 0xf92cddf4fc024c49, final_hash 0x7573fe39c01fb659 -> 0x3f0d68bfc74cf35f +// Random25/1/1: boundary 0x9006cf963c9d2db4 -> 0x4c59eab8929878ab, rows 0x39a1e5e11f193964 -> 0x90bf03c0ed26cd99, trades 0x5ff4203ec9ac379e -> 0xf92cddf4fc024c49, final_hash 0xb372cc6219683abd -> 0xeb9d9c36364972df +// Random25/1/0: boundary 0x9006cf963c9d2db4 -> 0x4c59eab8929878ab, trades 0x5ff4203ec9ac379e -> 0xf92cddf4fc024c49, final_hash 0xb372cc6219683abd -> 0xeb9d9c36364972df +// Random26/0/1: rows 0xd559a888703734a3 -> 0xc4ceaa2f46b3b1c7, final_hash 0x758182130fa5a986 -> 0x6d9f1512cb3c63ef +// Random26/0/0: final_hash 0x758182130fa5a986 -> 0x6d9f1512cb3c63ef +// Random26/1/1: rows 0x3d8702945df8ba26 -> 0x383f34833e879b1b, final_hash 0xeeb06d8c2a0c4772 -> 0x500ef2279c277ba0 +// Random26/1/0: final_hash 0xeeb06d8c2a0c4772 -> 0x500ef2279c277ba0 +// Random27/0/1: boundary 0x51e3ce5734d5737c -> 0xeafcaf72c5794d88, rows 0x35105942d4fa267f -> 0xfbb77c6f11f0944f, final_hash 0x030065ad81ba4087 -> 0xf2d844781c8e9819 +// Random27/0/0: boundary 0x51e3ce5734d5737c -> 0xeafcaf72c5794d88, final_hash 0x030065ad81ba4087 -> 0xf2d844781c8e9819 +// Random27/1/1: boundary 0x81fa7403c9f754c1 -> 0x433fe5d0340138d8, rows 0x4f43080f0a6c1974 -> 0x80e409be19d6131d, final_hash 0x900de7e225926b9b -> 0x2a459b69d95400b1 +// Random27/1/0: boundary 0x81fa7403c9f754c1 -> 0x433fe5d0340138d8, final_hash 0x900de7e225926b9b -> 0x2a459b69d95400b1 +// Random31/0/1: boundary 0x3b5f096d5bca7d49 -> 0x09b545a0067e8c26, rows 0xd72b574c253a553d -> 0x3c6ae0150bb63629, final_hash 0x466ccec496cced70 -> 0xfaf6a143c8b0d038 +// Random31/0/0: boundary 0x3b5f096d5bca7d49 -> 0x09b545a0067e8c26, final_hash 0x466ccec496cced70 -> 0xfaf6a143c8b0d038 +// Random31/1/1: boundary 0xc14041a700ffb6c1 -> 0xb42bd9cfbe060a73, rows 0xe3f3a394089513b1 -> 0x928f7cfde5bdb105, final_hash 0xc8e0343f71af4c85 -> 0xf602f077ecb2b7cf +// Random31/1/0: boundary 0xc14041a700ffb6c1 -> 0xb42bd9cfbe060a73, final_hash 0xc8e0343f71af4c85 -> 0xf602f077ecb2b7cf +// Random32/0/1: boundary 0x7bb420637675ac73 -> 0x220cd02053dbc40a, rows 0x7a70d1e9f78179c9 -> 0x9afe2ada13350695, final_hash 0x0e03ccad16dc63fd -> 0xe577534dd3033b52 +// Random32/0/0: boundary 0x7bb420637675ac73 -> 0x220cd02053dbc40a, final_hash 0x0e03ccad16dc63fd -> 0xe577534dd3033b52 +// Random32/1/1: boundary 0x8bd26473f6dc3b55 -> 0xc0ca43094334f008, rows 0xeb260eee22e520f0 -> 0xe85eb4136b8c435d, final_hash 0x597150e94c1a4012 -> 0xba233c9fc46444a4 +// Random32/1/0: boundary 0x8bd26473f6dc3b55 -> 0xc0ca43094334f008, final_hash 0x597150e94c1a4012 -> 0xba233c9fc46444a4 +// Random36/0/1: boundary 0x3e3529f5adea5e8a -> 0xd4b5588948d4b755, rows 0x16d9ee97193355f5 -> 0x6af2dc7dd208ef5d, trades 0x8ee686191c3d0539 -> 0x24bf064fe8416ffd, final_hash 0x32a2c44c6f794a8c -> 0x1980e25b047dea78 +// Random36/0/0: boundary 0x3e3529f5adea5e8a -> 0xd4b5588948d4b755, trades 0x8ee686191c3d0539 -> 0x24bf064fe8416ffd, final_hash 0x32a2c44c6f794a8c -> 0x1980e25b047dea78 +// Random36/1/1: boundary 0x99c3ad89ead2d6e0 -> 0xd1532dd9d52ae5f4, rows 0x144bd2f3c5b9c0f8 -> 0x0e1d68d8d6c4c5e2, trades 0x10b01a5c8873f5d5 -> 0x86d1b68c9a0453a2, final_hash 0xe6c283fd13b0660c -> 0xe93d9e6c724efef6 +// Random36/1/0: boundary 0x99c3ad89ead2d6e0 -> 0xd1532dd9d52ae5f4, trades 0x10b01a5c8873f5d5 -> 0x86d1b68c9a0453a2, final_hash 0xe6c283fd13b0660c -> 0xe93d9e6c724efef6 +// Random40/0/1: boundary 0x9f1a94ed263d51df -> 0x869959795f89f7af, rows 0x6f59acfa20c1f416 -> 0xe9919376f3542ea7, final_hash 0x95af814f9835cb65 -> 0xd91533e6639e22f9 +// Random40/0/0: boundary 0x9f1a94ed263d51df -> 0x869959795f89f7af, final_hash 0x95af814f9835cb65 -> 0xd91533e6639e22f9 +// Random40/1/1: boundary 0xf0451d9a693e54e3 -> 0xf47407130500a9bd, rows 0x01721733b1316376 -> 0xda9b4928c023ca07, final_hash 0x8fdb15e706d2b994 -> 0x5a8f1ec5a18f3731 +// Random40/1/0: boundary 0xf0451d9a693e54e3 -> 0xf47407130500a9bd, final_hash 0x8fdb15e706d2b994 -> 0x5a8f1ec5a18f3731 +// Random43/0/1: boundary 0x83e2926a6d6dec9c -> 0x7fb8967d6d6d24d2, rows 0x784d62a813c85f48 -> 0x98cae92178fe91eb, final_hash 0x5d4d3ed83aa4608c -> 0xdc29a310e9c7d06b +// Random43/0/0: boundary 0x83e2926a6d6dec9c -> 0x7fb8967d6d6d24d2, final_hash 0x5d4d3ed83aa4608c -> 0xdc29a310e9c7d06b +// Random43/1/1: boundary 0xb9a953e47994593f -> 0xffd1c55ce888ef72, rows 0x879899b5598309ee -> 0x8ac3c2c1053a4819, final_hash 0xb6e20a498ce1b25a -> 0x45393018e93b32be +// Random43/1/0: boundary 0xb9a953e47994593f -> 0xffd1c55ce888ef72, final_hash 0xb6e20a498ce1b25a -> 0x45393018e93b32be +// Random45/1/1: boundary 0x81ba9360ff25c670 -> 0x32a1d85e85b93f0a, rows 0xe345a7e76585b1de -> 0xebcfcdf78cd848a5, final_hash 0x6c13a376f0a1a840 -> 0x56b0e295081820c0 +// Random45/1/0: boundary 0x81ba9360ff25c670 -> 0x32a1d85e85b93f0a, final_hash 0x6c13a376f0a1a840 -> 0x56b0e295081820c0 constexpr Pinned kPinned[] = { {"Quiet", 0, 1, 0x1c2b2e37a44f6877ull, 0x81799b58f0e052b1ull, 0x47fe0d7eaf8e51e3ull, 0xe0aa4c0240939bcfull, 40, 40, 0}, {"Quiet", 0, 0, 0x1c2b2e37a44f6877ull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0xe0aa4c0240939bcfull, 40, 0, 0}, @@ -919,10 +983,10 @@ constexpr Pinned kPinned[] = { {"Bracket", 0, 0, 0x5ceffdf162a625baull, 0x14650fb0739d0383ull, 0xc17c8a18143849dcull, 0xb62be9f335a690fbull, 48, 0, 1}, {"Bracket", 1, 1, 0x53093fd52be58e6aull, 0x26c6fc455e7a9e3full, 0xd474aeacc0bce022ull, 0x2fbb7d2d1228c959ull, 48, 48, 1}, {"Bracket", 1, 0, 0x53093fd52be58e6aull, 0x14650fb0739d0383ull, 0xd474aeacc0bce022ull, 0x2fbb7d2d1228c959ull, 48, 0, 1}, - {"Trail", 0, 1, 0x5211ba6f22808f15ull, 0x5fd4f2baecf12c67ull, 0xd244c7dc0fdc94a1ull, 0x1ca55654b8ff3a6eull, 48, 48, 1}, - {"Trail", 0, 0, 0x5211ba6f22808f15ull, 0x14650fb0739d0383ull, 0xd244c7dc0fdc94a1ull, 0x1ca55654b8ff3a6eull, 48, 0, 1}, - {"Trail", 1, 1, 0x596611dc371ff159ull, 0x5ebc78f3f36b3da8ull, 0xd244c7dc0fdc94a1ull, 0x2d3ea22c1cd861e4ull, 48, 48, 1}, - {"Trail", 1, 0, 0x596611dc371ff159ull, 0x14650fb0739d0383ull, 0xd244c7dc0fdc94a1ull, 0x2d3ea22c1cd861e4ull, 48, 0, 1}, + {"Trail", 0, 1, 0xa09d959161ab83b5ull, 0x6db293f4d9ba407eull, 0xd244c7dc0fdc94a1ull, 0xf9e526fb6a079950ull, 48, 48, 1}, + {"Trail", 0, 0, 0xa09d959161ab83b5ull, 0x14650fb0739d0383ull, 0xd244c7dc0fdc94a1ull, 0xf9e526fb6a079950ull, 48, 0, 1}, + {"Trail", 1, 1, 0x2a39cc8c834a5287ull, 0xe23d175bdbe6d254ull, 0xd244c7dc0fdc94a1ull, 0xffac0df8a2dbc19eull, 48, 48, 1}, + {"Trail", 1, 0, 0x2a39cc8c834a5287ull, 0x14650fb0739d0383ull, 0xd244c7dc0fdc94a1ull, 0xffac0df8a2dbc19eull, 48, 0, 1}, {"DualStops", 0, 1, 0xf53e1aa110098577ull, 0xa45979a387e09947ull, 0xb19c9aea35851016ull, 0x9d20a3a0e3c4b991ull, 40, 40, 1}, {"DualStops", 0, 0, 0xf53e1aa110098577ull, 0x14650fb0739d0383ull, 0xb19c9aea35851016ull, 0x9d20a3a0e3c4b991ull, 40, 0, 1}, {"DualStops", 1, 1, 0xd56451041c2c2c4full, 0x3525fdbfa98d36bcull, 0xb19c9aea35851016ull, 0x0e2c1cdfb96b6301ull, 40, 40, 1}, @@ -979,10 +1043,10 @@ constexpr Pinned kPinned[] = { {"AccountFx", 0, 0, 0xaeec7af5859fe6b3ull, 0x14650fb0739d0383ull, 0x3b853d0c008c3078ull, 0x2a28b4e61cd5cbbcull, 48, 0, 1}, {"Stream", 0, 1, 0x49574a22705f52c0ull, 0x3cade1c0a6b65b75ull, 0xce38e7485b22ec04ull, 0x8c9bb935bf0c32daull, 40, 40, 5}, {"Stream", 0, 0, 0x49574a22705f52c0ull, 0x14650fb0739d0383ull, 0xce38e7485b22ec04ull, 0x8c9bb935bf0c32daull, 40, 0, 5}, - {"Random01", 0, 1, 0xc538d8fca421c703ull, 0xc035295ef347b140ull, 0x5f25a36c2031c262ull, 0x166e28e2277135d2ull, 55, 55, 15}, - {"Random01", 0, 0, 0xc538d8fca421c703ull, 0x14650fb0739d0383ull, 0x5f25a36c2031c262ull, 0x166e28e2277135d2ull, 55, 0, 15}, - {"Random01", 1, 1, 0x5b8b5e29d258d71cull, 0x16a72508f5fdd125ull, 0x81d6c3334c3a4ed0ull, 0xa0becd4ec596146eull, 55, 55, 15}, - {"Random01", 1, 0, 0x5b8b5e29d258d71cull, 0x14650fb0739d0383ull, 0x81d6c3334c3a4ed0ull, 0xa0becd4ec596146eull, 55, 0, 15}, + {"Random01", 0, 1, 0xfa3864e9f5f3a619ull, 0x22da84303f34b121ull, 0x71cab0c731b4168dull, 0xa142013345fb25ddull, 55, 55, 15}, + {"Random01", 0, 0, 0xfa3864e9f5f3a619ull, 0x14650fb0739d0383ull, 0x71cab0c731b4168dull, 0xa142013345fb25ddull, 55, 0, 15}, + {"Random01", 1, 1, 0xfdafd368c22a3bb4ull, 0x30ace005c5ea2e1bull, 0xee55d992ca5fbe97ull, 0xb44a05ed46f5109dull, 55, 55, 15}, + {"Random01", 1, 0, 0xfdafd368c22a3bb4ull, 0x14650fb0739d0383ull, 0xee55d992ca5fbe97ull, 0xb44a05ed46f5109dull, 55, 0, 15}, {"Random02", 0, 1, 0xf933c58464603966ull, 0x8549ba7d7e735482ull, 0xec986a4dcdf52208ull, 0xd9870babe5d18626ull, 58, 40, 12}, {"Random02", 0, 0, 0xf933c58464603966ull, 0x14650fb0739d0383ull, 0xec986a4dcdf52208ull, 0xd9870babe5d18626ull, 58, 0, 12}, {"Random02", 1, 1, 0x25ba5f1040df3b56ull, 0x1593a8e96a0c90a9ull, 0x95d130f42b9a5776ull, 0x0fb50af38c25c54dull, 58, 40, 12}, @@ -995,10 +1059,10 @@ constexpr Pinned kPinned[] = { {"Random04", 0, 0, 0x0c41be29e4497a7cull, 0x14650fb0739d0383ull, 0xc687874028817a5bull, 0x542f1b01ef32d535ull, 44, 0, 18}, {"Random04", 1, 1, 0x93ade030662e0b77ull, 0x5beecc5dc910feb8ull, 0x37389353e6ac8baeull, 0x254f4f4933be69e0ull, 44, 44, 18}, {"Random04", 1, 0, 0x93ade030662e0b77ull, 0x14650fb0739d0383ull, 0x37389353e6ac8baeull, 0x254f4f4933be69e0ull, 44, 0, 18}, - {"Random05", 0, 1, 0x38db302327d01e80ull, 0x2461425b8e878f0dull, 0xae3b2415c28fc5cbull, 0xe32002fbd2d6ea3eull, 58, 53, 3}, - {"Random05", 0, 0, 0x38db302327d01e80ull, 0x14650fb0739d0383ull, 0xae3b2415c28fc5cbull, 0xe32002fbd2d6ea3eull, 58, 0, 3}, - {"Random05", 1, 1, 0xe3ae54dee0ba6db5ull, 0xc1c6474702bc75e0ull, 0x6b60d1d77a27186full, 0x70343cf2d03a75ebull, 66, 53, 10}, - {"Random05", 1, 0, 0xe3ae54dee0ba6db5ull, 0x14650fb0739d0383ull, 0x6b60d1d77a27186full, 0x70343cf2d03a75ebull, 66, 0, 10}, + {"Random05", 0, 1, 0x3e8e6495fe2e6dc1ull, 0x2703dbc59d3516a6ull, 0x5da77f8971db3d7aull, 0xb72557b7f6894eafull, 58, 53, 3}, + {"Random05", 0, 0, 0x3e8e6495fe2e6dc1ull, 0x14650fb0739d0383ull, 0x5da77f8971db3d7aull, 0xb72557b7f6894eafull, 58, 0, 3}, + {"Random05", 1, 1, 0x4be132773d14c0e0ull, 0xa6803dead66d3343ull, 0xdf770d465571b020ull, 0xb3561169b361d8bfull, 66, 53, 10}, + {"Random05", 1, 0, 0x4be132773d14c0e0ull, 0x14650fb0739d0383ull, 0xdf770d465571b020ull, 0xb3561169b361d8bfull, 66, 0, 10}, {"Random06", 0, 1, 0xecb44da2cbdb5c42ull, 0x5485171eedfc86cfull, 0xb724c9aadc73f827ull, 0xff2efe1925e45a9dull, 46, 46, 13}, {"Random06", 0, 0, 0xecb44da2cbdb5c42ull, 0x14650fb0739d0383ull, 0xb724c9aadc73f827ull, 0xff2efe1925e45a9dull, 46, 0, 13}, {"Random06", 1, 1, 0x65cc03efd59ec6b5ull, 0x2dce2153546130dbull, 0xb1c8ba35c0dd393dull, 0x1130940f86f108d2ull, 46, 46, 13}, @@ -1015,10 +1079,10 @@ constexpr Pinned kPinned[] = { {"Random09", 0, 0, 0x400c46d70e6e84bcull, 0x14650fb0739d0383ull, 0x8abae7302d7911beull, 0x1f6cdd2e111e016aull, 58, 0, 2}, {"Random09", 1, 1, 0x32d4b35f029bad56ull, 0x447742874dde46e0ull, 0x99aae055f2d8091dull, 0x3d99e7abdd92a773ull, 58, 58, 2}, {"Random09", 1, 0, 0x32d4b35f029bad56ull, 0x14650fb0739d0383ull, 0x99aae055f2d8091dull, 0x3d99e7abdd92a773ull, 58, 0, 2}, - {"Random10", 0, 1, 0xe529db3277bf6dd9ull, 0x1a655455a6bd3b6full, 0x70ed423d22403976ull, 0xffdc77023cb52b44ull, 71, 51, 24}, - {"Random10", 0, 0, 0xe529db3277bf6dd9ull, 0x14650fb0739d0383ull, 0x70ed423d22403976ull, 0xffdc77023cb52b44ull, 71, 0, 24}, - {"Random10", 1, 1, 0xb3a8db733c3f30bdull, 0xa2424fa6550c3ef1ull, 0x2d930b1932bc89bdull, 0x61b614a68f564bf0ull, 68, 51, 23}, - {"Random10", 1, 0, 0xb3a8db733c3f30bdull, 0x14650fb0739d0383ull, 0x2d930b1932bc89bdull, 0x61b614a68f564bf0ull, 68, 0, 23}, + {"Random10", 0, 1, 0x998ee74ea73334c6ull, 0x5a3104007433abc0ull, 0x274b98362186a62full, 0x0a7888fb9b890fb4ull, 70, 51, 23}, + {"Random10", 0, 0, 0x998ee74ea73334c6ull, 0x14650fb0739d0383ull, 0x274b98362186a62full, 0x0a7888fb9b890fb4ull, 70, 0, 23}, + {"Random10", 1, 1, 0xde7dbd52ac33a7ffull, 0xaf8f0ca763b06949ull, 0x5dcb9b87576b70edull, 0xfbe70c357ac296cfull, 67, 51, 22}, + {"Random10", 1, 0, 0xde7dbd52ac33a7ffull, 0x14650fb0739d0383ull, 0x5dcb9b87576b70edull, 0xfbe70c357ac296cfull, 67, 0, 22}, {"Random11", 0, 1, 0x805038d1cb0a3848ull, 0xebc3fde6db4a37f3ull, 0x2f92b1af8d2cfafcull, 0x27bc15c5c142c728ull, 60, 60, 3}, {"Random11", 0, 0, 0x805038d1cb0a3848ull, 0x14650fb0739d0383ull, 0x2f92b1af8d2cfafcull, 0x27bc15c5c142c728ull, 60, 0, 3}, {"Random11", 1, 1, 0x4a739854a58a3db9ull, 0xc91f1e4519586a06ull, 0xad6d6d2577cca5c9ull, 0x8af717fd3fa04097ull, 60, 60, 8}, @@ -1027,14 +1091,14 @@ constexpr Pinned kPinned[] = { {"Random12", 0, 0, 0xb9a9bb3fafab7349ull, 0x14650fb0739d0383ull, 0x9ec5e7b69521e874ull, 0x29c94b5df165789aull, 92, 0, 22}, {"Random12", 1, 1, 0x298ab491e3caa61full, 0xf8e78400f1c148acull, 0xf2c6cada55accbe3ull, 0x927c4394206b42b2ull, 93, 62, 22}, {"Random12", 1, 0, 0x298ab491e3caa61full, 0x14650fb0739d0383ull, 0xf2c6cada55accbe3ull, 0x927c4394206b42b2ull, 93, 0, 22}, - {"Random13", 0, 1, 0xf255fd63895e7badull, 0x41470781ab6bef64ull, 0xb5e34ee7b3268a86ull, 0x57de0e46784b0f54ull, 60, 60, 16}, - {"Random13", 0, 0, 0xf255fd63895e7badull, 0x14650fb0739d0383ull, 0xb5e34ee7b3268a86ull, 0x57de0e46784b0f54ull, 60, 0, 16}, - {"Random13", 1, 1, 0x7abd074c56aca8c6ull, 0x41de5734424c47edull, 0xab774513033e4b33ull, 0xa6590b3d3950e7bbull, 60, 60, 16}, - {"Random13", 1, 0, 0x7abd074c56aca8c6ull, 0x14650fb0739d0383ull, 0xab774513033e4b33ull, 0xa6590b3d3950e7bbull, 60, 0, 16}, - {"Random14", 0, 1, 0x6b59b79be4590a72ull, 0x6d4cbed6a7d3b423ull, 0xbcda0382be163781ull, 0x918405ca8ede2f52ull, 49, 49, 17}, - {"Random14", 0, 0, 0x6b59b79be4590a72ull, 0x14650fb0739d0383ull, 0xbcda0382be163781ull, 0x918405ca8ede2f52ull, 49, 0, 17}, - {"Random14", 1, 1, 0x8dfaddf560dc80caull, 0x13e689009402d1e0ull, 0x8bca41c28a85c67aull, 0x70415e0fc326b13cull, 49, 49, 17}, - {"Random14", 1, 0, 0x8dfaddf560dc80caull, 0x14650fb0739d0383ull, 0x8bca41c28a85c67aull, 0x70415e0fc326b13cull, 49, 0, 17}, + {"Random13", 0, 1, 0x39caae56088c1db8ull, 0x4028b545fb2c3ad9ull, 0x7f720ad1034d2991ull, 0xc19e5f5bbefe0e7full, 60, 60, 16}, + {"Random13", 0, 0, 0x39caae56088c1db8ull, 0x14650fb0739d0383ull, 0x7f720ad1034d2991ull, 0xc19e5f5bbefe0e7full, 60, 0, 16}, + {"Random13", 1, 1, 0x07b714c492b5ae85ull, 0xa03b3f8bed95186dull, 0xfe702e1eb2f469c0ull, 0xa9f1d1a463925b52ull, 60, 60, 16}, + {"Random13", 1, 0, 0x07b714c492b5ae85ull, 0x14650fb0739d0383ull, 0xfe702e1eb2f469c0ull, 0xa9f1d1a463925b52ull, 60, 0, 16}, + {"Random14", 0, 1, 0x21085d70db4c336bull, 0x394ebfbc2c15522aull, 0x4e8d14373274d558ull, 0xad1f71b1afdd37f8ull, 49, 49, 16}, + {"Random14", 0, 0, 0x21085d70db4c336bull, 0x14650fb0739d0383ull, 0x4e8d14373274d558ull, 0xad1f71b1afdd37f8ull, 49, 0, 16}, + {"Random14", 1, 1, 0x5a8dbff598244e8cull, 0x4c48857b5cd25816ull, 0xbec287230f684e1dull, 0x7f45221fb1111aa3ull, 49, 49, 16}, + {"Random14", 1, 0, 0x5a8dbff598244e8cull, 0x14650fb0739d0383ull, 0xbec287230f684e1dull, 0x7f45221fb1111aa3ull, 49, 0, 16}, {"Random15", 0, 1, 0x1fe125a3fb2912dfull, 0x9ef69d4200bfb809ull, 0xa729d04861d8f487ull, 0xf8da94854e79d89eull, 42, 42, 11}, {"Random15", 0, 0, 0x1fe125a3fb2912dfull, 0x14650fb0739d0383ull, 0xa729d04861d8f487ull, 0xf8da94854e79d89eull, 42, 0, 11}, {"Random15", 1, 1, 0x7b17bf5a43044023ull, 0xdeec1e0290d1370dull, 0xeabf56d064294b39ull, 0x01758143c50ac69dull, 42, 42, 11}, @@ -1059,10 +1123,10 @@ constexpr Pinned kPinned[] = { {"Random20", 0, 0, 0x90b20d166e1357e0ull, 0x14650fb0739d0383ull, 0x35a6da11cd35b29cull, 0x3968c24c22199036ull, 70, 0, 21}, {"Random20", 1, 1, 0xa82d4e53d2192a5full, 0x8e1411c055e37af3ull, 0xafad93e39c8617d9ull, 0x5c29f15f02417fcaull, 72, 57, 21}, {"Random20", 1, 0, 0xa82d4e53d2192a5full, 0x14650fb0739d0383ull, 0xafad93e39c8617d9ull, 0x5c29f15f02417fcaull, 72, 0, 21}, - {"Random21", 0, 1, 0xe1da26acd373548aull, 0xad828c8396454565ull, 0x7d29f9137d1bc337ull, 0xd6a6f90f609ca64aull, 54, 54, 18}, - {"Random21", 0, 0, 0xe1da26acd373548aull, 0x14650fb0739d0383ull, 0x7d29f9137d1bc337ull, 0xd6a6f90f609ca64aull, 54, 0, 18}, - {"Random21", 1, 1, 0x1b2388f0f789a7d1ull, 0xc1ca64a0715ca8b4ull, 0x32f61d041209dc84ull, 0x08da0b92c166b77cull, 54, 54, 18}, - {"Random21", 1, 0, 0x1b2388f0f789a7d1ull, 0x14650fb0739d0383ull, 0x32f61d041209dc84ull, 0x08da0b92c166b77cull, 54, 0, 18}, + {"Random21", 0, 1, 0x18e96beb584671fcull, 0x6c5b4e5a67209f94ull, 0x0571605cf9e50eb1ull, 0xca719be0f46f42acull, 54, 54, 18}, + {"Random21", 0, 0, 0x18e96beb584671fcull, 0x14650fb0739d0383ull, 0x0571605cf9e50eb1ull, 0xca719be0f46f42acull, 54, 0, 18}, + {"Random21", 1, 1, 0xd32a038cc5b6ea6eull, 0x9074957bf890f8e6ull, 0x20e23c0060589e5eull, 0xb6387745e72b0075ull, 54, 54, 18}, + {"Random21", 1, 0, 0xd32a038cc5b6ea6eull, 0x14650fb0739d0383ull, 0x20e23c0060589e5eull, 0xb6387745e72b0075ull, 54, 0, 18}, {"Random22", 0, 1, 0xef05a2f01ca0c05cull, 0xb151ebfe9a44ebebull, 0x603c10590ec68778ull, 0x1b1b4c42bccc2191ull, 68, 49, 13}, {"Random22", 0, 0, 0xef05a2f01ca0c05cull, 0x14650fb0739d0383ull, 0x603c10590ec68778ull, 0x1b1b4c42bccc2191ull, 68, 0, 13}, {"Random22", 1, 1, 0x1624d3651edc9089ull, 0x2a2f5457686fdb7full, 0x29259a278cc3600bull, 0xb361721c9b7f57e7ull, 68, 49, 13}, @@ -1075,18 +1139,18 @@ constexpr Pinned kPinned[] = { {"Random24", 0, 0, 0xadbbd26ef9c19430ull, 0x14650fb0739d0383ull, 0xcdafd09a1c076093ull, 0x880c60f0ef5d5a7dull, 60, 0, 16}, {"Random24", 1, 1, 0x91d34f247834f4e9ull, 0xf9fa951efcc4b1caull, 0xe744bb83904b658aull, 0xc06e2e1df8db5231ull, 60, 60, 16}, {"Random24", 1, 0, 0x91d34f247834f4e9ull, 0x14650fb0739d0383ull, 0xe744bb83904b658aull, 0xc06e2e1df8db5231ull, 60, 0, 16}, - {"Random25", 0, 1, 0x873e291d0017c5dcull, 0xd20a941e2361c0dfull, 0x5ff4203ec9ac379eull, 0x7573fe39c01fb659ull, 44, 44, 8}, - {"Random25", 0, 0, 0x873e291d0017c5dcull, 0x14650fb0739d0383ull, 0x5ff4203ec9ac379eull, 0x7573fe39c01fb659ull, 44, 0, 8}, - {"Random25", 1, 1, 0x9006cf963c9d2db4ull, 0x39a1e5e11f193964ull, 0x5ff4203ec9ac379eull, 0xb372cc6219683abdull, 44, 44, 8}, - {"Random25", 1, 0, 0x9006cf963c9d2db4ull, 0x14650fb0739d0383ull, 0x5ff4203ec9ac379eull, 0xb372cc6219683abdull, 44, 0, 8}, - {"Random26", 0, 1, 0x406a1ebe98ed8ee5ull, 0xd559a888703734a3ull, 0xd9522717c794d53bull, 0x758182130fa5a986ull, 42, 42, 12}, - {"Random26", 0, 0, 0x406a1ebe98ed8ee5ull, 0x14650fb0739d0383ull, 0xd9522717c794d53bull, 0x758182130fa5a986ull, 42, 0, 12}, - {"Random26", 1, 1, 0x28ce140c5a29687dull, 0x3d8702945df8ba26ull, 0xf7b8acd382701dd1ull, 0xeeb06d8c2a0c4772ull, 42, 42, 12}, - {"Random26", 1, 0, 0x28ce140c5a29687dull, 0x14650fb0739d0383ull, 0xf7b8acd382701dd1ull, 0xeeb06d8c2a0c4772ull, 42, 0, 12}, - {"Random27", 0, 1, 0x51e3ce5734d5737cull, 0x35105942d4fa267full, 0x3c0165d3836f406aull, 0x030065ad81ba4087ull, 50, 50, 22}, - {"Random27", 0, 0, 0x51e3ce5734d5737cull, 0x14650fb0739d0383ull, 0x3c0165d3836f406aull, 0x030065ad81ba4087ull, 50, 0, 22}, - {"Random27", 1, 1, 0x81fa7403c9f754c1ull, 0x4f43080f0a6c1974ull, 0x97addcfee41a4dd8ull, 0x900de7e225926b9bull, 50, 50, 21}, - {"Random27", 1, 0, 0x81fa7403c9f754c1ull, 0x14650fb0739d0383ull, 0x97addcfee41a4dd8ull, 0x900de7e225926b9bull, 50, 0, 21}, + {"Random25", 0, 1, 0x1091baf336b7e96aull, 0x5836120ce0f3d783ull, 0xf92cddf4fc024c49ull, 0x3f0d68bfc74cf35full, 44, 44, 8}, + {"Random25", 0, 0, 0x1091baf336b7e96aull, 0x14650fb0739d0383ull, 0xf92cddf4fc024c49ull, 0x3f0d68bfc74cf35full, 44, 0, 8}, + {"Random25", 1, 1, 0x4c59eab8929878abull, 0x90bf03c0ed26cd99ull, 0xf92cddf4fc024c49ull, 0xeb9d9c36364972dfull, 44, 44, 8}, + {"Random25", 1, 0, 0x4c59eab8929878abull, 0x14650fb0739d0383ull, 0xf92cddf4fc024c49ull, 0xeb9d9c36364972dfull, 44, 0, 8}, + {"Random26", 0, 1, 0x406a1ebe98ed8ee5ull, 0xc4ceaa2f46b3b1c7ull, 0xd9522717c794d53bull, 0x6d9f1512cb3c63efull, 42, 42, 12}, + {"Random26", 0, 0, 0x406a1ebe98ed8ee5ull, 0x14650fb0739d0383ull, 0xd9522717c794d53bull, 0x6d9f1512cb3c63efull, 42, 0, 12}, + {"Random26", 1, 1, 0x28ce140c5a29687dull, 0x383f34833e879b1bull, 0xf7b8acd382701dd1ull, 0x500ef2279c277ba0ull, 42, 42, 12}, + {"Random26", 1, 0, 0x28ce140c5a29687dull, 0x14650fb0739d0383ull, 0xf7b8acd382701dd1ull, 0x500ef2279c277ba0ull, 42, 0, 12}, + {"Random27", 0, 1, 0xeafcaf72c5794d88ull, 0xfbb77c6f11f0944full, 0x3c0165d3836f406aull, 0xf2d844781c8e9819ull, 50, 50, 22}, + {"Random27", 0, 0, 0xeafcaf72c5794d88ull, 0x14650fb0739d0383ull, 0x3c0165d3836f406aull, 0xf2d844781c8e9819ull, 50, 0, 22}, + {"Random27", 1, 1, 0x433fe5d0340138d8ull, 0x80e409be19d6131dull, 0x97addcfee41a4dd8ull, 0x2a459b69d95400b1ull, 50, 50, 21}, + {"Random27", 1, 0, 0x433fe5d0340138d8ull, 0x14650fb0739d0383ull, 0x97addcfee41a4dd8ull, 0x2a459b69d95400b1ull, 50, 0, 21}, {"Random28", 0, 1, 0xa0bb8c1925814fe3ull, 0x8e8763454e7d2dcaull, 0xc447f3f5058cd81bull, 0xe51c2406011cd87dull, 54, 54, 19}, {"Random28", 0, 0, 0xa0bb8c1925814fe3ull, 0x14650fb0739d0383ull, 0xc447f3f5058cd81bull, 0xe51c2406011cd87dull, 54, 0, 19}, {"Random28", 1, 1, 0xdb2802eb6d8819c7ull, 0x910e834a102ac932ull, 0xee17522a5f3c15dfull, 0x7406a539e21e57ceull, 54, 54, 19}, @@ -1099,14 +1163,14 @@ constexpr Pinned kPinned[] = { {"Random30", 0, 0, 0x585a1a9f34476a29ull, 0x14650fb0739d0383ull, 0x260809f2d3657490ull, 0x8e3b3ec00a1454a2ull, 61, 0, 1}, {"Random30", 1, 1, 0x8de0587cbe97fc0aull, 0x2fc1ebe82b1783eeull, 0x260809f2d3657490ull, 0x7ffde30dc93fc9d7ull, 61, 61, 1}, {"Random30", 1, 0, 0x8de0587cbe97fc0aull, 0x14650fb0739d0383ull, 0x260809f2d3657490ull, 0x7ffde30dc93fc9d7ull, 61, 0, 1}, - {"Random31", 0, 1, 0x3b5f096d5bca7d49ull, 0xd72b574c253a553dull, 0xcad16841aad879ddull, 0x466ccec496cced70ull, 49, 49, 9}, - {"Random31", 0, 0, 0x3b5f096d5bca7d49ull, 0x14650fb0739d0383ull, 0xcad16841aad879ddull, 0x466ccec496cced70ull, 49, 0, 9}, - {"Random31", 1, 1, 0xc14041a700ffb6c1ull, 0xe3f3a394089513b1ull, 0x2569d07bfd939198ull, 0xc8e0343f71af4c85ull, 49, 49, 9}, - {"Random31", 1, 0, 0xc14041a700ffb6c1ull, 0x14650fb0739d0383ull, 0x2569d07bfd939198ull, 0xc8e0343f71af4c85ull, 49, 0, 9}, - {"Random32", 0, 1, 0x7bb420637675ac73ull, 0x7a70d1e9f78179c9ull, 0x4994badb687c8750ull, 0x0e03ccad16dc63fdull, 62, 62, 19}, - {"Random32", 0, 0, 0x7bb420637675ac73ull, 0x14650fb0739d0383ull, 0x4994badb687c8750ull, 0x0e03ccad16dc63fdull, 62, 0, 19}, - {"Random32", 1, 1, 0x8bd26473f6dc3b55ull, 0xeb260eee22e520f0ull, 0xb1a7a66b620c4fd5ull, 0x597150e94c1a4012ull, 62, 62, 19}, - {"Random32", 1, 0, 0x8bd26473f6dc3b55ull, 0x14650fb0739d0383ull, 0xb1a7a66b620c4fd5ull, 0x597150e94c1a4012ull, 62, 0, 19}, + {"Random31", 0, 1, 0x09b545a0067e8c26ull, 0x3c6ae0150bb63629ull, 0xcad16841aad879ddull, 0xfaf6a143c8b0d038ull, 49, 49, 9}, + {"Random31", 0, 0, 0x09b545a0067e8c26ull, 0x14650fb0739d0383ull, 0xcad16841aad879ddull, 0xfaf6a143c8b0d038ull, 49, 0, 9}, + {"Random31", 1, 1, 0xb42bd9cfbe060a73ull, 0x928f7cfde5bdb105ull, 0x2569d07bfd939198ull, 0xf602f077ecb2b7cfull, 49, 49, 9}, + {"Random31", 1, 0, 0xb42bd9cfbe060a73ull, 0x14650fb0739d0383ull, 0x2569d07bfd939198ull, 0xf602f077ecb2b7cfull, 49, 0, 9}, + {"Random32", 0, 1, 0x220cd02053dbc40aull, 0x9afe2ada13350695ull, 0x4994badb687c8750ull, 0xe577534dd3033b52ull, 62, 62, 19}, + {"Random32", 0, 0, 0x220cd02053dbc40aull, 0x14650fb0739d0383ull, 0x4994badb687c8750ull, 0xe577534dd3033b52ull, 62, 0, 19}, + {"Random32", 1, 1, 0xc0ca43094334f008ull, 0xe85eb4136b8c435dull, 0xb1a7a66b620c4fd5ull, 0xba233c9fc46444a4ull, 62, 62, 19}, + {"Random32", 1, 0, 0xc0ca43094334f008ull, 0x14650fb0739d0383ull, 0xb1a7a66b620c4fd5ull, 0xba233c9fc46444a4ull, 62, 0, 19}, {"Random33", 0, 1, 0xbfcfebe96632bd39ull, 0x116bc2f57887ba4bull, 0xf8d13762bc3ab9c0ull, 0xf8a0e855a842d39cull, 51, 51, 13}, {"Random33", 0, 0, 0xbfcfebe96632bd39ull, 0x14650fb0739d0383ull, 0xf8d13762bc3ab9c0ull, 0xf8a0e855a842d39cull, 51, 0, 13}, {"Random33", 1, 1, 0xe17f0353f37c41f1ull, 0x0ba98d74e8b769b7ull, 0xca5862380ffcef2full, 0xb76d3ca699ea4ebdull, 51, 51, 13}, @@ -1119,10 +1183,10 @@ constexpr Pinned kPinned[] = { {"Random35", 0, 0, 0xeb16fb1a6bc80c17ull, 0x14650fb0739d0383ull, 0xa8b1e0f14c6cc49aull, 0xb971c6bc6d7ac182ull, 63, 0, 24}, {"Random35", 1, 1, 0x193ec2cb9a163900ull, 0x9233c78214742aa3ull, 0xb2b344c283516d30ull, 0xa9bdcf22e28567b2ull, 63, 63, 24}, {"Random35", 1, 0, 0x193ec2cb9a163900ull, 0x14650fb0739d0383ull, 0xb2b344c283516d30ull, 0xa9bdcf22e28567b2ull, 63, 0, 24}, - {"Random36", 0, 1, 0x3e3529f5adea5e8aull, 0x16d9ee97193355f5ull, 0x8ee686191c3d0539ull, 0x32a2c44c6f794a8cull, 87, 60, 21}, - {"Random36", 0, 0, 0x3e3529f5adea5e8aull, 0x14650fb0739d0383ull, 0x8ee686191c3d0539ull, 0x32a2c44c6f794a8cull, 87, 0, 21}, - {"Random36", 1, 1, 0x99c3ad89ead2d6e0ull, 0x144bd2f3c5b9c0f8ull, 0x10b01a5c8873f5d5ull, 0xe6c283fd13b0660cull, 87, 60, 21}, - {"Random36", 1, 0, 0x99c3ad89ead2d6e0ull, 0x14650fb0739d0383ull, 0x10b01a5c8873f5d5ull, 0xe6c283fd13b0660cull, 87, 0, 21}, + {"Random36", 0, 1, 0xd4b5588948d4b755ull, 0x6af2dc7dd208ef5dull, 0x24bf064fe8416ffdull, 0x1980e25b047dea78ull, 87, 60, 21}, + {"Random36", 0, 0, 0xd4b5588948d4b755ull, 0x14650fb0739d0383ull, 0x24bf064fe8416ffdull, 0x1980e25b047dea78ull, 87, 0, 21}, + {"Random36", 1, 1, 0xd1532dd9d52ae5f4ull, 0x0e1d68d8d6c4c5e2ull, 0x86d1b68c9a0453a2ull, 0xe93d9e6c724efef6ull, 87, 60, 21}, + {"Random36", 1, 0, 0xd1532dd9d52ae5f4ull, 0x14650fb0739d0383ull, 0x86d1b68c9a0453a2ull, 0xe93d9e6c724efef6ull, 87, 0, 21}, {"Random37", 0, 1, 0x3a89ff92c403a3e7ull, 0xddda8d77310d918full, 0x6b11a4e13499e50dull, 0xff9c74898bf3a72aull, 71, 45, 18}, {"Random37", 0, 0, 0x3a89ff92c403a3e7ull, 0x14650fb0739d0383ull, 0x6b11a4e13499e50dull, 0xff9c74898bf3a72aull, 71, 0, 18}, {"Random37", 1, 1, 0x8a36a09ee42999ecull, 0xf12dd4800e96f492ull, 0x15dfd0d0fb5f649aull, 0x9f14bee9808ee8baull, 70, 45, 17}, @@ -1135,10 +1199,10 @@ constexpr Pinned kPinned[] = { {"Random39", 0, 0, 0x9bcdaebb98702055ull, 0x14650fb0739d0383ull, 0xc193386eb35a8e65ull, 0x3026f01eb73dfcdaull, 44, 0, 19}, {"Random39", 1, 1, 0x5fa98522adc27aafull, 0x50b22f98ba965993ull, 0xeea051f0749b0428ull, 0x123f0c5b09aeae91ull, 44, 44, 19}, {"Random39", 1, 0, 0x5fa98522adc27aafull, 0x14650fb0739d0383ull, 0xeea051f0749b0428ull, 0x123f0c5b09aeae91ull, 44, 0, 19}, - {"Random40", 0, 1, 0x9f1a94ed263d51dfull, 0x6f59acfa20c1f416ull, 0x76e2d6a7f41b161cull, 0x95af814f9835cb65ull, 77, 53, 18}, - {"Random40", 0, 0, 0x9f1a94ed263d51dfull, 0x14650fb0739d0383ull, 0x76e2d6a7f41b161cull, 0x95af814f9835cb65ull, 77, 0, 18}, - {"Random40", 1, 1, 0xf0451d9a693e54e3ull, 0x01721733b1316376ull, 0x9a84d463fe03ec91ull, 0x8fdb15e706d2b994ull, 78, 53, 19}, - {"Random40", 1, 0, 0xf0451d9a693e54e3ull, 0x14650fb0739d0383ull, 0x9a84d463fe03ec91ull, 0x8fdb15e706d2b994ull, 78, 0, 19}, + {"Random40", 0, 1, 0x869959795f89f7afull, 0xe9919376f3542ea7ull, 0x76e2d6a7f41b161cull, 0xd91533e6639e22f9ull, 77, 53, 18}, + {"Random40", 0, 0, 0x869959795f89f7afull, 0x14650fb0739d0383ull, 0x76e2d6a7f41b161cull, 0xd91533e6639e22f9ull, 77, 0, 18}, + {"Random40", 1, 1, 0xf47407130500a9bdull, 0xda9b4928c023ca07ull, 0x9a84d463fe03ec91ull, 0x5a8f1ec5a18f3731ull, 78, 53, 19}, + {"Random40", 1, 0, 0xf47407130500a9bdull, 0x14650fb0739d0383ull, 0x9a84d463fe03ec91ull, 0x5a8f1ec5a18f3731ull, 78, 0, 19}, {"Random41", 0, 1, 0x6509ace183e7d2cdull, 0x957c5244d07600b9ull, 0x4b1ec3cc1bd0909cull, 0xcc1203a69591b134ull, 51, 40, 6}, {"Random41", 0, 0, 0x6509ace183e7d2cdull, 0x14650fb0739d0383ull, 0x4b1ec3cc1bd0909cull, 0xcc1203a69591b134ull, 51, 0, 6}, {"Random41", 1, 1, 0xd2d43ab5fdd2ef0bull, 0xb907bbe1bb2da083ull, 0x4b1ec3cc1bd0909cull, 0xc55d2d003a5bec69ull, 51, 40, 6}, @@ -1147,18 +1211,18 @@ constexpr Pinned kPinned[] = { {"Random42", 0, 0, 0xa0ee848263988a3dull, 0x14650fb0739d0383ull, 0x75b6953551fe9575ull, 0xb124b9f52e8cf9f2ull, 60, 0, 12}, {"Random42", 1, 1, 0x7f6282e75cc72bb0ull, 0x23bb6075acaedb2eull, 0xf736fbadc411c223ull, 0xdb2120d5edab4d9full, 60, 60, 12}, {"Random42", 1, 0, 0x7f6282e75cc72bb0ull, 0x14650fb0739d0383ull, 0xf736fbadc411c223ull, 0xdb2120d5edab4d9full, 60, 0, 12}, - {"Random43", 0, 1, 0x83e2926a6d6dec9cull, 0x784d62a813c85f48ull, 0xbd8ba613082e0da5ull, 0x5d4d3ed83aa4608cull, 63, 63, 14}, - {"Random43", 0, 0, 0x83e2926a6d6dec9cull, 0x14650fb0739d0383ull, 0xbd8ba613082e0da5ull, 0x5d4d3ed83aa4608cull, 63, 0, 14}, - {"Random43", 1, 1, 0xb9a953e47994593full, 0x879899b5598309eeull, 0x572d3f9f9521e7caull, 0xb6e20a498ce1b25aull, 63, 63, 14}, - {"Random43", 1, 0, 0xb9a953e47994593full, 0x14650fb0739d0383ull, 0x572d3f9f9521e7caull, 0xb6e20a498ce1b25aull, 63, 0, 14}, + {"Random43", 0, 1, 0x7fb8967d6d6d24d2ull, 0x98cae92178fe91ebull, 0xbd8ba613082e0da5ull, 0xdc29a310e9c7d06bull, 63, 63, 14}, + {"Random43", 0, 0, 0x7fb8967d6d6d24d2ull, 0x14650fb0739d0383ull, 0xbd8ba613082e0da5ull, 0xdc29a310e9c7d06bull, 63, 0, 14}, + {"Random43", 1, 1, 0xffd1c55ce888ef72ull, 0x8ac3c2c1053a4819ull, 0x572d3f9f9521e7caull, 0x45393018e93b32beull, 63, 63, 14}, + {"Random43", 1, 0, 0xffd1c55ce888ef72ull, 0x14650fb0739d0383ull, 0x572d3f9f9521e7caull, 0x45393018e93b32beull, 63, 0, 14}, {"Random44", 0, 1, 0xdae57d80c4d2a634ull, 0xf7669a1535370da9ull, 0x513c7ae44c865ec8ull, 0xc555f0e88b5eb378ull, 69, 47, 15}, {"Random44", 0, 0, 0xdae57d80c4d2a634ull, 0x14650fb0739d0383ull, 0x513c7ae44c865ec8ull, 0xc555f0e88b5eb378ull, 69, 0, 15}, {"Random44", 1, 1, 0x360b704190f157adull, 0x5b2a75cdabd7235cull, 0x1a79f44215a3981eull, 0xde36f7f290af13a5ull, 69, 47, 15}, {"Random44", 1, 0, 0x360b704190f157adull, 0x14650fb0739d0383ull, 0x1a79f44215a3981eull, 0xde36f7f290af13a5ull, 69, 0, 15}, {"Random45", 0, 1, 0xbf351c50f4a44f8full, 0xfee900af4573bbc1ull, 0xf543670d7cc61817ull, 0x7f2867c87c78723eull, 99, 63, 25}, {"Random45", 0, 0, 0xbf351c50f4a44f8full, 0x14650fb0739d0383ull, 0xf543670d7cc61817ull, 0x7f2867c87c78723eull, 99, 0, 25}, - {"Random45", 1, 1, 0x81ba9360ff25c670ull, 0xe345a7e76585b1deull, 0xa2aef1f38f464b4dull, 0x6c13a376f0a1a840ull, 99, 63, 24}, - {"Random45", 1, 0, 0x81ba9360ff25c670ull, 0x14650fb0739d0383ull, 0xa2aef1f38f464b4dull, 0x6c13a376f0a1a840ull, 99, 0, 24}, + {"Random45", 1, 1, 0x32a1d85e85b93f0aull, 0xebcfcdf78cd848a5ull, 0xa2aef1f38f464b4dull, 0x56b0e295081820c0ull, 99, 63, 24}, + {"Random45", 1, 0, 0x32a1d85e85b93f0aull, 0x14650fb0739d0383ull, 0xa2aef1f38f464b4dull, 0x56b0e295081820c0ull, 99, 0, 24}, {"Random46", 0, 1, 0x3ad713968f2546d9ull, 0x84dd0930114cd829ull, 0x8fe7667934a81f87ull, 0x6bb382cfb976e148ull, 52, 52, 1}, {"Random46", 0, 0, 0x3ad713968f2546d9ull, 0x14650fb0739d0383ull, 0x8fe7667934a81f87ull, 0x6bb382cfb976e148ull, 52, 0, 1}, {"Random46", 1, 1, 0x997f2e0ee9786d91ull, 0x1ce86cd2c755bea3ull, 0x8fe7667934a81f87ull, 0x556134f8585397c9ull, 52, 52, 1}, diff --git a/tests/test_publication_witness.cpp b/tests/test_publication_witness.cpp index 81b5170f..72aaada5 100644 --- a/tests/test_publication_witness.cpp +++ b/tests/test_publication_witness.cpp @@ -65,7 +65,14 @@ // a new digest. Every count is unchanged, and this TU with the three hash // inputs taken out (the host-extension fold, the recorded broker-state hash // rows, the final hash) prints the same table against main 91d65ad6 and the -// lane's library, byte for byte. +// lane's library, byte for byte. Re-harvested once more for R5 lane +// PAR-ORDERS (21 runs): a pending entry's trailing exit is no longer placed +// as an anchored kernel child (17 runs: the host-extension fold, the recorded +// broker-state hash rows and the final hash), and a process_orders_on_close +// add stays under the pyramiding cap when the pending opposite entry is not +// in the close pass (4 runs). With the three hash inputs taken out, this TU +// prints INT25's table byte for byte but for Storm11 and Storm11M, whose +// trades the cap moves. #include #include diff --git a/tests/test_publication_witness_pinned.inc b/tests/test_publication_witness_pinned.inc index bb55d210..a68b95be 100644 --- a/tests/test_publication_witness_pinned.inc +++ b/tests/test_publication_witness_pinned.inc @@ -5,6 +5,29 @@ // once for K-ULP3's whole-lot close and once for V19-FIX's hash step: see // the provenance note in the test. // Generated -- never edit a row by hand. +// R5 lane PAR-ORDERS re-harvested it the same way on its tree (21 of 156 runs move; +// see the lane's hash commit for why each moves): +// ConfigFlags113: digest 0x2a4b7804a0a3e27e -> 0xb0d7186d612e1063 +// ConfigFlags113M: digest 0x88c14690db445933 -> 0x066c20032094d9bf +// Storm01: digest 0x75c523aefe67f41a -> 0x2940f65451a3189f +// Storm01M: digest 0x5ea8235c86419d28 -> 0xbfc8780fbe24d51c +// Storm06: digest 0x52fe595453aed8f6 -> 0x4053ebf00e230b53 +// Storm06M: digest 0x570cecdbe9d2d0c0 -> 0x4d4c9f8c75e1901b +// Storm09: digest 0xe0004089dbf5f4ae -> 0x09195d4f14ec28dd +// Storm09M: digest 0x977b07e1c905757a -> 0x5a6cd7deec2033ff +// Storm10: digest 0xe7a0f75a29b2bd10 -> 0x7ae3261a9cbdeff3 +// Storm10M: digest 0xf3739b7278c799f2 -> 0x03595faccb3309cd +// Storm11: digest 0x144d68a22896e3bf -> 0xf46684d86365208a, trades 24 -> 22 +// Storm11M: digest 0xcbb7769e3a9bf7c6 -> 0xb5205907e08e01bf, trades 24 -> 22 +// Storm13: digest 0x327d8e77ac1de02c -> 0xde3f322bf0c59a98 +// Storm13M: digest 0x6d1a8b0e3f3199b3 -> 0xf7d902eba9385f60 +// Storm17M: digest 0x00de1819d109b846 -> 0x46e2d77c04f174e3 +// Storm18: digest 0xcb801349463a0b9a -> 0xe7e874c21bf9c5fc +// Storm18M: digest 0xc661ff254ed1d0b0 -> 0x75b39ccdb9b4542d +// Storm19: digest 0x33d7bba6e955b14a -> 0x8c49334fb012a382 +// Storm19M: digest 0x6e51f913b73007d5 -> 0x83ea12d967fea7e7 +// Storm22: digest 0xce56d5badb05638c -> 0xda5de455f464504d +// Storm22M: digest 0xa8b971a9116eb495 -> 0xfdaa191a97b50614 constexpr Pinned kPinned[] = { {"Config01", 0x13599d44631228e2ull, 144, 44, 144}, {"ConfigFlags101", 0x9d75aa3c41307c6aull, 145, 45, 144}, @@ -55,9 +78,9 @@ constexpr Pinned kPinned[] = { {"Config12M", 0xd27a9a9ee92fb498ull, 144, 48, 144}, {"ConfigFlags112M", 0xa9764e2ca9500901ull, 144, 18, 144}, {"Config13", 0x5c4aa53ee5c2d4b9ull, 48, 19, 48}, - {"ConfigFlags113", 0x2a4b7804a0a3e27eull, 48, 19, 48}, + {"ConfigFlags113", 0xb0d7186d612e1063ull, 48, 19, 48}, {"Config13M", 0x5ba51d9728c119b8ull, 48, 19, 48}, - {"ConfigFlags113M", 0x88c14690db445933ull, 48, 19, 48}, + {"ConfigFlags113M", 0x066c20032094d9bfull, 48, 19, 48}, {"Config14", 0x6e95423cd9f2042aull, 144, 40, 144}, {"ConfigFlags114", 0x96a34f2bb0dc2df1ull, 144, 60, 144}, {"Config14M", 0x3c6809a3326959b5ull, 144, 40, 144}, @@ -80,8 +103,8 @@ constexpr Pinned kPinned[] = { {"CloseOrderAddsM", 0x72762eb6d5e1230cull, 36, 22, 36}, {"DelayedStops", 0xa655cdf979a2d473ull, 66, 20, 40}, {"DelayedStopsM", 0x1d34635f60c4dbdbull, 66, 20, 40}, - {"Storm01", 0x75c523aefe67f41aull, 43, 13, 43}, - {"Storm01M", 0x5ea8235c86419d28ull, 43, 14, 43}, + {"Storm01", 0x2940f65451a3189full, 43, 13, 43}, + {"Storm01M", 0xbfc8780fbe24d51cull, 43, 14, 43}, {"Storm02", 0x951345d2c3fa83a0ull, 63, 12, 42}, {"Storm02M", 0x1521264d78122ebeull, 63, 12, 42}, {"Storm03", 0x25f90b9eeb5f057aull, 41, 17, 41}, @@ -90,22 +113,22 @@ constexpr Pinned kPinned[] = { {"Storm04M", 0xa1dd548b8f00457eull, 54, 20, 54}, {"Storm05", 0x8eacccdcac89765cull, 81, 17, 53}, {"Storm05M", 0x67d9decf4dd40cb4ull, 82, 17, 53}, - {"Storm06", 0x52fe595453aed8f6ull, 40, 11, 40}, - {"Storm06M", 0x570cecdbe9d2d0c0ull, 40, 11, 40}, + {"Storm06", 0x4053ebf00e230b53ull, 40, 11, 40}, + {"Storm06M", 0x4d4c9f8c75e1901bull, 40, 11, 40}, {"Storm07", 0xf7e280a190e60133ull, 55, 15, 55}, {"Storm07M", 0x3b29ea97a5b2dd0aull, 55, 15, 55}, {"Storm08", 0x9302e1c04c2dfedfull, 81, 14, 58}, {"Storm08M", 0x8944facb400c0524ull, 81, 14, 58}, - {"Storm09", 0xe0004089dbf5f4aeull, 48, 21, 48}, - {"Storm09M", 0x977b07e1c905757aull, 48, 20, 48}, - {"Storm10", 0xe7a0f75a29b2bd10ull, 65, 16, 57}, - {"Storm10M", 0xf3739b7278c799f2ull, 66, 16, 57}, - {"Storm11", 0x144d68a22896e3bfull, 59, 24, 59}, - {"Storm11M", 0xcbb7769e3a9bf7c6ull, 59, 24, 59}, + {"Storm09", 0x09195d4f14ec28ddull, 48, 21, 48}, + {"Storm09M", 0x5a6cd7deec2033ffull, 48, 20, 48}, + {"Storm10", 0x7ae3261a9cbdeff3ull, 65, 16, 57}, + {"Storm10M", 0x03595faccb3309cdull, 66, 16, 57}, + {"Storm11", 0xf46684d86365208aull, 59, 22, 59}, + {"Storm11M", 0xb5205907e08e01bfull, 59, 22, 59}, {"Storm12", 0x8b151167a2310837ull, 86, 20, 56}, {"Storm12M", 0xc49a83bf5709682bull, 86, 20, 56}, - {"Storm13", 0x327d8e77ac1de02cull, 46, 23, 46}, - {"Storm13M", 0x6d1a8b0e3f3199b3ull, 46, 23, 46}, + {"Storm13", 0xde3f322bf0c59a98ull, 46, 23, 46}, + {"Storm13M", 0xf7d902eba9385f60ull, 46, 23, 46}, {"Storm14", 0x1ac3a18ac2e4ed4bull, 46, 18, 46}, {"Storm14M", 0xd88e046854060dd6ull, 46, 18, 46}, {"Storm15", 0x6229ca73e971d1c1ull, 43, 13, 43}, @@ -113,17 +136,17 @@ constexpr Pinned kPinned[] = { {"Storm16", 0x8918c41ad3b998bdull, 58, 18, 58}, {"Storm16M", 0x6b216cf99a1880dfull, 58, 18, 58}, {"Storm17", 0x644e0ad8e5fd4833ull, 82, 21, 54}, - {"Storm17M", 0x00de1819d109b846ull, 77, 17, 54}, - {"Storm18", 0xcb801349463a0b9aull, 80, 25, 49}, - {"Storm18M", 0xc661ff254ed1d0b0ull, 80, 25, 49}, - {"Storm19", 0x33d7bba6e955b14aull, 86, 22, 49}, - {"Storm19M", 0x6e51f913b73007d5ull, 86, 22, 49}, + {"Storm17M", 0x46e2d77c04f174e3ull, 77, 17, 54}, + {"Storm18", 0xe7e874c21bf9c5fcull, 80, 25, 49}, + {"Storm18M", 0x75b39ccdb9b4542dull, 80, 25, 49}, + {"Storm19", 0x8c49334fb012a382ull, 86, 22, 49}, + {"Storm19M", 0x83ea12d967fea7e7ull, 86, 22, 49}, {"Storm20", 0xe93eebdfc01f57b4ull, 45, 12, 42}, {"Storm20M", 0xfdb5a657600e2375ull, 45, 12, 42}, {"Storm21", 0xc0aad15a28128003ull, 47, 14, 47}, {"Storm21M", 0xbc3fbaacf5272ad1ull, 47, 14, 47}, - {"Storm22", 0xce56d5badb05638cull, 76, 19, 50}, - {"Storm22M", 0xa8b971a9116eb495ull, 76, 19, 50}, + {"Storm22", 0xda5de455f464504dull, 76, 19, 50}, + {"Storm22M", 0xfdaa191a97b50614ull, 76, 19, 50}, {"Storm23", 0xb91e295d6da6778cull, 89, 27, 54}, {"Storm23M", 0x78f7d0134456384full, 89, 27, 54}, {"Storm24", 0x6f40ccb0266ca278ull, 54, 25, 54}, From 7c47d880c020e70cdbf24d103faa7d9c01d6b9e6 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:19:06 +0800 Subject: [PATCH 32/83] Kernel: a current execution refuses a request bound to a host roster instead of failing the run (R5 lane PAR-ORDERS-2, item 1) validate_current_execution admitted a BindCohort owner, but the evaluation's current shape (native_order.cpp current_shape) executes only Independent, BindOpening and BindOpenings: a cohort close whose roster held a live lot was validated, left unevaluated, and apply_evaluation threw "native current evaluated allowance mismatch", ending the run (live-state reversals seed 3246599 on the base; 1780393 once 6d applies). An empty roster answered UnreadyOwner. Such a request is now refused as UnsupportedRequest before its point is taken, and fills at the next match like any queued close. ADR-0001 rule 2: (a) one legality rule, the evaluation's own, decides a command before any point is taken, for every host and for the C surface; a roster read at the match is not a membership fixed at the command. (b) No spec field, hook or TradingView knob: an existing enumerator, no ABI change. tests/test_native_current_cohort_refusal.cpp (kernel-only; fail-before on the base: the run fails with the allowance mismatch). Floors: kernel 278, release 679. The live-state battery's pinned abort for 3246599 flips in the lane's hash re-pin commit. Pinned rows moved: tests/test_native_cohort_sliced_close.cpp's four preview rows (R5 lane K-COHORT) -- expectation corrected: the roster close's read-only preview answered the selection's settlement -> it is refused as UnsupportedRequest, because it previewed an execution that failed the run; the same selection's preview still settles (Applied, 3 / 4 rows). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit af7618d0f36d0501a447ba543aba77261b6524a3) INT26 pick note: picked right after PAR-ORDERS (the late-picks list; its base is PAR-ORDERS' 6945fc19). The docs conflicts were anchor digits only, merged by carrying this branch's numbers through the lane's line moves; ADR-0001's new rule-2 row sits after execute_current's, above H-DOCGATES' four comment-inventory rows, so "the last four rows" still names those; the floors are summed: kernel 282 -> 283, release 694 -> 695. The native-engine.md sentence it extends with the roster refusal, "Priced, budgeted and waiting requests retain queued semantics and receive an explicit current-execution refusal.", was written with the selected native closes (f736676). --- CONTRIBUTING.md | 4 +- docs/adr/0001-kernel-adapter-boundary.md | 7 +- docs/design/native-feature-parity.md | 44 ++-- docs/pages/contributing-llm.md | 6 +- docs/pages/native-engine.md | 16 +- docs/pages/pine-to-native.md | 2 +- scripts/ci_verify.py | 10 +- src/native_execution_consumer.cpp | 11 +- tests/CMakeLists.txt | 4 + tests/test_native_cohort_sliced_close.cpp | 32 ++- tests/test_native_current_cohort_refusal.cpp | 240 +++++++++++++++++++ 11 files changed, 313 insertions(+), 63 deletions(-) create mode 100644 tests/test_native_current_cohort_refusal.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed10e3b8..8e9df4c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -152,8 +152,8 @@ What each gate refuses: ### The floors `ci_verify.py` counts the CTest rows that actually **ran** and fails below a -floor — `KERNEL_MIN_TESTS` ci_verify.py:252 and `RELEASE_MIN_TESTS` -ci_verify.py:381. +floor — `KERNEL_MIN_TESTS` ci_verify.py:243 and `RELEASE_MIN_TESTS` +ci_verify.py:356. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 2bb70d1e..c5c8a0d4 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -77,7 +77,7 @@ summary of it. (`native_host.hpp:881`); also `on_native_input` (`native_host.hpp:863`), `on_native_tick` (`native_host.hpp:866`), `on_native_timeframe_bar` (`native_host.hpp:873`), `on_native_bar_open` — at the modeled opening, before its matching pass - (`native_host.hpp:877`, `on_native_bar_open` `native_execution_consumer.cpp:7062-7064`) — + (`native_host.hpp:877`, `on_native_bar_open` `native_execution_consumer.cpp:7065-7067`) — `on_native_recalculate` for every calculation of the run (`native_host.hpp:901`), `on_native_sub_bar` (`native_host.hpp:916`) and post-fill `on_native_applied` (`native_host.hpp:926`). `on_bar` is `final` (`native_host.hpp:845`). @@ -697,7 +697,7 @@ number names its symbol on this tree. | `strategy_configure_native_v1` (`c_abi.cpp:896`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:3979`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | | `strategy_configure_native_fx_curve_v1` (`pineforge.h:584`) | **typed additive route** | `strategy_configure_native_fx_curve_ext_v1` writes the C twin of `NativeFxCurveValidation` and its offending index. The old `-1` route remains unchanged, while a C caller can now distinguish a decreasing timestamp, non-positive rate, allocation failure or wrong phase. | | `on_native_timeframe_bar` (`native_host.hpp:873`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:797`) | **query routed** | The callback signature stays frozen. `strategy_native_timeframe_bar_interval_v1` is valid only during that callback and copies the kernel calendar interval without deriving it from the delivered bar or delivery timestamp. | -| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7626-7638`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1085-1090`) sees the same values, so no hash contract moves. | +| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7644-7656`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1085-1090`) sees the same values, so no hash contract moves. | | `pf_native_lot_excursion_v1` (`native_c_api.h:1871`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | ## TradingView-calibrated kernel mechanisms (rule 2's own account) @@ -755,7 +755,7 @@ The table records, at the tree the doc wave read, what each block described and | `ta::ATR` `include/pineforge/ta.hpp:321` (the `TradingView` comment `sha256:4f6f6939aec2773684b114a13dd002eb58990d6d96c3056ad87361753faf7a0a` :333-340) | average true range over Wilder's `RMA` | Wilder's ATR is the textbook definition; the comment records which convention and the tape that pinned the recompute behaviour | a seeding/averaging field on an indicator whose definition is the convention | `tests/test_ta_rma_warmup.cpp`, `tests/test_recompute.cpp` | | `ta::StdDev` `include/pineforge/ta.hpp:430` (the `TradingView` comment `sha256:7cc1bbcd32dfe673285d51a8ca7080890ac551858099b523480d998f46408666` :420-429) | the population (biased) standard deviation | biased vs sample is a textbook choice, and the kernel states which | a per-instance switch is the *right* answer where two conventions genuinely compete, which is why `ta::EmaSeeding` exists and this one does not need it | `tests/test_ta_indicators_extras.cpp` | | `float_band_eq` `include/pineforge/ta_compare_band.hpp:38` | an absolute 1e-10 float comparison for indicator equality | an epsilon band is needed by any float comparison; the width is the calibration | a band-width field on a comparison the indicators share | `tests/test_dmi_parity.cpp`, `tests/test_ta_osc_edge.cpp` | -| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8571-8574` (lookahead) and `clear_if_gapped` `:8565-8568` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | +| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8605-8608` (lookahead) and `clear_if_gapped` `:8599-8602` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | | `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2230`, `::gaps` `include/pineforge/native_c_api.h:2234` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:795`, `pf_native_gaps_e` `:806`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | | the authoritative-feed partition `src/engine_aux_security.cpp` | a feed is the venue's own bars of one timeframe; its stamps are the period partition | already ruled: design §2.ii row s, and the residual table's own row | the policy knob is installing the feed or not | `tests/test_native_htf_subscriptions.cpp`, `tests/test_native_security_feed.cpp` | | `session_template_knows_early_close` `src/engine_security.cpp` | the instrument-class classification | already ruled: design §2.ii row t; `SymInfo::type`'s vocabulary is fixed by the frozen C ABI | — | `tests/test_native_wm_buckets.cpp` | @@ -773,6 +773,7 @@ The table records, at the tree the doc wave read, what each block described and | `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7856-7872` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | | `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7833-7854` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | | `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6601` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | +| `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:6342` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | | the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:334`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1541`, `:1556`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:1854`). A bare host never calls the builder, so its calendar stays nominal | `tests/test_o_close_pct_day_anchor.cpp` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | | the bar-time accessors -- `_decompose_bar_time` and `_bar_hour` through `_bar_weekofyear` `include/pineforge/engine.hpp:1066-1093` | the presented bar's stamp broken into calendar fields in UTC, memoized per stamp | a function of the stamp alone, and UTC is the engine's storage timezone | none: they read no spec field and fold into no digest. They are protected `BacktestEngine` members, part of the codegen contract. Their comment claimed that generated code reads them for Pine's bare `hour` / `minute` / `dayofweek`. It does not: the codegen transpiler emits `pine_hour(current_bar_.timestamp, syminfo_.timezone)` and its siblings on the exchange timezone (`pineforge_codegen/codegen/tables.py` at the corpus's pinned commit `f3285d79`), which are the language calendar functions above. No generated strategy in `corpus/` names a `_bar_*` accessor. The comment now says so (R5 lane H-DOCGATES) | `tests/test_chart_timezone.cpp` (`_bar_hour()` is the UTC hour) and `tests/test_intraday_rollover_chart_tz_l4a.cpp` | | the conventional offset spellings of `timezone_accepted` `include/pineforge/native_calendar.hpp:201` (its comment's "TV sign" line) | `UTC+05:30` and `GMT-4` mean what they say: five and a half hours ahead of UTC, four behind (ISO 8601's sign), where a POSIX `TZ` string of the same shape means the opposite; a POSIX rule string keeps POSIX's own sign | a zone spelling is data, and the sign is the one a venue printing `UTC+8` means; nothing branches on a platform | a sign-convention switch over a spelling whose meaning is fixed | `tests/test_native_calendar.cpp` (`resolve_civil("UTC+05:30", …)` and `resolve_civil("GMT-4", …)` against their fixed instants) | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 6da9d62d..b50e2dff 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -59,7 +59,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:13046; lowering `submit_one` pine_adapter.cpp:9070-9082, `BindCohort` pine_adapter.cpp:9571-9577, `submit_leg` pine_adapter.cpp:9595-9619; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | | OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8675-8678; `materialize_relative_exits` pine_adapter.cpp:17397-17438, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19158-19187) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10910-10934), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15673, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15907) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:651-653 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL13 | *(quirk)* Same-bar command batching and the seven deferred queues (`pending_bracket_legs_` pine_adapter.hpp:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10910-10934), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15673, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15907) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:684-686 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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 @@ -116,13 +116,13 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:881, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2318-2319) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7505-7509 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16160) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7062-7066; `execute_current` legal native_host.hpp:1092 | 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:7009 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7060 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1061 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. | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2318-2319) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7523-7527 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16160) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7065-7069; `execute_current` legal native_host.hpp:1092 | 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:7008 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7059 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1061 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:863, `NativeInputContext` native_host.hpp:761-766 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16303, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6916, native_execution_consumer.cpp:6895-6925; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | -| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:866 fires per accepted realtime print native_execution_consumer.cpp:7144-7148; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | -| 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:7499-7616 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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 | — | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16303, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6919, native_execution_consumer.cpp:6919-6949; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:866 fires per accepted realtime print native_execution_consumer.cpp:7147-7151; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | +| 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:7517-7634 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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:2448-2504) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:234-320) 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:5757), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1659-1666 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | @@ -132,11 +132,11 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1484-1487, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1392-1548; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1485 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1124 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1107 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | -| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI `strategy_set_native_security_feed` c_abi.cpp:839) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1429-1445 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1517; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1447-1452 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:487 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8276 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:46-56). | +| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI `strategy_set_native_security_feed` c_abi.cpp:839) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1429-1445 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1517; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1447-1452 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:487 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8275 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:46-56). | | HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:637) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:916 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | | HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:21-220, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:241-413; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:514) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:255-336) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | -| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:863; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7856-7873, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7863, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | +| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:863; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7876-7893, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7883, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | ### 1.7 Fill price, slippage, commission, FX @@ -148,7 +148,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:12004-12013, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:1190 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:8671). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | +| FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1190 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:8705). **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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9062-9067, `exit_limit_trigger` pine_adapter.cpp:9596-9617), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11713) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -161,8 +161,8 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:219; `fill_metrics_section` engine_report.cpp:119-139 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | | RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1684) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1409) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | -| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9236, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | -| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1358, native_execution_consumer.cpp:10622; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | +| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9279, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | +| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1358, native_execution_consumer.cpp:10681; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | | RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:538; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1931 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:411 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:405 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:411 is its deprecated spelling, which the generic default forwards to (`tests/test_native_host_hash_extension.cpp`). | K | L2 (the virtual rides the shared bump) | F:I3 O:J1 S:X3 | — | @@ -176,7 +176,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OT4 | Cooperative abort | **yes** — `request_abort` engine.hpp:2158, `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:896, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:940, probe `strategy_execution_contract` c_abi.cpp:873, stream `strategy_stream_begin` c_abi.cpp:624 **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:548 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:907 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 58 runtime exports (and eight generated per-strategy exports) (`tests/test_native_c_api.c`, `tests/test_native_c_api_frozen_header.cpp`). | K | L13 | F:J6 O:J5 | R5-7: post-v1. S covers it in its §2.3 without a row. | -| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1454 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7266), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1765 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:857; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | +| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1454 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7267), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1765 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:857; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | | OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:288-290) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | --- @@ -204,9 +204,9 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---| | a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:82-91) and of the rich `BacktestEngine::run` overload (engine.hpp:1780-1785, engine_consumer.cpp:81-93). The kernel only forwards it as `NativeBeginArgs::overrides_opaque` native_host.hpp:746; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:292-295 | 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) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:29 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:356-359 (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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8732`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2308-2309. 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:260-261. **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:741) | 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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8766`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2308-2309. 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:260-261. **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:741) | 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:454-460) | 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:6421 | NEUTRAL | Reuse as the reference arithmetic of the price-grid and sizing lanes, delete the rest | O (single-source) | +| e | Dead helpers in the public kernel header | **Done.** All four are gone from `engine.hpp`: `round_to_mintick_directional` and `apply_slippage` were the pre-R5 spelling of the directional snap and the slippage step and the kernel now fills on `native_matching::grid_round_directional` / `native_matching::apply_slippage` src/native_matching.hpp:259 (the engine.hpp comment that named them went with R5 lane E10's residue sweep); `apply_limit_fill` and `reserve_percent_commission` are deleted outright, and only the adapter still spells the first two names, in its own comments. `round_to_mintick` engine.hpp:753 and `bar_fill_price` engine.hpp:771 stay, live through the call `bar_fill_price` native_execution_consumer.cpp:6420 | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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) | @@ -281,8 +281,8 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the | | F (its §2.iv A) | O (its G10) | S (its P7) | Chosen | |---|---|---|---|---| | API shape | spec `subscriptions` + push callback `on_native_timeframe_bar` | `register_native_series` + pull `native_series_bar(id)` / `native_series_slot_is_new(id)` | spec `security_requests` + `configure_native_security_feed` + `native_security(id)` view | F's subscription in the spec + push callback, plus O's pull accessor | -| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:656-671, `contribute_input` native_execution_consumer.cpp:7980, `seal_script` native_execution_consumer.cpp:7957) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | -| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7094), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | +| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:656-671, `contribute_input` native_execution_consumer.cpp:8012, `seal_script` native_execution_consumer.cpp:7977) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | +| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7097), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | | Authoritative bars | `authoritative_bars` on the subscription | `Source::ExchangeFeed` through `set_native_security_feed` | `configure_native_security_feed(id, bars, tf)` | F's field, installed through the existing store | | Lookahead | none, by construction | `lookahead = false` default, available | `lookahead_on = false`, `gaps_on = true` | open → §4 U1 | @@ -298,7 +298,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2945-3013) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2949-3017) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:45, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -341,7 +341,7 @@ the campaign's own anchor audit found that the first thing to rot: | the request-epoch sentence | `native_order_v4` | `native_order_v7` native_order.hpp:25 | | the two host-epoch sentences | host epoch v16 | `engine_script_run_v19` native_host.hpp:20 | | "What still requires Pine compatibility to build" | `BacktestEngine` still holds `CapAttachment` / `OrderPriority` / `IntradayCap` | `engine.hpp` has zero references to them; the cap type is `CapAttachment` intraday_cap.hpp:18 and the constructor is `PineStrategyHost`'s pine_strategy_host.hpp:244 | -| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9106-9146 shows no such refusal (still unpinned by a test; the page says so) | +| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9149-9189 shows no such refusal (still unpinned by a test; the page says so) | | added | — | the self-aggregation recipe beside the subscriptions it is an alternative to; the open-bar lookahead warning with `current_partial_bar` native_host.hpp:1061; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -1343,7 +1343,7 @@ virtual void hash_host_extension(BrokerStateHashSink&) const {} // F (S: hash_ NativeMetricsView native_metrics() const; // S: net_profit, equity, closed_trades, open_lots; non-virtual ``` -- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` + `record_equity_point(script_open_ms)` (engine.hpp:1380, engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7188, native_execution_consumer.cpp:7188, native_execution_consumer.cpp:7188) and after the `AfterCalculation` close; appends the per-bar broker hash when recording is on; synthesizes the open-position row at run end / `stream_end` with the existing `build_close_trade_with_costs` (engine_orders.cpp:70). +- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` + `record_equity_point(script_open_ms)` (engine.hpp:1380, engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7191, native_execution_consumer.cpp:7191, native_execution_consumer.cpp:7191) 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:2357), 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:1011). 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). @@ -1419,9 +1419,9 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1624-1625) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6978-6990) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1651-1652) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6981-6993) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). 2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5616-5620) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1659-1666) is calibrated against it. -3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6835-6842). `on_native_tick` stays the observation hook (S). +3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6834-6841). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2553); valid in bar-open, applied and tick callbacks. 5. The kernel never attempts language-state rollback; that stays Pine's (`snapshot / restore / commit_coof_script_state`, pine_scheduler_native.cpp:52-104) (O). diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 128270a4..9ea28fcf 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -117,8 +117,8 @@ finding to report, not a step to take. `scripts/check_doc_reverts.py`. 10. **A test row never silently disappears.** Each profile counts the rows that - *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:252 and - `RELEASE_MIN_TESTS` ci_verify.py:381. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:243 and + `RELEASE_MIN_TESTS` ci_verify.py:356. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -264,7 +264,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:252, `RELEASE_MIN_TESTS` ci_verify.py:381). It +(`KERNEL_MIN_TESTS` ci_verify.py:243, `RELEASE_MIN_TESTS` ci_verify.py:356). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 7258bf8d..a2286552 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -462,7 +462,7 @@ spec fields. The rich `run(bars, n, input_tf, script_tf, inputs, syminfo, overrides, …)` overload (`engine.hpp:1623-1634`) is **not** refused as a source mutation: it reaches `NativeExecutionConsumer::run_rich` -(`native_execution_consumer.cpp:9106-9146`), which admits the begin, checks the +(`native_execution_consumer.cpp:9149-9189`), which admits the begin, checks the timeframe arguments against the spec, preflights and pumps the batch exactly like the plain overload. `inputs` / `syminfo` / `overrides` are carried only as `NativeBeginArgs` fields to `prepare_native_begin` — the overrides as the @@ -571,7 +571,11 @@ or prepared financial plan. Current execution supports Market with ImmediateRemaining, resolved units, Independent/BindOpening/BindOpenings ownership and existing groups; current Transact remains Independent. Priced, budgeted and waiting requests retain queued semantics and receive an -explicit current-execution refusal. Only the chosen target is consumed. +explicit current-execution refusal, and so does a request bound to a host +roster (`BindCohort`), whose membership is read at the match: it is refused as +`UnsupportedRequest` and fills at the next match like any queued close +(`tests/test_native_current_cohort_refusal.cpp`). Only the chosen target is +consumed. `current_execution_point()` returns an owning presentation of the active callback's quote and calendar-derived decision context. AsPresented uses that @@ -630,9 +634,9 @@ search then sees only the unconsumed suffix (`born_on_remaining_path`, segment, and discrete points, keep the ordinary birth gate above. `on_native_bar_open` fires at the modeled opening, before that point's matching -pass (`native_execution_consumer.cpp:7062-7064`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:7065-7067`). **Lookahead warning:** the `Bar` it receives is the *complete* script bar — the consumer has already set -`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6954`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6957`), the complete bar unless the spec asks for `NativeOpenBarView::OpenOnly` — so its high, low and close are the finished bar's, not what is known at the open. A host that must decide on open-only information reads @@ -2038,7 +2042,7 @@ default, set while no run is active — because each row is a full the live state, not the run's length: the closed rows enter through a running digest). With the switch on, one row follows each point, after the extremes that point just folded -(`record_script_report_point`, `native_execution_consumer.cpp:7783`), so +(`record_script_report_point`, `native_execution_consumer.cpp:7801`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -2849,7 +2853,7 @@ Only completed buckets are published, so this recipe has no lookahead by construction. It is the same class the kernel's own subscription evaluator aggregates with, and the one the kernel's `script_bucket_completions` query feeds when the Pine scheduler asks how its input span buckets -(`TimeframeAggregator` `native_execution_consumer.cpp:7863`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:7883`). What it does **not** give you is what a declared subscription does: an `authoritative_bars` feed, the `gaps` and `lookahead` delivery rules, the lazy-seal chronology, a C spelling, and the diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 9515f248..af2b92cb 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -127,7 +127,7 @@ rather than defaulted. | `commission_type` | `NativeFeeKind` native_run_spec.hpp:19 | `fee_kind` pineforge.h:540 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | | `commission_value` | `fee_value` native_run_spec.hpp:589 | `fee_value` pineforge.h:540 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | | `slippage` | `slippage_ticks` native_run_spec.hpp:583 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3950-3951. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7505-7506, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7523-7524, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | | `pyramiding` | `max_open_lots` native_run_spec.hpp:595 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:2914). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). | | `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:344 with `size_basis` native_c_api.h:2106 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | | `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2110 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 588e92a3..98c298ec 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -249,7 +249,10 @@ # 282 = those 281 plus the one source-free TU of R5 lane K-OCA-KEEP # (test_native_group_keep_handle; INT26 pick, summed). It registers in release # too. -KERNEL_MIN_TESTS = 282 +# 283 = those 282 plus R5 lane PAR-ORDERS-2's source-free +# test_native_current_cohort_refusal (it registers in release too; INT26 +# pick, summed). +KERNEL_MIN_TESTS = 283 # Release-row floor, the same gate for the default profile. Before lane P7 # only the kernel profile had one, so a row that left release alone (a # source-bound TU dropped from TEST_SOURCES, a deleted twin or ABI row) left a @@ -378,7 +381,10 @@ # 694 = those 693 plus the one R5 lane K-OCA-KEEP row KERNEL_MIN_TESTS lists # above; the C replace takes no options, so the lane adds no C row (INT26 # pick, summed). -RELEASE_MIN_TESTS = 694 +# 695 = those 694 plus R5 lane PAR-ORDERS-2's +# test_native_current_cohort_refusal (see KERNEL_MIN_TESTS; INT26 pick, +# summed). +RELEASE_MIN_TESTS = 695 # R5 lane H-DOCGATES (AUDIT4-opus perf N-7) raises both floors by one more: # test_native_runtime_ambient_lifo, the runtime blocks' LIFO death row, is # source-free, so it registers and runs in the kernel profile (279 -> 280) and diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index 5504e8f7..13ab839d 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -6389,10 +6389,13 @@ std::optional NativeExecutionConsumer::validate_current_ex } else if (!std::holds_alternative(request.intent)) { return Refusal::UnsupportedRequest; } + // A BindCohort roster is read at the match, never fixed at the command: + // the evaluation's current shape (native_order.cpp current_shape) admits + // no such owner, so a cohort close is refused here, before its point is + // taken, rather than left unevaluated there (R5 lane PAR-ORDERS-2). if (!std::holds_alternative(request.owner) && !std::holds_alternative(request.owner) - && !std::holds_alternative(request.owner) - && !std::holds_alternative(request.owner)) + && !std::holds_alternative(request.owner)) return Refusal::UnsupportedRequest; const auto target = read_target(engine, live); if (std::holds_alternative(live->authority)) { @@ -6402,10 +6405,6 @@ std::optional NativeExecutionConsumer::validate_current_ex bool any_live = false; for (const auto& row : target.openings) any_live |= row.has_live_matching_lot; if (!any_live) return Refusal::UnreadyOwner; - } else if (std::holds_alternative(live->authority)) { - bool any_live = false; - for (const auto& row : target.openings) any_live |= row.has_live_matching_lot; - if (!any_live) return Refusal::UnreadyOwner; } return std::nullopt; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 88b15f2b..9cf407f1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1362,6 +1362,10 @@ list(APPEND TEST_SOURCES test_aggregated_entry_bar_index_tape) list(APPEND TEST_SOURCES test_magnified_aggregated_tape) # G2-23 every condition that keeps request.security on the source evaluator. list(APPEND TEST_SOURCES test_adapter_security_route_conditions) +# R5 lane PAR-ORDERS-2: a current execution refuses a request bound to a host +# roster (BindCohort) instead of failing the run. Source-free, so it also runs +# in the kernel-only profile. +list(APPEND TEST_SOURCES test_native_current_cohort_refusal) # R4-D L4b-fix restores every removed ShortSeed, affordability, sizing and # deferred-close oracle as an individually compiled native-route twin. Keep diff --git a/tests/test_native_cohort_sliced_close.cpp b/tests/test_native_cohort_sliced_close.cpp index 02e47ce1..3444177e 100644 --- a/tests/test_native_cohort_sliced_close.cpp +++ b/tests/test_native_cohort_sliced_close.cpp @@ -23,8 +23,9 @@ // entry filled whole in one slice (the control, which passed before the fix), // in three slices, or in two with the third still working at the close; no // sibling, an OCA pair of roster closes under either group effect, or a -// bracket on the second entry. The host's terms facts and a read-only preview -// of the same close name each member once. Source-free: the kernel-only +// bracket on the second entry. The host's terms facts name each member once; +// a read-only preview of the roster close is refused, as its execution is, +// where the same selection's preview settles. Source-free: the kernel-only // profile registers the row. #include @@ -520,8 +521,13 @@ void judge(const Shape& shape, bool show) { if (show) print_rows("rows, cohort | per-entry", cohort, per_entry); } -// A read-only preview of the cohort close answers what the same selection's -// preview answers, and the run it previews in completes either way. +// A read-only preview of the cohort close is refused as its execution is: a +// current execution admits no roster owner, whose members are read at the +// match (R5 lane PAR-ORDERS-2; expectation corrected: the cohort preview +// answered the selection's settlement -> it is refused as UnsupportedRequest, +// because the preview settled a request whose execute_current failed the run, +// "native current evaluated allowance mismatch"). The same selection's preview +// settles, and the run each preview is taken in completes either way. long previews = 0; void preview(Side side, Others others) { Shape shape; @@ -538,21 +544,11 @@ void preview(Side side, Others others) { if (!cohort.preview || !selected.preview) return; const auto& x = *cohort.preview; const auto& y = *selected.preview; - CHECK(!x.refusal && !y.refusal); - CHECK(!x.terms_rejection && !y.terms_rejection); + CHECK(x.refusal == NativeCurrentRefusal::UnsupportedRequest); + CHECK(!x.terms_rejection && !x.settlement_readiness && x.closed_row_pnl.empty()); + CHECK(!y.refusal && !y.terms_rejection); CHECK(y.settlement_readiness == ex::Status::Applied); - CHECK(x.settlement_readiness == y.settlement_readiness); - if (x.settlement_readiness != y.settlement_readiness) { - std::printf(" [%s] cohort preview readiness=%d, selected preview readiness=%d\n", - scenario.c_str(), - x.settlement_readiness ? int(*x.settlement_readiness) : -1, - y.settlement_readiness ? int(*y.settlement_readiness) : -1); - } - CHECK(x.closed_row_pnl == y.closed_row_pnl); - CHECK(x.closed_row_pnl.size() == (others == Others::Enrolled ? 4u : 3u)); - CHECK(x.account.realized_balance == y.account.realized_balance); - CHECK(x.account.marked_equity == y.account.marked_equity); - CHECK(x.account.signed_units_after == y.account.signed_units_after); + CHECK(y.closed_row_pnl.size() == (others == Others::Enrolled ? 4u : 3u)); ++previews; } diff --git a/tests/test_native_current_cohort_refusal.cpp b/tests/test_native_current_cohort_refusal.cpp new file mode 100644 index 00000000..685e8f03 --- /dev/null +++ b/tests/test_native_current_cohort_refusal.cpp @@ -0,0 +1,240 @@ +// R5 lane PAR-ORDERS-2: a synchronous current execution refuses a request +// bound to a host roster (BindCohort) instead of failing the run. +// +// validate_current_execution admitted a BindCohort owner (R4 slice C) while +// the evaluation's current shape (native_order.cpp current_shape) admits only +// Independent, BindOpening and BindOpenings owners. A cohort close whose +// roster held a live lot therefore passed validation, took its point, came +// back from the evaluation unevaluated (NotEligible) and failed the run with +// "native current evaluated allowance mismatch". The documented contract +// (native-engine.md "Selected exposure and current execution") never listed +// BindCohort; the validation now refuses it as UnsupportedRequest, before a +// point is taken, whatever its roster holds. +// +// Witnessed here on the kernel alone: +// 1. a host-sized cohort close whose roster holds a live lot: inspect and +// execute both answer UnsupportedRequest, nothing is taken or recorded +// (continuation hash unchanged), the run stays Running, and the request +// stays live and fills at the next open like any queued market close; +// 2. a cohort close whose roster holds no live lot: the same refusal (it was +// UnreadyOwner), and it also waits and fills once the roster's entry has. +// A fixed roster (BindOpenings) of the same lot still executes at once. +// +// Fail-before, this TU against the lane's base library (6945fc19): case 1's +// execute_current throws std::logic_error("native current evaluated allowance +// mismatch") and the run ends Failed -- the first failure is +// FAIL test_native_current_cohort_refusal.cpp:: !host.threw +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; + +namespace { + +int checks = 0; +int failures = 0; + +#define CHECK(expr) do { \ + ++checks; \ + if (!(expr)) { \ + ++failures; \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #expr); \ + } \ +} while (0) + +constexpr std::int64_t kT = 1736121600000LL; + +Bar bar(std::int64_t timestamp, double open, double high, double low, double close) { + return {open, high, low, close, 1.0, timestamp}; +} + +NativeRunSpec spec_for(const char* key) { + NativeRunSpec spec; + spec.event_retention = NativeEventRetention::Full; + spec.identity = {key, 1}; + spec.input_tf = "1"; + spec.script_tf = "1"; + spec.tickerid = "PA2:COHORT"; + spec.timezone = "UTC"; + spec.session = "24x7"; + spec.initial_capital = 10000.0; + spec.point_value = 1.0; + spec.account_fx = 1.0; + spec.price_tick = 0.01; + return spec; +} + +no::Request market(double units, const char* label) { + no::Request request; + request.intent = no::Transact{units}; + request.label = label; + return request; +} + +no::Request cohort_close(no::CohortHandle cohort, const char* label) { + no::Request request; + request.intent = no::HostSized{no::HostSizedKind::Close, std::nullopt}; + request.owner = no::BindCohort{cohort}; + request.label = label; + return request; +} + +std::vector applied(const NativeStrategyHost& host, const char* label) { + std::vector out; + for (const auto& row : host.native_events(0)) { + if (!row.command) continue; + if (const auto* event = std::get_if(&*row.command)) { + if (event->request().label == label) out.push_back(*event); + } + } + return out; +} + +class CohortHost final : public NativeStrategyHost { +public: + int bars = 0; + bool threw = false; + std::string thrown; + std::optional live_preview; + std::optional live_result; + std::optional empty_result; + bool hash_unchanged = false; + bool fixed_roster_applied = false; + no::CohortHandle live_cohort{}; + no::CohortHandle empty_cohort{}; + no::RequestHandle held{}; + + // A host-sized close answers the whole scope it is bound to. + no::ExecutionTerms resolve_execution_terms(const NativeExecutionTermsFacts& facts) const override { + if (std::holds_alternative(facts.definition->request.intent)) { + return {facts.default_resolved_price, facts.scope_exposure_units, + no::OpeningShape::Transact}; + } + return {facts.default_resolved_price, std::nullopt, no::OpeningShape::Transact}; + } + + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + ++bars; + if (bars == 1) { + // Two lots on the live roster, filled at bar 2's open. + live_cohort = cohort_open(); + const auto first = submit(market(1.0, "live-first")); + const auto second = submit(market(1.0, "live-second")); + CHECK(first.handle.has_value() && second.handle.has_value()); + if (first.handle) { cohort_add(live_cohort, *first.handle); held = *first.handle; } + if (second.handle) cohort_add(live_cohort, *second.handle); + // A roster whose one entry rests below the market until bar 4. + empty_cohort = cohort_open(); + auto resting = market(1.0, "empty-entry"); + resting.trigger = no::Limit{97.0}; + const auto entry = submit(resting); + CHECK(entry.handle.has_value()); + if (entry.handle) cohort_add(empty_cohort, *entry.handle); + } else if (bars == 2) { + // 1. The live roster: refused, nothing taken. + const auto accepted = submit(cohort_close(live_cohort, "cohort-close")); + CHECK(accepted.handle.has_value()); + if (!accepted.handle) return; + const NativeCurrentExecution command{*accepted.handle, NativeCurrentPriceRule::NearestTick}; + const auto before = native_continuation_hash(); + live_preview = inspect_current_execution(command).refusal; + try { + const auto result = execute_current(command); + if (const auto* refusal = std::get_if(&result)) + live_result = *refusal; + } catch (const std::exception& e) { + threw = true; + thrown = e.what(); + } + hash_unchanged = native_continuation_hash() == before; + // 2. The roster with no live lot: the same refusal. + const auto waiting = submit(cohort_close(empty_cohort, "empty-close")); + CHECK(waiting.handle.has_value()); + if (waiting.handle) { + try { + const auto result = execute_current( + {*waiting.handle, NativeCurrentPriceRule::NearestTick}); + if (const auto* refusal = std::get_if(&result)) + empty_result = *refusal; + } catch (const std::exception& e) { + threw = true; + thrown = e.what(); + } + } + } else if (bars == 3 && held.incarnation != 0) { + // A fixed roster executes at once: nothing live remains on it + // once the cohort close filled at this bar's open, so reopen one. + const auto lot = submit(market(1.0, "fixed-entry")); + CHECK(lot.handle.has_value()); + } else if (bars == 4) { + no::Request close; + close.intent = no::Flatten{}; + close.label = "fixed-close"; + const auto opening = applied(*this, "fixed-entry"); + if (!opening.empty()) { + close.owner = no::BindOpenings{{opening.front().handle()}, + opening.front().cycle_after}; + const auto accepted = submit(close); + if (accepted.handle) { + const auto result = execute_current( + {*accepted.handle, NativeCurrentPriceRule::NearestTick}); + fixed_roster_applied = std::holds_alternative(result); + } + } + } + } +}; + +} // namespace + +int main() { + CohortHost host; + CHECK(host.configure_native(spec_for("pa2-cohort-refusal")).status == NativeSetupStatus::Applied); + const Bar bars[] = { + bar(kT, 100, 100, 100, 100), + bar(kT + 60000, 101, 101, 101, 101), + bar(kT + 120000, 102, 102, 102, 102), + bar(kT + 180000, 99, 99, 96, 98), + bar(kT + 240000, 98, 98, 98, 98), + bar(kT + 300000, 98, 98, 98, 98), + }; + host.run(bars, 6); + + // 1. Refused as UnsupportedRequest, before any point was taken. + CHECK(!host.threw); + if (host.threw) std::printf(" execute_current threw: %s\n", host.thrown.c_str()); + CHECK(host.live_preview == NativeCurrentRefusal::UnsupportedRequest); + CHECK(host.live_result == NativeCurrentRefusal::UnsupportedRequest); + CHECK(host.hash_unchanged); + CHECK(host.native_state().kind != NativeLifecycleKind::Failed); + // ... and still live: it fills at bar 3's open like a queued market close, + // closing both lots of its roster. + const auto cohort_fills = applied(host, "cohort-close"); + CHECK(cohort_fills.size() == 1); + if (!cohort_fills.empty()) { + CHECK(std::fabs(cohort_fills.front().closed_units - 2.0) < 1e-12); + CHECK(std::fabs(cohort_fills.front().resolved_price - 102.0) < 1e-12); + CHECK(cohort_fills.front().cursor.point.provenance != NativePriceProvenance::CurrentExecution); + } + // 2. A roster with no live lot: the same refusal (UnreadyOwner before), + // and the close fills once its entry has (the limit at 97 on bar 4). + CHECK(host.empty_result == NativeCurrentRefusal::UnsupportedRequest); + CHECK(applied(host, "empty-entry").size() == 1); + CHECK(applied(host, "empty-close").size() == 1); + // A fixed roster of a live lot still executes at once. + CHECK(host.fixed_roster_applied); + CHECK(host.native_state().kind != NativeLifecycleKind::Failed); + std::printf("%s native current cohort refusal: %d checks, %d failures\n", + failures == 0 ? "PASS" : "FAIL", checks, failures); + return failures == 0 ? 0 : 1; +} From d8264054e358aa3d85593fd97d74b47490d434ed Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:24:41 +0800 Subject: [PATCH 33/83] Tests: TradingView's trades for an exit a calc_on_order_fills recalculation places while flat (R5 lane PAR-ORDERS-2, item 1) Eleven `lab tv` tapes of the lane's own script (tests/fixtures/flat_coof_exit, NYSE:F 15m, the three days of tests/fixtures/session_islastbar) replayed through the Pine adapter: a pending parent's protective bracket (pend-xf), a pending parent's stop crossed at its birth (pend-xs-crossed -- the shape that ended the run with "native current evaluated allowance mismatch" before the kernel's refusal), and an exit placed while its id has no order at all (xf-* / xs-*, with and without a quantity, process_orders_on_close and calc_on_order_fills). Fail-before on the lane's base: 3 failures (pend-xs-crossed aborts). On this tree: 134 checks, 0 failures; the divergences TradingView shows and this engine keeps are RECORDED rows asserted to differ (pend-xf row 6, pend-xs-crossed rows 2/4/6, xf-qty rows 3/6, xs-qty rows 3/9, xf-tight-qty rows 3/6/9). Release floor 680. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 80fc1f1a89b147055756f643207789266ee4db3d) INT26 pick note: the floor conflict is summed (release 695 -> 696) and the two floor citations name the assignment lines; H-MEASURE's margin_entry_bar tape row had become the tape list's last entry here, so the list's closing parenthesis moves to the lane's new row (git's clean merge had closed the list twice). --- CONTRIBUTING.md | 4 +- docs/pages/contributing-llm.md | 6 +- scripts/ci_verify.py | 4 +- tests/CMakeLists.txt | 9 +- tests/fixtures/flat_coof_exit/README.md | 34 ++ .../pa2-i1-pend-xf-pooc/meta.json | 16 + .../pa2-i1-pend-xf-pooc/metrics.json | 40 ++ .../pa2-i1-pend-xf-pooc/strategy.pine | 42 ++ .../pa2-i1-pend-xf-pooc/tv_trades.csv | 13 + .../pa2-i1-pend-xs-crossed-pooc/meta.json | 16 + .../pa2-i1-pend-xs-crossed-pooc/metrics.json | 40 ++ .../pa2-i1-pend-xs-crossed-pooc/strategy.pine | 43 ++ .../pa2-i1-pend-xs-crossed-pooc/tv_trades.csv | 13 + .../pa2-i1-xf-dyn-pooc/meta.json | 16 + .../pa2-i1-xf-dyn-pooc/metrics.json | 40 ++ .../pa2-i1-xf-dyn-pooc/strategy.pine | 47 +++ .../pa2-i1-xf-dyn-pooc/tv_trades.csv | 19 + .../pa2-i1-xf-qty-pooc/meta.json | 16 + .../pa2-i1-xf-qty-pooc/metrics.json | 40 ++ .../pa2-i1-xf-qty-pooc/strategy.pine | 47 +++ .../pa2-i1-xf-qty-pooc/tv_trades.csv | 19 + .../pa2-i1-xf-qty-pyr2/meta.json | 16 + .../pa2-i1-xf-qty-pyr2/metrics.json | 40 ++ .../pa2-i1-xf-qty-pyr2/strategy.pine | 47 +++ .../pa2-i1-xf-qty-pyr2/tv_trades.csv | 19 + .../pa2-i1-xf-tight-qty-nocoof-pooc/meta.json | 16 + .../metrics.json | 40 ++ .../strategy.pine | 44 ++ .../tv_trades.csv | 13 + .../pa2-i1-xf-tight-qty-pooc/meta.json | 16 + .../pa2-i1-xf-tight-qty-pooc/metrics.json | 40 ++ .../pa2-i1-xf-tight-qty-pooc/strategy.pine | 46 +++ .../pa2-i1-xf-tight-qty-pooc/tv_trades.csv | 19 + .../pa2-i1-xs-dyn-pooc/meta.json | 16 + .../pa2-i1-xs-dyn-pooc/metrics.json | 40 ++ .../pa2-i1-xs-dyn-pooc/strategy.pine | 47 +++ .../pa2-i1-xs-dyn-pooc/tv_trades.csv | 19 + .../flat_coof_exit/pa2-i1-xs-dyn/meta.json | 16 + .../flat_coof_exit/pa2-i1-xs-dyn/metrics.json | 40 ++ .../pa2-i1-xs-dyn/strategy.pine | 47 +++ .../pa2-i1-xs-dyn/tv_trades.csv | 19 + .../pa2-i1-xs-qty-pooc/meta.json | 16 + .../pa2-i1-xs-qty-pooc/metrics.json | 40 ++ .../pa2-i1-xs-qty-pooc/strategy.pine | 47 +++ .../pa2-i1-xs-qty-pooc/tv_trades.csv | 19 + .../flat_coof_exit/pa2-i1-xs-qty/meta.json | 16 + .../flat_coof_exit/pa2-i1-xs-qty/metrics.json | 40 ++ .../pa2-i1-xs-qty/strategy.pine | 47 +++ .../pa2-i1-xs-qty/tv_trades.csv | 19 + tests/test_flat_coof_exit_tapes.cpp | 375 ++++++++++++++++++ 50 files changed, 1736 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/flat_coof_exit/README.md create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/meta.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/metrics.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/strategy.pine create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/tv_trades.csv create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/meta.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/metrics.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/strategy.pine create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/tv_trades.csv create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/meta.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/metrics.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/strategy.pine create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/tv_trades.csv create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/meta.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/metrics.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/strategy.pine create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/tv_trades.csv create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/meta.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/metrics.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/strategy.pine create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/tv_trades.csv create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/meta.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/metrics.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/strategy.pine create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/tv_trades.csv create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/meta.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/metrics.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/strategy.pine create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/tv_trades.csv create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/meta.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/metrics.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/strategy.pine create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/tv_trades.csv create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/meta.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/metrics.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/strategy.pine create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/tv_trades.csv create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/meta.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/metrics.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/strategy.pine create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/tv_trades.csv create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/meta.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/metrics.json create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/strategy.pine create mode 100644 tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/tv_trades.csv create mode 100644 tests/test_flat_coof_exit_tapes.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e9df4c7..22560beb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -152,8 +152,8 @@ What each gate refuses: ### The floors `ci_verify.py` counts the CTest rows that actually **ran** and fails below a -floor — `KERNEL_MIN_TESTS` ci_verify.py:243 and `RELEASE_MIN_TESTS` -ci_verify.py:356. +floor — `KERNEL_MIN_TESTS` ci_verify.py:255 and `RELEASE_MIN_TESTS` +ci_verify.py:389. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 9ea28fcf..f68abf5b 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -117,8 +117,8 @@ finding to report, not a step to take. `scripts/check_doc_reverts.py`. 10. **A test row never silently disappears.** Each profile counts the rows that - *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:243 and - `RELEASE_MIN_TESTS` ci_verify.py:356. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:255 and + `RELEASE_MIN_TESTS` ci_verify.py:389. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -264,7 +264,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:243, `RELEASE_MIN_TESTS` ci_verify.py:356). It +(`KERNEL_MIN_TESTS` ci_verify.py:255, `RELEASE_MIN_TESTS` ci_verify.py:389). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 98c298ec..7d0324a5 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -384,7 +384,9 @@ # 695 = those 694 plus R5 lane PAR-ORDERS-2's # test_native_current_cohort_refusal (see KERNEL_MIN_TESTS; INT26 pick, # summed). -RELEASE_MIN_TESTS = 695 +# 696 = those 695 plus R5 lane PAR-ORDERS-2's test_flat_coof_exit_tapes +# (source-bound; INT26 pick, summed). +RELEASE_MIN_TESTS = 696 # R5 lane H-DOCGATES (AUDIT4-opus perf N-7) raises both floors by one more: # test_native_runtime_ambient_lifo, the runtime blocks' LIFO death row, is # source-free, so it registers and runs in the kernel profile (279 -> 280) and diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9cf407f1..461da244 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1366,6 +1366,11 @@ list(APPEND TEST_SOURCES test_adapter_security_route_conditions) # roster (BindCohort) instead of failing the run. Source-free, so it also runs # in the kernel-only profile. list(APPEND TEST_SOURCES test_native_current_cohort_refusal) +# R5 lane PAR-ORDERS-2: TradingView's own trades (`lab tv` tapes under +# tests/fixtures/, each directory's README.md names them) replayed through the +# Pine adapter. All source-bound. +# A calc_on_order_fills exit placed while flat, for a pending or a filled parent. +list(APPEND TEST_SOURCES test_flat_coof_exit_tapes) # R4-D L4b-fix restores every removed ShortSeed, affordability, sizing and # deferred-close oracle as an individually compiled native-route twin. Keep @@ -2107,7 +2112,9 @@ foreach(_pf_hm_row IN ITEMS "test_session_ismarket_tape|PINEFORGE_G236_FIXTURE_DIR|session_ismarket" "test_aggregated_entry_bar_index_tape|PINEFORGE_HM_F1E_FIXTURE_DIR|aggregated_entry_bar_index" "test_magnified_aggregated_tape|PINEFORGE_HM_MAG_FIXTURE_DIR|magnified_aggregated" - "test_adapter_margin_schedule_differential|PINEFORGE_HM_M7A_FIXTURE_DIR|margin_entry_bar") + "test_adapter_margin_schedule_differential|PINEFORGE_HM_M7A_FIXTURE_DIR|margin_entry_bar" + # R5 lane PAR-ORDERS-2's tape rows. + "test_flat_coof_exit_tapes|PINEFORGE_PA2_FLAT_COOF_FIXTURE_DIR|flat_coof_exit") string(REPLACE "|" ";" _pf_hm_fields "${_pf_hm_row}") list(GET _pf_hm_fields 0 _pf_hm_target) list(GET _pf_hm_fields 1 _pf_hm_macro) diff --git a/tests/fixtures/flat_coof_exit/README.md b/tests/fixtures/flat_coof_exit/README.md new file mode 100644 index 00000000..a033617c --- /dev/null +++ b/tests/fixtures/flat_coof_exit/README.md @@ -0,0 +1,34 @@ +# Flat recalculation exit tapes (R5 lane PAR-ORDERS-2, item 1) + +TradingView's own trades for a `strategy.exit` placed in a `calc_on_order_fills` +recalculation on a FLAT book: market entry E fills at bar j's open and its exit TX +sits inside that bar's first leg, so TX flattens the book mid-leg (cycles j = 2, 30, +41), and the recalculation places an entry and an exit for it, or an exit for an id +with no order yet. `tests/test_flat_coof_exit_tapes.cpp` replays each tape through +the Pine adapter on the 15m chart. + +Each directory is one `lab tv` export (pineforge-workflow, channel `ws-report-v1`, +`rangeProof: covered`, NYSE:F 15, window 2025-07-01 .. 2025-07-08), byte-identical: +`strategy.pine`, `tv_trades.csv` (times UTC+8), `meta.json`, `metrics.json`. The +scripts count chart bars from 2025-07-02 09:30 ET, the first bar of +`tests/fixtures/session_islastbar/bars.inc`, whose 15m and 1m bars the rows replay. + +| probe | variant | trades | tvTradesSha256 | note | +|---|---|---|---|---| +| `pa2-i1-pend-xf-pooc` | pending buy limit entry L, bracket XF 50c either side (POOC) | 6 | `b563b368e50a261d89437b2bc649f3e0b0cfc1f00f7aaa932a62cb6dcd7fc26a` | `tv-tape-pa2-i1-pend-xf-pooc-b563b368` | +| `pa2-i1-pend-xs-crossed-pooc` | pending sell limit entry S, XS stop 50c below (crossed) (POOC) | 6 | `cdb5ca043f4870e951c60e3aee246d74fc1d47bebe2cb6f2c5c7adc989df2952` | `tv-tape-pa2-i1-pend-xs-crossed-pooc-cdb5ca04` | +| `pa2-i1-xf-qty-pooc` | XF of a long id with no order, qty 100 (POOC) | 9 | `c78b18abe1bd4af3d01c40b7c1705d3d7a20505796e449b699515a590c1c5fce` | `tv-tape-pa2-i1-xf-qty-pooc-c78b18ab` | +| `pa2-i1-xf-qty-pyr2` | the same without POOC, pyramiding 2 | 9 | `d127e5c29b28963fd10614a25dcc8f9e646269b76a5e49a36b08ae22b7bd60bd` | `tv-tape-pa2-i1-xf-qty-pyr2-d127e5c2` | +| `pa2-i1-xf-dyn-pooc` | XF of a long id with no order, whole position (POOC) | 9 | `c78b18abe1bd4af3d01c40b7c1705d3d7a20505796e449b699515a590c1c5fce` | `tv-tape-pa2-i1-xf-dyn-pooc-c78b18ab` | +| `pa2-i1-xs-dyn-pooc` | XS of a short id with no order, stop 50c below (POOC) | 9 | `54720b55cf52d84d1455db4abc4ef12c119dfae53013ddfdff6d2df6e5332013` | `tv-tape-pa2-i1-xs-dyn-pooc-54720b55` | +| `pa2-i1-xs-dyn` | the same without POOC | 9 | `626653faf60e832fc66d8cde05c62315193ca08faa8332ff36bfe7d9326f34ff` | `tv-tape-pa2-i1-xs-dyn-626653fa` | +| `pa2-i1-xs-qty-pooc` | XS with qty 100 (POOC) | 9 | `54720b55cf52d84d1455db4abc4ef12c119dfae53013ddfdff6d2df6e5332013` | `tv-tape-pa2-i1-xs-qty-pooc-54720b55` | +| `pa2-i1-xs-qty` | the same without POOC | 9 | `626653faf60e832fc66d8cde05c62315193ca08faa8332ff36bfe7d9326f34ff` | `tv-tape-pa2-i1-xs-qty-626653fa` | +| `pa2-i1-xf-tight-qty-pooc` | XF of a long id with no order, qty 100, bracket 3c either side (POOC) | 9 | `c78b18abe1bd4af3d01c40b7c1705d3d7a20505796e449b699515a590c1c5fce` | `tv-tape-pa2-i1-xf-tight-qty-pooc-c78b18ab` | +| `pa2-i1-xf-tight-qty-nocoof-pooc` | the same placed at a bar close, calc_on_order_fills off (POOC) | 6 | `3999af4912a43d9cc4e5f13697b1016a09bb7f5fcda33c656c8321b7c8ea330a` | `tv-tape-pa2-i1-xf-tight-qty-nocoof-pooc-3999af49` | + +What they show: a pending parent's untouched protective bracket rests (3 of 3); a +pending parent's crossed stop exits at the parent's own fill (3 of 3); an exit placed +while its id has no order at all is never active -- the id's trade runs to the +`close_all` even with a 3c bracket, which is why five of the tapes carry the same +trade list (`c78b18ab...`, `626653fa...`, `54720b55...`). diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/meta.json b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/meta.json new file mode 100644 index 00000000..536b76ce --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I1 pend-xf-pooc", + "slug": "pa2-i1-pend-xf-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:59:09.701Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "037b2cef7a1e2238bb868ce92272ea18f7536d8622d76492eb604c79775225a0", + "tv_trades_sha256": "b563b368e50a261d89437b2bc649f3e0b0cfc1f00f7aaa932a62cb6dcd7fc26a", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/metrics.json b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/metrics.json new file mode 100644 index 00000000..18b9ac1e --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i1-pend-xf-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "037b2cef7a1e2238bb868ce92272ea18f7536d8622d76492eb604c79775225a0", + "tvTradesCsvHash": "b563b368e50a261d89437b2bc649f3e0b0cfc1f00f7aaa932a62cb6dcd7fc26a", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75667", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "430f07bd8fc3e1022a0cca04f3b63fd1e00c9f7cc79659533573b77d18738215", + "decodedReportSha256": "e146338c753677f8488fea2353e9f3ff7c70c9b94c018a8c29a70c31d3509524", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:59:09.700Z" +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/strategy.pine b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/strategy.pine new file mode 100644 index 00000000..687f005f --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/strategy.pine @@ -0,0 +1,42 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 1, the rule for an exit whose id has no filled entry. +// Market entry E fills at bar j's open and its exit TX sits inside bar j's first leg, +// so TX flattens the book mid-leg (cycles j = 2, 30, 41; levels read off those bars). +// The recalculation that fill starts places a buy LIMIT entry L 2c below and its exit XF +// (a bracket 50c either side, never touched); on bar j+3 everything is closed. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I1 pend-xf-pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +n = strategy.closedtrades +justFlat = units == 0 and n > 0 and strategy.closedtrades.exit_id(n - 1) == "TX" and strategy.closedtrades.exit_bar_index(n - 1) == bar_index +lvl = k == 1 ? 11.46 : k == 29 ? 11.85 : k == 40 ? 11.66 : na +if k >= 0 and time < tEnd + if not na(lvl) and units == 0 + strategy.entry("E", strategy.long) + if k == 40 + strategy.exit("TX", "E", stop=lvl) + else + strategy.exit("TX", "E", limit=lvl) + if justFlat + strategy.entry("L", strategy.long, limit=(t - 2) / 100) + strategy.exit("XF", "L", limit=(t + 50) / 100, stop=(t - 50) / 100) + if (k == 5 or k == 33 or k == 44) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/tv_trades.csv b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/tv_trades.csv new file mode 100644 index 00000000..f553a888 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xf-pooc/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:00,TX,11.46,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +1,Entry long,2025-07-02 21:45,E,11.45,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +2,Exit long,2025-07-02 22:45,Close position order,11.51,100,1143,7.7706,0.68,0.2294,8.8857,0.78,-2.1143,-0.18,8.5415,0.01,3 +2,Entry long,2025-07-02 22:00,L,11.43,100,1143,7.7706,0.68,0.2294,8.8857,0.78,-2.1143,-0.18,8.5415,0.01,3 +3,Exit long,2025-07-03 22:30,TX,11.85,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,9.3046,0.01,1 +3,Entry long,2025-07-03 22:15,E,11.84,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,9.3046,0.01,1 +4,Exit long,2025-07-03 23:15,Close position order,11.84,100,1184,-0.2368,-0.02,0.2368,2.8816,0.24,-4.1184,-0.35,9.0678,0.01,3 +4,Entry long,2025-07-03 22:30,L,11.84,100,1184,-0.2368,-0.02,0.2368,2.8816,0.24,-4.1184,-0.35,9.0678,0.01,3 +5,Exit long,2025-07-07 21:45,TX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,6.8344,0.01,1 +5,Entry long,2025-07-07 21:30,E,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,6.8344,0.01,1 +6,Exit long,2025-07-07 22:30,Close position order,11.7,100,1165,4.7665,0.41,0.2335,13.8835,1.19,0,0.00,11.6009,0.01,3 +6,Entry long,2025-07-07 21:45,L,11.65,100,1165,4.7665,0.41,0.2335,13.8835,1.19,0,0.00,11.6009,0.01,3 \ No newline at end of file diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/meta.json b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/meta.json new file mode 100644 index 00000000..68c48748 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I1 pend-xs-crossed-pooc", + "slug": "pa2-i1-pend-xs-crossed-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:59:05.836Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "fe1a7f5d33cee09b70ece5e726184803a4f8b8e7755a6b145fe9e50e83b5391a", + "tv_trades_sha256": "cdb5ca043f4870e951c60e3aee246d74fc1d47bebe2cb6f2c5c7adc989df2952", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/metrics.json b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/metrics.json new file mode 100644 index 00000000..ec2d3ed9 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i1-pend-xs-crossed-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "fe1a7f5d33cee09b70ece5e726184803a4f8b8e7755a6b145fe9e50e83b5391a", + "tvTradesCsvHash": "cdb5ca043f4870e951c60e3aee246d74fc1d47bebe2cb6f2c5c7adc989df2952", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75666", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "9db4a54dcec392dc3d6134dc2732255a145a63e047854388e0a9ba3bbaf9f678", + "decodedReportSha256": "6139b74e31966cc964889e0eee2023ec8c1547ca7991089c70cf9ceee06c4ab9", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:59:05.835Z" +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/strategy.pine b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/strategy.pine new file mode 100644 index 00000000..837d6a5e --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 1, the rule for an exit whose id has no filled entry. +// Market entry E fills at bar j's open and its exit TX sits inside bar j's first leg, +// so TX flattens the book mid-leg (cycles j = 2, 30, 41; levels read off those bars). +// The recalculation that fill starts places a sell LIMIT entry S 2c above and its exit XS +// (stop 50c BELOW the price: crossed as soon as S is open; limit 100c below); on bar j+3 +// everything is closed. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I1 pend-xs-crossed-pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +n = strategy.closedtrades +justFlat = units == 0 and n > 0 and strategy.closedtrades.exit_id(n - 1) == "TX" and strategy.closedtrades.exit_bar_index(n - 1) == bar_index +lvl = k == 1 ? 11.46 : k == 29 ? 11.85 : k == 40 ? 11.66 : na +if k >= 0 and time < tEnd + if not na(lvl) and units == 0 + strategy.entry("E", strategy.long) + if k == 40 + strategy.exit("TX", "E", stop=lvl) + else + strategy.exit("TX", "E", limit=lvl) + if justFlat + strategy.entry("S", strategy.short, limit=(t + 2) / 100) + strategy.exit("XS", "S", limit=(t - 100) / 100, stop=(t - 50) / 100) + if (k == 5 or k == 33 or k == 44) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/tv_trades.csv b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/tv_trades.csv new file mode 100644 index 00000000..f4482452 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-pend-xs-crossed-pooc/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:00,TX,11.46,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +1,Entry long,2025-07-02 21:45,E,11.45,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +2,Exit short,2025-07-02 22:00,XS,11.48,100,1148,-0.2296,-0.02,0.2296,0,0.00,0,0.00,0.5413,0.00,0 +2,Entry short,2025-07-02 22:00,S,11.48,100,1148,-0.2296,-0.02,0.2296,0,0.00,0,0.00,0.5413,0.00,0 +3,Exit long,2025-07-03 22:30,TX,11.85,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,1.3044,0.00,1 +3,Entry long,2025-07-03 22:15,E,11.84,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,1.3044,0.00,1 +4,Exit short,2025-07-03 23:45,XS,11.88,100,1188,-0.2376,-0.02,0.2376,0,0.00,0,0.00,1.0668,0.00,0 +4,Entry short,2025-07-03 23:45,S,11.88,100,1188,-0.2376,-0.02,0.2376,0,0.00,0,0.00,1.0668,0.00,0 +5,Exit long,2025-07-07 21:45,TX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-1.1666,0.00,1 +5,Entry long,2025-07-07 21:30,E,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-1.1666,0.00,1 +6,Exit short,2025-07-07 22:00,XS,11.74,100,1174,-0.2348,-0.02,0.2348,0,0.00,0,0.00,-1.4014,0.00,0 +6,Entry short,2025-07-07 22:00,S,11.74,100,1174,-0.2348,-0.02,0.2348,0,0.00,0,0.00,-1.4014,0.00,0 \ No newline at end of file diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/meta.json b/tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/meta.json new file mode 100644 index 00000000..d8deadcd --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I1 xf-dyn-pooc", + "slug": "pa2-i1-xf-dyn-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:55:17.416Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "b3d579a8ecf57f050215a823519161064e7e3615f4678aeed5916b7a35b7c770", + "tv_trades_sha256": "c78b18abe1bd4af3d01c40b7c1705d3d7a20505796e449b699515a590c1c5fce", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/metrics.json b/tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/metrics.json new file mode 100644 index 00000000..5685dc88 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i1-xf-dyn-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "b3d579a8ecf57f050215a823519161064e7e3615f4678aeed5916b7a35b7c770", + "tvTradesCsvHash": "c78b18abe1bd4af3d01c40b7c1705d3d7a20505796e449b699515a590c1c5fce", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75480", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "1d4edf778ccbf18c99710a0e36a9bcb6cda8747bf3ca3b4b46617ec0e8b28f3a", + "decodedReportSha256": "fcbdab144c324d230903cee02a819e880085d6c9f2838835de5bceeb57c26e67", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:55:17.415Z" +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/strategy.pine b/tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/strategy.pine new file mode 100644 index 00000000..068028b9 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/strategy.pine @@ -0,0 +1,47 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 1: a strategy.exit placed in a calc_on_order_fills +// recalculation on a FLAT book, for an entry id that has no order yet. Market entry E +// fills at bar j's open and its exit TX sits inside bar j's first leg (a limit on a +// high-first bar, a stop on a low-first one), so TX flattens the book mid-leg; the +// recalculation that fill starts places the exit XF of a LONG id L -- a bracket 50c either side, never touched, +// and a market entry M. On bar j+1 M is closed and the id entered; on bar j+3 everything +// is closed. Cycles j = 2, 30, 41 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I1 xf-dyn-pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +n = strategy.closedtrades +justFlat = units == 0 and n > 0 and strategy.closedtrades.exit_id(n - 1) == "TX" and strategy.closedtrades.exit_bar_index(n - 1) == bar_index +lvl = k == 1 ? 11.46 : k == 29 ? 11.85 : k == 40 ? 11.66 : na +if k >= 0 and time < tEnd + if not na(lvl) and units == 0 + strategy.entry("E", strategy.long) + if k == 40 + strategy.exit("TX", "E", stop=lvl) + else + strategy.exit("TX", "E", limit=lvl) + if justFlat + strategy.exit("XF", "L", limit=(t + 50) / 100, stop=(t - 50) / 100) + strategy.entry("M", strategy.long) + if (k == 3 or k == 31 or k == 42) and units != 0 + strategy.close("M") + strategy.entry("L", strategy.long) + if (k == 5 or k == 33 or k == 44) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/tv_trades.csv b/tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/tv_trades.csv new file mode 100644 index 00000000..3cc0d273 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-dyn-pooc/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:00,TX,11.46,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +1,Entry long,2025-07-02 21:45,E,11.45,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +2,Exit long,2025-07-02 22:15,Close entry(s) order M,11.47,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.5733,0.00,1 +2,Entry long,2025-07-02 22:00,M,11.48,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.5733,0.00,1 +3,Exit long,2025-07-02 22:45,Close position order,11.51,100,1147,3.7702,0.33,0.2298,4.8853,0.43,-3.1147,-0.27,3.3116,0.00,2 +3,Entry long,2025-07-02 22:15,L,11.47,100,1147,3.7702,0.33,0.2298,4.8853,0.43,-3.1147,-0.27,3.3116,0.00,2 +4,Exit long,2025-07-03 22:30,TX,11.85,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,4.0747,0.00,1 +4,Entry long,2025-07-03 22:15,E,11.84,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,4.0747,0.00,1 +5,Exit long,2025-07-03 22:45,Close entry(s) order M,11.86,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,2.7188,0.00,1 +5,Entry long,2025-07-03 22:30,M,11.87,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,2.7188,0.00,1 +6,Exit long,2025-07-03 23:15,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,0.6004,0.00,2 +6,Entry long,2025-07-03 22:45,L,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,0.6004,0.00,2 +7,Exit long,2025-07-07 21:45,TX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-1.633,0.00,1 +7,Entry long,2025-07-07 21:30,E,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-1.633,0.00,1 +8,Exit long,2025-07-07 22:00,Close entry(s) order M,11.76,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,9.0153,0.01,1 +8,Entry long,2025-07-07 21:45,M,11.65,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,9.0153,0.01,1 +9,Exit long,2025-07-07 22:30,Close position order,11.7,100,1176,-6.2346,-0.53,0.2346,0,0.00,-9.1176,-0.78,2.8983,0.00,2 +9,Entry long,2025-07-07 22:00,L,11.76,100,1176,-6.2346,-0.53,0.2346,0,0.00,-9.1176,-0.78,2.8983,0.00,2 \ No newline at end of file diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/meta.json b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/meta.json new file mode 100644 index 00000000..3920a65c --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I1 xf-qty-pooc", + "slug": "pa2-i1-xf-qty-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:55:26.018Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "b2270ecfbc2315e2c0bb436099685c5efb017357f6d7343086e9e6836d0cd7f8", + "tv_trades_sha256": "c78b18abe1bd4af3d01c40b7c1705d3d7a20505796e449b699515a590c1c5fce", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/metrics.json b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/metrics.json new file mode 100644 index 00000000..cf92c88a --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i1-xf-qty-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "b2270ecfbc2315e2c0bb436099685c5efb017357f6d7343086e9e6836d0cd7f8", + "tvTradesCsvHash": "c78b18abe1bd4af3d01c40b7c1705d3d7a20505796e449b699515a590c1c5fce", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75486", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "0e65a677555a55a4bc19398179635fd331624f607473d941d93964a66a575254", + "decodedReportSha256": "fcbdab144c324d230903cee02a819e880085d6c9f2838835de5bceeb57c26e67", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:55:26.017Z" +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/strategy.pine b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/strategy.pine new file mode 100644 index 00000000..d8945ff8 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/strategy.pine @@ -0,0 +1,47 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 1: a strategy.exit placed in a calc_on_order_fills +// recalculation on a FLAT book, for an entry id that has no order yet. Market entry E +// fills at bar j's open and its exit TX sits inside bar j's first leg (a limit on a +// high-first bar, a stop on a low-first one), so TX flattens the book mid-leg; the +// recalculation that fill starts places the exit XF of a LONG id L -- a bracket 50c either side, never touched, with qty=100, +// and a market entry M. On bar j+1 M is closed and the id entered; on bar j+3 everything +// is closed. Cycles j = 2, 30, 41 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I1 xf-qty-pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +n = strategy.closedtrades +justFlat = units == 0 and n > 0 and strategy.closedtrades.exit_id(n - 1) == "TX" and strategy.closedtrades.exit_bar_index(n - 1) == bar_index +lvl = k == 1 ? 11.46 : k == 29 ? 11.85 : k == 40 ? 11.66 : na +if k >= 0 and time < tEnd + if not na(lvl) and units == 0 + strategy.entry("E", strategy.long) + if k == 40 + strategy.exit("TX", "E", stop=lvl) + else + strategy.exit("TX", "E", limit=lvl) + if justFlat + strategy.exit("XF", "L", limit=(t + 50) / 100, stop=(t - 50) / 100, qty=100) + strategy.entry("M", strategy.long) + if (k == 3 or k == 31 or k == 42) and units != 0 + strategy.close("M") + strategy.entry("L", strategy.long) + if (k == 5 or k == 33 or k == 44) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/tv_trades.csv b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/tv_trades.csv new file mode 100644 index 00000000..3cc0d273 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pooc/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:00,TX,11.46,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +1,Entry long,2025-07-02 21:45,E,11.45,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +2,Exit long,2025-07-02 22:15,Close entry(s) order M,11.47,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.5733,0.00,1 +2,Entry long,2025-07-02 22:00,M,11.48,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.5733,0.00,1 +3,Exit long,2025-07-02 22:45,Close position order,11.51,100,1147,3.7702,0.33,0.2298,4.8853,0.43,-3.1147,-0.27,3.3116,0.00,2 +3,Entry long,2025-07-02 22:15,L,11.47,100,1147,3.7702,0.33,0.2298,4.8853,0.43,-3.1147,-0.27,3.3116,0.00,2 +4,Exit long,2025-07-03 22:30,TX,11.85,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,4.0747,0.00,1 +4,Entry long,2025-07-03 22:15,E,11.84,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,4.0747,0.00,1 +5,Exit long,2025-07-03 22:45,Close entry(s) order M,11.86,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,2.7188,0.00,1 +5,Entry long,2025-07-03 22:30,M,11.87,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,2.7188,0.00,1 +6,Exit long,2025-07-03 23:15,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,0.6004,0.00,2 +6,Entry long,2025-07-03 22:45,L,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,0.6004,0.00,2 +7,Exit long,2025-07-07 21:45,TX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-1.633,0.00,1 +7,Entry long,2025-07-07 21:30,E,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-1.633,0.00,1 +8,Exit long,2025-07-07 22:00,Close entry(s) order M,11.76,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,9.0153,0.01,1 +8,Entry long,2025-07-07 21:45,M,11.65,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,9.0153,0.01,1 +9,Exit long,2025-07-07 22:30,Close position order,11.7,100,1176,-6.2346,-0.53,0.2346,0,0.00,-9.1176,-0.78,2.8983,0.00,2 +9,Entry long,2025-07-07 22:00,L,11.76,100,1176,-6.2346,-0.53,0.2346,0,0.00,-9.1176,-0.78,2.8983,0.00,2 \ No newline at end of file diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/meta.json b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/meta.json new file mode 100644 index 00000000..4a3e2922 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I1 xf-qty-pyr2", + "slug": "pa2-i1-xf-qty-pyr2", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:55:29.896Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "efefa06ead78eb08591f0ed9814a9bef1e48dae8349910c1819f790c026dc5bc", + "tv_trades_sha256": "d127e5c29b28963fd10614a25dcc8f9e646269b76a5e49a36b08ae22b7bd60bd", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/metrics.json b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/metrics.json new file mode 100644 index 00000000..7de8d514 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i1-xf-qty-pyr2", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "efefa06ead78eb08591f0ed9814a9bef1e48dae8349910c1819f790c026dc5bc", + "tvTradesCsvHash": "d127e5c29b28963fd10614a25dcc8f9e646269b76a5e49a36b08ae22b7bd60bd", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75491", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "5481aee097dfe0c2e7055b9d7033915026117187a146fc796b1121d50f4812af", + "decodedReportSha256": "052ad01f6065181db6e0bf54a579c57b8581fb4d4fcad1ddc56500c53912793a", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:55:29.894Z" +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/strategy.pine b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/strategy.pine new file mode 100644 index 00000000..8182bc5a --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/strategy.pine @@ -0,0 +1,47 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 1: a strategy.exit placed in a calc_on_order_fills +// recalculation on a FLAT book, for an entry id that has no order yet. Market entry E +// fills at bar j's open and its exit TX sits inside bar j's first leg (a limit on a +// high-first bar, a stop on a low-first one), so TX flattens the book mid-leg; the +// recalculation that fill starts places the exit XF of a LONG id L -- a bracket 50c either side, never touched, with qty=100, +// and a market entry M. On bar j+1 M is closed and the id entered; on bar j+3 everything +// is closed. Cycles j = 2, 30, 41 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I1 xf-qty-pyr2", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=2, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +n = strategy.closedtrades +justFlat = units == 0 and n > 0 and strategy.closedtrades.exit_id(n - 1) == "TX" and strategy.closedtrades.exit_bar_index(n - 1) == bar_index +lvl = k == 1 ? 11.46 : k == 29 ? 11.85 : k == 40 ? 11.66 : na +if k >= 0 and time < tEnd + if not na(lvl) and units == 0 + strategy.entry("E", strategy.long) + if k == 40 + strategy.exit("TX", "E", stop=lvl) + else + strategy.exit("TX", "E", limit=lvl) + if justFlat + strategy.exit("XF", "L", limit=(t + 50) / 100, stop=(t - 50) / 100, qty=100) + strategy.entry("M", strategy.long) + if (k == 3 or k == 31 or k == 42) and units != 0 + strategy.close("M") + strategy.entry("L", strategy.long) + if (k == 5 or k == 33 or k == 44) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/tv_trades.csv b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/tv_trades.csv new file mode 100644 index 00000000..0ddd89a4 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-qty-pyr2/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:00,TX,11.46,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,0 +1,Entry long,2025-07-02 22:00,E,11.45,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,0 +2,Exit long,2025-07-02 22:30,Close entry(s) order M,11.47,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.5733,0.00,2 +2,Entry long,2025-07-02 22:00,M,11.48,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.5733,0.00,2 +3,Exit long,2025-07-02 23:00,Close position order,11.5,100,1147,2.7703,0.24,0.2297,4.8853,0.43,-3.1147,-0.27,2.3117,0.00,2 +3,Entry long,2025-07-02 22:30,L,11.47,100,1147,2.7703,0.24,0.2297,4.8853,0.43,-3.1147,-0.27,2.3117,0.00,2 +4,Exit long,2025-07-03 22:30,TX,11.85,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,3.0748,0.00,0 +4,Entry long,2025-07-03 22:30,E,11.84,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,3.0748,0.00,0 +5,Exit long,2025-07-03 23:00,Close entry(s) order M,11.86,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,1.7189,0.00,2 +5,Entry long,2025-07-03 22:30,M,11.87,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,1.7189,0.00,2 +6,Exit long,2025-07-03 23:30,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,-0.3995,0.00,2 +6,Entry long,2025-07-03 23:00,L,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,-0.3995,0.00,2 +7,Exit long,2025-07-07 21:45,TX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-2.6329,0.00,0 +7,Entry long,2025-07-07 21:45,E,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-2.6329,0.00,0 +8,Exit long,2025-07-07 22:15,Close entry(s) order M,11.76,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,8.0154,0.01,2 +8,Entry long,2025-07-07 21:45,M,11.65,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,8.0154,0.01,2 +9,Exit long,2025-07-07 22:45,Close position order,11.7,100,1176,-6.2346,-0.53,0.2346,0,0.00,-9.1176,-0.78,1.8984,0.00,2 +9,Entry long,2025-07-07 22:15,L,11.76,100,1176,-6.2346,-0.53,0.2346,0,0.00,-9.1176,-0.78,1.8984,0.00,2 \ No newline at end of file diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/meta.json b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/meta.json new file mode 100644 index 00000000..eceba457 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I1 xf-tight-qty-nocoof-pooc", + "slug": "pa2-i1-xf-tight-qty-nocoof-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:59:02.251Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "11877d4c5c9032a8f281b31bcc26cfdf8b9cb90a9580557aebf0b50377ea6e3a", + "tv_trades_sha256": "3999af4912a43d9cc4e5f13697b1016a09bb7f5fcda33c656c8321b7c8ea330a", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/metrics.json b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/metrics.json new file mode 100644 index 00000000..3e911b2a --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i1-xf-tight-qty-nocoof-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "11877d4c5c9032a8f281b31bcc26cfdf8b9cb90a9580557aebf0b50377ea6e3a", + "tvTradesCsvHash": "3999af4912a43d9cc4e5f13697b1016a09bb7f5fcda33c656c8321b7c8ea330a", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75665", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "31b205a4ef695af2a1d25a71fda8e69b3a0e714386c549820fb9978baaf2339c", + "decodedReportSha256": "39073a04d194c1e3a4fc31ecf33bbdfffb74e438ba9cc30431559667145dfecb", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:59:02.250Z" +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/strategy.pine b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/strategy.pine new file mode 100644 index 00000000..c03355f5 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/strategy.pine @@ -0,0 +1,44 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 1, the rule for an exit whose id has no filled entry. +// Market entry E fills at bar j's open and its exit TX sits inside bar j's first leg, +// so TX flattens the book mid-leg (cycles j = 2, 30, 41; levels read off those bars). +// WITHOUT calc_on_order_fills: the exit XF of a LONG id L with no order yet (qty=100, a +// bracket 3c either side) is placed at bar j-1's close calculation, with E; L is entered +// on bar j+1; on bar j+3 everything is closed. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I1 xf-tight-qty-nocoof-pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +n = strategy.closedtrades +justFlat = units == 0 and n > 0 and strategy.closedtrades.exit_id(n - 1) == "TX" and strategy.closedtrades.exit_bar_index(n - 1) == bar_index +lvl = k == 1 ? 11.46 : k == 29 ? 11.85 : k == 40 ? 11.66 : na +if k >= 0 and time < tEnd + if not na(lvl) and units == 0 + strategy.entry("E", strategy.long) + if k == 40 + strategy.exit("TX", "E", stop=lvl) + else + strategy.exit("TX", "E", limit=lvl) + if not na(lvl) and units == 0 + strategy.exit("XF", "L", limit=(t + 3) / 100, stop=(t - 3) / 100, qty=100) + if (k == 3 or k == 31 or k == 42) and units == 0 + strategy.entry("L", strategy.long) + if (k == 5 or k == 33 or k == 44) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/tv_trades.csv b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/tv_trades.csv new file mode 100644 index 00000000..58443d1a --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-nocoof-pooc/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:00,TX,11.46,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +1,Entry long,2025-07-02 21:45,E,11.45,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +2,Exit long,2025-07-02 22:45,Close position order,11.51,100,1147,3.7702,0.33,0.2298,4.8853,0.43,-3.1147,-0.27,4.5411,0.00,2 +2,Entry long,2025-07-02 22:15,L,11.47,100,1147,3.7702,0.33,0.2298,4.8853,0.43,-3.1147,-0.27,4.5411,0.00,2 +3,Exit long,2025-07-03 22:30,TX,11.85,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,5.3042,0.01,1 +3,Entry long,2025-07-03 22:15,E,11.84,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,5.3042,0.01,1 +4,Exit long,2025-07-03 23:15,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,3.0672,0.00,2 +4,Entry long,2025-07-03 22:45,L,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,3.0672,0.00,2 +5,Exit long,2025-07-07 21:45,TX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,0.8338,0.00,1 +5,Entry long,2025-07-07 21:30,E,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,0.8338,0.00,1 +6,Exit long,2025-07-07 22:30,Close position order,11.7,100,1176,-6.2346,-0.53,0.2346,0,0.00,-9.1176,-0.78,-5.4008,-0.01,2 +6,Entry long,2025-07-07 22:00,L,11.76,100,1176,-6.2346,-0.53,0.2346,0,0.00,-9.1176,-0.78,-5.4008,-0.01,2 \ No newline at end of file diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/meta.json b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/meta.json new file mode 100644 index 00000000..07da515d --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I1 xf-tight-qty-pooc", + "slug": "pa2-i1-xf-tight-qty-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:58:58.495Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "864d4f5230d071f2bbe294165da28d0443b170a8004b9f037200c4a4a93437ae", + "tv_trades_sha256": "c78b18abe1bd4af3d01c40b7c1705d3d7a20505796e449b699515a590c1c5fce", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/metrics.json b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/metrics.json new file mode 100644 index 00000000..5510d989 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i1-xf-tight-qty-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "864d4f5230d071f2bbe294165da28d0443b170a8004b9f037200c4a4a93437ae", + "tvTradesCsvHash": "c78b18abe1bd4af3d01c40b7c1705d3d7a20505796e449b699515a590c1c5fce", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75664", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "816800d1dba95108b9338f9d5968d7e94a85fe173440d002a21ffaa00121acdf", + "decodedReportSha256": "fcbdab144c324d230903cee02a819e880085d6c9f2838835de5bceeb57c26e67", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:58:58.494Z" +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/strategy.pine b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/strategy.pine new file mode 100644 index 00000000..ed7db2c2 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/strategy.pine @@ -0,0 +1,46 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 1, the rule for an exit whose id has no filled entry. +// Market entry E fills at bar j's open and its exit TX sits inside bar j's first leg, +// so TX flattens the book mid-leg (cycles j = 2, 30, 41; levels read off those bars). +// The recalculation that fill starts places the exit XF of a LONG id L with no order yet +// (qty=100, a bracket 3c either side: reached once L is open) and a market entry M; on bar +// j+1 M is closed and L entered; on bar j+3 everything is closed. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I1 xf-tight-qty-pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +n = strategy.closedtrades +justFlat = units == 0 and n > 0 and strategy.closedtrades.exit_id(n - 1) == "TX" and strategy.closedtrades.exit_bar_index(n - 1) == bar_index +lvl = k == 1 ? 11.46 : k == 29 ? 11.85 : k == 40 ? 11.66 : na +if k >= 0 and time < tEnd + if not na(lvl) and units == 0 + strategy.entry("E", strategy.long) + if k == 40 + strategy.exit("TX", "E", stop=lvl) + else + strategy.exit("TX", "E", limit=lvl) + if justFlat + strategy.exit("XF", "L", limit=(t + 3) / 100, stop=(t - 3) / 100, qty=100) + strategy.entry("M", strategy.long) + if (k == 3 or k == 31 or k == 42) and units != 0 + strategy.close("M") + strategy.entry("L", strategy.long) + if (k == 5 or k == 33 or k == 44) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/tv_trades.csv b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/tv_trades.csv new file mode 100644 index 00000000..3cc0d273 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xf-tight-qty-pooc/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:00,TX,11.46,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +1,Entry long,2025-07-02 21:45,E,11.45,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +2,Exit long,2025-07-02 22:15,Close entry(s) order M,11.47,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.5733,0.00,1 +2,Entry long,2025-07-02 22:00,M,11.48,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.5733,0.00,1 +3,Exit long,2025-07-02 22:45,Close position order,11.51,100,1147,3.7702,0.33,0.2298,4.8853,0.43,-3.1147,-0.27,3.3116,0.00,2 +3,Entry long,2025-07-02 22:15,L,11.47,100,1147,3.7702,0.33,0.2298,4.8853,0.43,-3.1147,-0.27,3.3116,0.00,2 +4,Exit long,2025-07-03 22:30,TX,11.85,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,4.0747,0.00,1 +4,Entry long,2025-07-03 22:15,E,11.84,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,4.0747,0.00,1 +5,Exit long,2025-07-03 22:45,Close entry(s) order M,11.86,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,2.7188,0.00,1 +5,Entry long,2025-07-03 22:30,M,11.87,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,2.7188,0.00,1 +6,Exit long,2025-07-03 23:15,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,0.6004,0.00,2 +6,Entry long,2025-07-03 22:45,L,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,0.6004,0.00,2 +7,Exit long,2025-07-07 21:45,TX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-1.633,0.00,1 +7,Entry long,2025-07-07 21:30,E,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-1.633,0.00,1 +8,Exit long,2025-07-07 22:00,Close entry(s) order M,11.76,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,9.0153,0.01,1 +8,Entry long,2025-07-07 21:45,M,11.65,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,9.0153,0.01,1 +9,Exit long,2025-07-07 22:30,Close position order,11.7,100,1176,-6.2346,-0.53,0.2346,0,0.00,-9.1176,-0.78,2.8983,0.00,2 +9,Entry long,2025-07-07 22:00,L,11.76,100,1176,-6.2346,-0.53,0.2346,0,0.00,-9.1176,-0.78,2.8983,0.00,2 \ No newline at end of file diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/meta.json b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/meta.json new file mode 100644 index 00000000..6c8640fe --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I1 xs-dyn-pooc", + "slug": "pa2-i1-xs-dyn-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:55:39.011Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "eecf51587b38b0cbe63d5b3e70d027cf4ddfa16397f3d69fc3bb0a567047ad90", + "tv_trades_sha256": "54720b55cf52d84d1455db4abc4ef12c119dfae53013ddfdff6d2df6e5332013", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/metrics.json b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/metrics.json new file mode 100644 index 00000000..90fd8434 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i1-xs-dyn-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "eecf51587b38b0cbe63d5b3e70d027cf4ddfa16397f3d69fc3bb0a567047ad90", + "tvTradesCsvHash": "54720b55cf52d84d1455db4abc4ef12c119dfae53013ddfdff6d2df6e5332013", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75502", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "2a064cfaf19051e851c55937594e71b78184930b5744ebc8a476ecda855437c5", + "decodedReportSha256": "91fc12cef13e90f7306ecd4926c5d4111730791e54e7c51e75fe23d95d9f793b", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:55:39.011Z" +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/strategy.pine b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/strategy.pine new file mode 100644 index 00000000..06eed3f8 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/strategy.pine @@ -0,0 +1,47 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 1: a strategy.exit placed in a calc_on_order_fills +// recalculation on a FLAT book, for an entry id that has no order yet. Market entry E +// fills at bar j's open and its exit TX sits inside bar j's first leg (a limit on a +// high-first bar, a stop on a low-first one), so TX flattens the book mid-leg; the +// recalculation that fill starts places the exit XS of a SHORT id S -- a stop 50c BELOW the price, so crossed as soon as S is open, +// and a market entry M. On bar j+1 M is closed and the id entered; on bar j+3 everything +// is closed. Cycles j = 2, 30, 41 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I1 xs-dyn-pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +n = strategy.closedtrades +justFlat = units == 0 and n > 0 and strategy.closedtrades.exit_id(n - 1) == "TX" and strategy.closedtrades.exit_bar_index(n - 1) == bar_index +lvl = k == 1 ? 11.46 : k == 29 ? 11.85 : k == 40 ? 11.66 : na +if k >= 0 and time < tEnd + if not na(lvl) and units == 0 + strategy.entry("E", strategy.long) + if k == 40 + strategy.exit("TX", "E", stop=lvl) + else + strategy.exit("TX", "E", limit=lvl) + if justFlat + strategy.exit("XS", "S", limit=(t - 100) / 100, stop=(t - 50) / 100) + strategy.entry("M", strategy.long) + if (k == 3 or k == 31 or k == 42) and units != 0 + strategy.close("M") + strategy.entry("S", strategy.short) + if (k == 5 or k == 33 or k == 44) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/tv_trades.csv b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/tv_trades.csv new file mode 100644 index 00000000..f1ad8b78 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn-pooc/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:00,TX,11.46,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +1,Entry long,2025-07-02 21:45,E,11.45,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +2,Exit long,2025-07-02 22:15,Close entry(s) order M,11.47,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.4586,0.00,1 +2,Entry long,2025-07-02 22:00,M,11.48,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.4586,0.00,1 +3,Exit short,2025-07-02 22:45,Close position order,11.51,100,1147,-4.2298,-0.37,0.2298,2.8853,0.25,-5.1147,-0.45,-4.6884,0.00,2 +3,Entry short,2025-07-02 22:15,S,11.47,100,1147,-4.2298,-0.37,0.2298,2.8853,0.25,-5.1147,-0.45,-4.6884,0.00,2 +4,Exit long,2025-07-03 22:30,TX,11.85,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,-3.9253,0.00,1 +4,Entry long,2025-07-03 22:15,E,11.84,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,-3.9253,0.00,1 +5,Exit long,2025-07-03 22:45,Close entry(s) order M,11.86,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,-5.1626,-0.01,1 +5,Entry long,2025-07-03 22:30,M,11.87,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,-5.1626,-0.01,1 +6,Exit short,2025-07-03 23:15,Close position order,11.84,100,1186,1.763,0.15,0.237,5.8814,0.50,-0.1186,-0.01,-3.3996,0.00,2 +6,Entry short,2025-07-03 22:45,S,11.86,100,1186,1.763,0.15,0.237,5.8814,0.50,-0.1186,-0.01,-3.3996,0.00,2 +7,Exit long,2025-07-07 21:45,TX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-5.633,-0.01,1 +7,Entry long,2025-07-07 21:30,E,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-5.633,-0.01,1 +8,Exit long,2025-07-07 22:00,Close entry(s) order M,11.76,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,5.1329,0.01,1 +8,Entry long,2025-07-07 21:45,M,11.65,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,5.1329,0.01,1 +9,Exit short,2025-07-07 22:30,Close position order,11.7,100,1176,5.7654,0.49,0.2346,8.8824,0.76,-0.1176,-0.01,10.8983,0.01,2 +9,Entry short,2025-07-07 22:00,S,11.76,100,1176,5.7654,0.49,0.2346,8.8824,0.76,-0.1176,-0.01,10.8983,0.01,2 \ No newline at end of file diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/meta.json b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/meta.json new file mode 100644 index 00000000..c5223300 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I1 xs-dyn", + "slug": "pa2-i1-xs-dyn", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:55:34.024Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "352d237f37244aa1d627ee8d1d3fa5993523a261990531527491c310d24417bc", + "tv_trades_sha256": "626653faf60e832fc66d8cde05c62315193ca08faa8332ff36bfe7d9326f34ff", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/metrics.json b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/metrics.json new file mode 100644 index 00000000..8e74ad00 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i1-xs-dyn", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "352d237f37244aa1d627ee8d1d3fa5993523a261990531527491c310d24417bc", + "tvTradesCsvHash": "626653faf60e832fc66d8cde05c62315193ca08faa8332ff36bfe7d9326f34ff", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75495", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f8a6906bd142cb51da89c8ff822e24a172c87d21926b259fbdcf147728fb6921", + "decodedReportSha256": "6cd0063985e14f9e11f01bb61e4187762540f9b711f41589ea1e64142e73e61a", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:55:34.023Z" +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/strategy.pine b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/strategy.pine new file mode 100644 index 00000000..a7d7378e --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/strategy.pine @@ -0,0 +1,47 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 1: a strategy.exit placed in a calc_on_order_fills +// recalculation on a FLAT book, for an entry id that has no order yet. Market entry E +// fills at bar j's open and its exit TX sits inside bar j's first leg (a limit on a +// high-first bar, a stop on a low-first one), so TX flattens the book mid-leg; the +// recalculation that fill starts places the exit XS of a SHORT id S -- a stop 50c BELOW the price, so crossed as soon as S is open, +// and a market entry M. On bar j+1 M is closed and the id entered; on bar j+3 everything +// is closed. Cycles j = 2, 30, 41 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I1 xs-dyn", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +n = strategy.closedtrades +justFlat = units == 0 and n > 0 and strategy.closedtrades.exit_id(n - 1) == "TX" and strategy.closedtrades.exit_bar_index(n - 1) == bar_index +lvl = k == 1 ? 11.46 : k == 29 ? 11.85 : k == 40 ? 11.66 : na +if k >= 0 and time < tEnd + if not na(lvl) and units == 0 + strategy.entry("E", strategy.long) + if k == 40 + strategy.exit("TX", "E", stop=lvl) + else + strategy.exit("TX", "E", limit=lvl) + if justFlat + strategy.exit("XS", "S", limit=(t - 100) / 100, stop=(t - 50) / 100) + strategy.entry("M", strategy.long) + if (k == 3 or k == 31 or k == 42) and units != 0 + strategy.close("M") + strategy.entry("S", strategy.short) + if (k == 5 or k == 33 or k == 44) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/tv_trades.csv b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/tv_trades.csv new file mode 100644 index 00000000..35523c42 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-dyn/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:00,TX,11.46,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,0 +1,Entry long,2025-07-02 22:00,E,11.45,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,0 +2,Exit long,2025-07-02 22:30,Close entry(s) order M,11.47,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.4586,0.00,2 +2,Entry long,2025-07-02 22:00,M,11.48,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.4586,0.00,2 +3,Exit short,2025-07-02 23:00,Close position order,11.5,100,1147,-3.2297,-0.28,0.2297,2.8853,0.25,-5.1147,-0.45,-3.6883,0.00,2 +3,Entry short,2025-07-02 22:30,S,11.47,100,1147,-3.2297,-0.28,0.2297,2.8853,0.25,-5.1147,-0.45,-3.6883,0.00,2 +4,Exit long,2025-07-03 22:30,TX,11.85,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,-2.9252,0.00,0 +4,Entry long,2025-07-03 22:30,E,11.84,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,-2.9252,0.00,0 +5,Exit long,2025-07-03 23:00,Close entry(s) order M,11.86,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,-4.1625,0.00,2 +5,Entry long,2025-07-03 22:30,M,11.87,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,-4.1625,0.00,2 +6,Exit short,2025-07-03 23:30,Close position order,11.84,100,1186,1.763,0.15,0.237,5.8814,0.50,-0.1186,-0.01,-2.3995,0.00,2 +6,Entry short,2025-07-03 23:00,S,11.86,100,1186,1.763,0.15,0.237,5.8814,0.50,-0.1186,-0.01,-2.3995,0.00,2 +7,Exit long,2025-07-07 21:45,TX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-4.6329,0.00,0 +7,Entry long,2025-07-07 21:45,E,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-4.6329,0.00,0 +8,Exit long,2025-07-07 22:15,Close entry(s) order M,11.76,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,6.133,0.01,2 +8,Entry long,2025-07-07 21:45,M,11.65,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,6.133,0.01,2 +9,Exit short,2025-07-07 22:45,Close position order,11.7,100,1176,5.7654,0.49,0.2346,8.8824,0.76,-0.1176,-0.01,11.8984,0.01,2 +9,Entry short,2025-07-07 22:15,S,11.76,100,1176,5.7654,0.49,0.2346,8.8824,0.76,-0.1176,-0.01,11.8984,0.01,2 \ No newline at end of file diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/meta.json b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/meta.json new file mode 100644 index 00000000..de5139aa --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I1 xs-qty-pooc", + "slug": "pa2-i1-xs-qty-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:55:46.688Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "afbef66c9c76f14a0a3fea5eea90883c38dadf0829e38d3c38d25e066fc2163b", + "tv_trades_sha256": "54720b55cf52d84d1455db4abc4ef12c119dfae53013ddfdff6d2df6e5332013", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/metrics.json b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/metrics.json new file mode 100644 index 00000000..db4a79de --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i1-xs-qty-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "afbef66c9c76f14a0a3fea5eea90883c38dadf0829e38d3c38d25e066fc2163b", + "tvTradesCsvHash": "54720b55cf52d84d1455db4abc4ef12c119dfae53013ddfdff6d2df6e5332013", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75512", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "5b5f0412e93f9bfba4ac5cea81496b1cafa00210648733b21a8e1aebcbbdef75", + "decodedReportSha256": "91fc12cef13e90f7306ecd4926c5d4111730791e54e7c51e75fe23d95d9f793b", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:55:46.687Z" +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/strategy.pine b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/strategy.pine new file mode 100644 index 00000000..33abd332 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/strategy.pine @@ -0,0 +1,47 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 1: a strategy.exit placed in a calc_on_order_fills +// recalculation on a FLAT book, for an entry id that has no order yet. Market entry E +// fills at bar j's open and its exit TX sits inside bar j's first leg (a limit on a +// high-first bar, a stop on a low-first one), so TX flattens the book mid-leg; the +// recalculation that fill starts places the exit XS of a SHORT id S -- a stop 50c BELOW the price, so crossed as soon as S is open, with qty=100, +// and a market entry M. On bar j+1 M is closed and the id entered; on bar j+3 everything +// is closed. Cycles j = 2, 30, 41 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I1 xs-qty-pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +n = strategy.closedtrades +justFlat = units == 0 and n > 0 and strategy.closedtrades.exit_id(n - 1) == "TX" and strategy.closedtrades.exit_bar_index(n - 1) == bar_index +lvl = k == 1 ? 11.46 : k == 29 ? 11.85 : k == 40 ? 11.66 : na +if k >= 0 and time < tEnd + if not na(lvl) and units == 0 + strategy.entry("E", strategy.long) + if k == 40 + strategy.exit("TX", "E", stop=lvl) + else + strategy.exit("TX", "E", limit=lvl) + if justFlat + strategy.exit("XS", "S", limit=(t - 100) / 100, stop=(t - 50) / 100, qty=100) + strategy.entry("M", strategy.long) + if (k == 3 or k == 31 or k == 42) and units != 0 + strategy.close("M") + strategy.entry("S", strategy.short) + if (k == 5 or k == 33 or k == 44) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/tv_trades.csv b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/tv_trades.csv new file mode 100644 index 00000000..f1ad8b78 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty-pooc/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:00,TX,11.46,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +1,Entry long,2025-07-02 21:45,E,11.45,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,1 +2,Exit long,2025-07-02 22:15,Close entry(s) order M,11.47,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.4586,0.00,1 +2,Entry long,2025-07-02 22:00,M,11.48,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.4586,0.00,1 +3,Exit short,2025-07-02 22:45,Close position order,11.51,100,1147,-4.2298,-0.37,0.2298,2.8853,0.25,-5.1147,-0.45,-4.6884,0.00,2 +3,Entry short,2025-07-02 22:15,S,11.47,100,1147,-4.2298,-0.37,0.2298,2.8853,0.25,-5.1147,-0.45,-4.6884,0.00,2 +4,Exit long,2025-07-03 22:30,TX,11.85,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,-3.9253,0.00,1 +4,Entry long,2025-07-03 22:15,E,11.84,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,-3.9253,0.00,1 +5,Exit long,2025-07-03 22:45,Close entry(s) order M,11.86,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,-5.1626,-0.01,1 +5,Entry long,2025-07-03 22:30,M,11.87,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,-5.1626,-0.01,1 +6,Exit short,2025-07-03 23:15,Close position order,11.84,100,1186,1.763,0.15,0.237,5.8814,0.50,-0.1186,-0.01,-3.3996,0.00,2 +6,Entry short,2025-07-03 22:45,S,11.86,100,1186,1.763,0.15,0.237,5.8814,0.50,-0.1186,-0.01,-3.3996,0.00,2 +7,Exit long,2025-07-07 21:45,TX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-5.633,-0.01,1 +7,Entry long,2025-07-07 21:30,E,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-5.633,-0.01,1 +8,Exit long,2025-07-07 22:00,Close entry(s) order M,11.76,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,5.1329,0.01,1 +8,Entry long,2025-07-07 21:45,M,11.65,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,5.1329,0.01,1 +9,Exit short,2025-07-07 22:30,Close position order,11.7,100,1176,5.7654,0.49,0.2346,8.8824,0.76,-0.1176,-0.01,10.8983,0.01,2 +9,Entry short,2025-07-07 22:00,S,11.76,100,1176,5.7654,0.49,0.2346,8.8824,0.76,-0.1176,-0.01,10.8983,0.01,2 \ No newline at end of file diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/meta.json b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/meta.json new file mode 100644 index 00000000..234086f2 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I1 xs-qty", + "slug": "pa2-i1-xs-qty", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:55:43.227Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "ff279fc3b5090d36f6bdc1ba8bda05249a8098779b312b2a1a345fd816b2dd7c", + "tv_trades_sha256": "626653faf60e832fc66d8cde05c62315193ca08faa8332ff36bfe7d9326f34ff", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/metrics.json b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/metrics.json new file mode 100644 index 00000000..952bdee2 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i1-xs-qty", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "ff279fc3b5090d36f6bdc1ba8bda05249a8098779b312b2a1a345fd816b2dd7c", + "tvTradesCsvHash": "626653faf60e832fc66d8cde05c62315193ca08faa8332ff36bfe7d9326f34ff", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75507", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "93b6035549ce8ff5f300fd742d93f2d4b506399156505ba291d9587fb0b51d25", + "decodedReportSha256": "6cd0063985e14f9e11f01bb61e4187762540f9b711f41589ea1e64142e73e61a", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:55:43.227Z" +} diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/strategy.pine b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/strategy.pine new file mode 100644 index 00000000..778fda44 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/strategy.pine @@ -0,0 +1,47 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 1: a strategy.exit placed in a calc_on_order_fills +// recalculation on a FLAT book, for an entry id that has no order yet. Market entry E +// fills at bar j's open and its exit TX sits inside bar j's first leg (a limit on a +// high-first bar, a stop on a low-first one), so TX flattens the book mid-leg; the +// recalculation that fill starts places the exit XS of a SHORT id S -- a stop 50c BELOW the price, so crossed as soon as S is open, with qty=100, +// and a market entry M. On bar j+1 M is closed and the id entered; on bar j+3 everything +// is closed. Cycles j = 2, 30, 41 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I1 xs-qty", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +n = strategy.closedtrades +justFlat = units == 0 and n > 0 and strategy.closedtrades.exit_id(n - 1) == "TX" and strategy.closedtrades.exit_bar_index(n - 1) == bar_index +lvl = k == 1 ? 11.46 : k == 29 ? 11.85 : k == 40 ? 11.66 : na +if k >= 0 and time < tEnd + if not na(lvl) and units == 0 + strategy.entry("E", strategy.long) + if k == 40 + strategy.exit("TX", "E", stop=lvl) + else + strategy.exit("TX", "E", limit=lvl) + if justFlat + strategy.exit("XS", "S", limit=(t - 100) / 100, stop=(t - 50) / 100, qty=100) + strategy.entry("M", strategy.long) + if (k == 3 or k == 31 or k == 42) and units != 0 + strategy.close("M") + strategy.entry("S", strategy.short) + if (k == 5 or k == 33 or k == 44) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/tv_trades.csv b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/tv_trades.csv new file mode 100644 index 00000000..35523c42 --- /dev/null +++ b/tests/fixtures/flat_coof_exit/pa2-i1-xs-qty/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:00,TX,11.46,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,0 +1,Entry long,2025-07-02 22:00,E,11.45,100,1145,0.7709,0.07,0.2291,0.8855,0.08,-0.1145,-0.01,0.7709,0.00,0 +2,Exit long,2025-07-02 22:30,Close entry(s) order M,11.47,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.4586,0.00,2 +2,Entry long,2025-07-02 22:00,M,11.48,100,1148,-1.2295,-0.11,0.2295,0.8852,0.08,-7.1148,-0.62,-0.4586,0.00,2 +3,Exit short,2025-07-02 23:00,Close position order,11.5,100,1147,-3.2297,-0.28,0.2297,2.8853,0.25,-5.1147,-0.45,-3.6883,0.00,2 +3,Entry short,2025-07-02 22:30,S,11.47,100,1147,-3.2297,-0.28,0.2297,2.8853,0.25,-5.1147,-0.45,-3.6883,0.00,2 +4,Exit long,2025-07-03 22:30,TX,11.85,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,-2.9252,0.00,0 +4,Entry long,2025-07-03 22:30,E,11.84,100,1184,0.7631,0.06,0.2369,0.8816,0.07,-0.1184,-0.01,-2.9252,0.00,0 +5,Exit long,2025-07-03 23:00,Close entry(s) order M,11.86,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,-4.1625,0.00,2 +5,Entry long,2025-07-03 22:30,M,11.87,100,1187,-1.2373,-0.10,0.2373,0,0.00,-7.1187,-0.60,-4.1625,0.00,2 +6,Exit short,2025-07-03 23:30,Close position order,11.84,100,1186,1.763,0.15,0.237,5.8814,0.50,-0.1186,-0.01,-2.3995,0.00,2 +6,Entry short,2025-07-03 23:00,S,11.86,100,1186,1.763,0.15,0.237,5.8814,0.50,-0.1186,-0.01,-2.3995,0.00,2 +7,Exit long,2025-07-07 21:45,TX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-4.6329,0.00,0 +7,Entry long,2025-07-07 21:45,E,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,-4.6329,0.00,0 +8,Exit long,2025-07-07 22:15,Close entry(s) order M,11.76,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,6.133,0.01,2 +8,Entry long,2025-07-07 21:45,M,11.65,100,1165,10.7659,0.92,0.2341,13.8835,1.19,0,0.00,6.133,0.01,2 +9,Exit short,2025-07-07 22:45,Close position order,11.7,100,1176,5.7654,0.49,0.2346,8.8824,0.76,-0.1176,-0.01,11.8984,0.01,2 +9,Entry short,2025-07-07 22:15,S,11.76,100,1176,5.7654,0.49,0.2346,8.8824,0.76,-0.1176,-0.01,11.8984,0.01,2 \ No newline at end of file diff --git a/tests/test_flat_coof_exit_tapes.cpp b/tests/test_flat_coof_exit_tapes.cpp new file mode 100644 index 00000000..cbefc917 --- /dev/null +++ b/tests/test_flat_coof_exit_tapes.cpp @@ -0,0 +1,375 @@ +/* + * test_flat_coof_exit_tapes.cpp -- R5 lane PAR-ORDERS-2 (item 1). + * + * A strategy.exit placed in a calc_on_order_fills recalculation on a FLAT book + * -- the recalculation a take-profit or stop-loss fill starts mid-way along the + * bar's first leg. The Pine adapter classifies the exit's stop at its birth + * against the book (exit(): an already-marketable stop goes to the next open + * as a market close, or with a quantity to the next waypoint), and a flat book + * has no side: it read a long's protective stop below the price as a short's + * buy stop, already reached. In the live-state battery (reversals seed + * 3246599) the delayed leg then reached the kernel's current execution as a + * cohort-bound close, which failed the run until the kernel refused it + * (tests/test_native_current_cohort_refusal.cpp), and closed the long at the + * next open though neither of its levels was touched. + * + * The lane's own synthetic scripts, NYSE:F 15m over the three days of + * tests/fixtures/session_islastbar (`lab tv`, ws-report-v1, rangeProof covered; + * tests/fixtures/flat_coof_exit, whose README names each tape). Market entry E + * fills at bar j's open and its exit TX sits inside that bar's first leg (a + * limit on a high-first bar, a stop on a low-first one), so TX flattens the + * book mid-leg (cycles j = 2, 30, 41), and the recalculation that fill starts + * pa2-i1-pend-xf-pooc places a buy limit entry L 2c below and its + * bracket XF 50c either side (never touched); + * pa2-i1-pend-xs-crossed-pooc places a sell limit entry S 2c above and its + * exit XS with the stop 50c BELOW the price -- + * crossed as soon as S is open (without the + * kernel's refusal this run failed); + * pa2-i1-xf-* / xs-* places an exit for an id with no order yet, a + * market entry M, and enters the id on bar j+1 + * (qty 100 or the whole position; with and without + * process_orders_on_close); + * pa2-i1-xf-tight-qty-* the same with a bracket 3c either side, reached + * once L is open -- and once without + * calc_on_order_fills, the exit placed at bar + * j-1's close calculation. + * + * What TradingView books, and what the rows pin: + * - A pending parent's protective bracket rests (pend-xf, 3 of 3 cycles): + * rows 1-5 exact. Row 6 is a RECORDED divergence of the limit entry itself + * (TradingView fills L at 11.65, this engine at 11.70), not of its exit. + * - A pending parent's crossed stop exits at the parent's own fill, on its + * bar (pend-xs-crossed, 3 of 3): this engine books it on the next bar + * (row 2) or at the stop's own level, far from the market (rows 4, 6). + * RECORDED; every run completes. + * - An exit placed while its id has no order at all is never active on + * TradingView: the id's trade runs to the close_all even with a 3c bracket + * (xf-tight-qty), and without calc_on_order_fills this engine agrees + * (xf-tight-qty-nocoof: exact). Inside a recalculation it does not: an + * exit with a quantity is re-targeted to the next waypoint and closes the + * trade a bar after its entry (xf-qty rows 3 and 6 / 6, xs-qty rows 3 and + * 9), and a reached bracket fills (xf-tight-qty rows 3, 6, 9). RECORDED. + * Every other row is exact: side, instants, prices, quantity, signals, bars. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; + +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +#ifndef PINEFORGE_PA2_FLAT_COOF_FIXTURE_DIR +#error "PINEFORGE_PA2_FLAT_COOF_FIXTURE_DIR must name tests/fixtures/flat_coof_exit" +#endif + +namespace { + +struct FeedBar { + std::int64_t ts; + double open, high, low, close, volume; +}; + +#include "fixtures/session_islastbar/bars.inc" + +constexpr std::int64_t kMinute = 60'000; +const double kNaN = std::numeric_limits::quiet_NaN(); + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +// "YYYY-MM-DD HH:MM" in the tape's UTC+8 -> UTC milliseconds. +std::int64_t tape_ms(const std::string& text) { + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + if (std::sscanf(text.c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) return -1; + const std::int64_t days = days_from_civil(y, static_cast(mo), + static_cast(d)); + return ((days * 24 + h - 8) * 60 + mi) * kMinute; +} + +struct TapeTrade { + bool is_long = true; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = 0.0, exit_price = 0.0, qty = 0.0; + std::string entry_signal, exit_signal; +}; + +std::vector read_tape(const std::string& slug) { + std::ifstream in(std::string(PINEFORGE_PA2_FLAT_COOF_FIXTURE_DIR) + "/" + slug + + "/tv_trades.csv"); + std::vector trades; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 17) continue; + const std::size_t n = static_cast(std::stoi(cell[0])); + if (trades.size() < n) trades.resize(n); + TapeTrade& t = trades[n - 1]; + const bool entry = cell[1].rfind("Entry", 0) == 0; + t.is_long = cell[1].find("long") != std::string::npos; + (entry ? t.entry_ms : t.exit_ms) = tape_ms(cell[2]); + (entry ? t.entry_signal : t.exit_signal) = cell[3]; + (entry ? t.entry_price : t.exit_price) = std::stod(cell[4]); + t.qty = std::stod(cell[5]); + } + return trades; +} + +// floor(x * 100 + 1e-6): the scripts' cent grid (NYSE:F mintick 0.01). +double cents(double x) { return std::floor(x * 100.0 + 1e-6); } + +enum class Shape { + PendingBracket, // pend-xf + PendingCrossed, // pend-xs-crossed + NoOrderLong, // xf-*: exit for L with no order yet + NoOrderShort, // xs-*: exit for S with no order yet + NoOrderTight, // xf-tight-qty: the bracket 3c either side + NoOrderTightBar, // xf-tight-qty-nocoof: placed at bar j-1's close +}; + +struct Variant { + const char* slug; + Shape shape; + bool qty; // the exit carries qty=100 + bool pooc; + int pyramiding; + unsigned recorded; // bit i+1: row i+1 is a RECORDED divergence +}; + +// The Pine probes' bodies (tests/fixtures/flat_coof_exit//strategy.pine). +class FlatExitHost final : public source::PineStrategyHost { +public: + explicit FlatExitHost(const Variant& v) : v_(v) { + set_syminfo_session("0930-1600"); + set_syminfo_timezone("America/New_York"); + source::PineStrategyConfig c; + c.initial_capital = 100000; + c.default_qty_type = static_cast(QtyType::FIXED); + c.default_qty_value = 100; + c.pyramiding = v.pyramiding; + c.process_orders_on_close = v.pooc; + c.calc_on_order_fills = v.shape != Shape::NoOrderTightBar; + c.slippage = 0; + c.commission_type = static_cast(CommissionType::PERCENT); + c.commission_value = 0.01; + c.margin_long = 100; + c.margin_short = 100; + configure_pine_strategy(c); + syminfo_mintick_ = 0.01; + } + void on_source_bar(const Bar& b) override { + const int k = bar_index_; // the replay's first bar is the Pine counter's origin + const double units = signed_position_size(); + const int n = trade_count(); + const bool just_flat = units == 0.0 && n > 0 && closed_trade_exit_id(n - 1) == "TX" + && closed_trade_exit_bar_index(n - 1) == pine_bar_index(); + const double lvl = k == 1 ? 11.46 : k == 29 ? 11.85 : k == 40 ? 11.66 : kNaN; + const double t = cents(b.close); + const double qty = v_.qty ? 100.0 : kNaN; + if (!std::isnan(lvl) && units == 0.0) { + strategy_entry("E", true); + if (k == 40) strategy_exit("TX", "E", kNaN, lvl); + else strategy_exit("TX", "E", lvl, kNaN); + if (v_.shape == Shape::NoOrderTightBar) + strategy_exit("XF", "L", (t + 3.0) / 100.0, (t - 3.0) / 100.0, kNaN, kNaN, kNaN, + 100.0, {}, 100.0); + } + if (just_flat) { + switch (v_.shape) { + case Shape::PendingBracket: + strategy_entry("L", true, (t - 2.0) / 100.0); + strategy_exit("XF", "L", (t + 50.0) / 100.0, (t - 50.0) / 100.0); + break; + case Shape::PendingCrossed: + strategy_entry("S", false, (t + 2.0) / 100.0); + strategy_exit("XS", "S", (t - 100.0) / 100.0, (t - 50.0) / 100.0); + break; + case Shape::NoOrderLong: + strategy_exit("XF", "L", (t + 50.0) / 100.0, (t - 50.0) / 100.0, kNaN, kNaN, + kNaN, 100.0, {}, qty); + strategy_entry("M", true); + break; + case Shape::NoOrderShort: + strategy_exit("XS", "S", (t - 100.0) / 100.0, (t - 50.0) / 100.0, kNaN, kNaN, + kNaN, 100.0, {}, qty); + strategy_entry("M", true); + break; + case Shape::NoOrderTight: + strategy_exit("XF", "L", (t + 3.0) / 100.0, (t - 3.0) / 100.0, kNaN, kNaN, + kNaN, 100.0, {}, 100.0); + strategy_entry("M", true); + break; + case Shape::NoOrderTightBar: + break; + } + } + const bool reenter = k == 3 || k == 31 || k == 42; + if (v_.shape == Shape::NoOrderTightBar) { + if (reenter && units == 0.0) strategy_entry("L", true); + } else if (v_.shape != Shape::PendingBracket && v_.shape != Shape::PendingCrossed + && reenter && units != 0.0) { + strategy_close("M"); + const bool long_id = v_.shape != Shape::NoOrderShort; + strategy_entry(long_id ? "L" : "S", long_id); + } + if ((k == 5 || k == 33 || k == 44) && units != 0.0) strategy_close_all(); + } + + struct Row { + bool is_long = true; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = 0.0, exit_price = 0.0, qty = 0.0; + int entry_bar = 0, exit_bar = 0; + std::string entry_id, exit_id; + }; + std::vector rows() const { + std::vector out; + for (int i = 0; i < trade_count(); ++i) { + Row r; + r.is_long = trades_[static_cast(i)].is_long; + r.entry_ms = closed_trade_entry_time(i); + r.exit_ms = closed_trade_exit_time(i); + r.entry_price = closed_trade_entry_price(i); + r.exit_price = closed_trade_exit_price(i); + r.qty = closed_trade_size(i); + r.entry_bar = closed_trade_entry_bar_index(i); + r.exit_bar = closed_trade_exit_bar_index(i); + r.entry_id = closed_trade_entry_id(i); + r.exit_id = closed_trade_exit_id(i); + out.push_back(r); + } + return out; + } + double open_units() const { return physical_position().signed_units; } + +private: + Variant v_; +}; + +template +std::vector feed(const FeedBar (&rows)[N]) { + std::vector bars; + for (const FeedBar& row : rows) { + Bar b{}; + b.timestamp = row.ts; + b.open = row.open; b.high = row.high; b.low = row.low; b.close = row.close; + b.volume = row.volume; + bars.push_back(b); + } + return bars; +} + +int chart_bar_of(std::int64_t ms) { + for (std::size_t i = 0; i < sizeof(kFord15) / sizeof(kFord15[0]); ++i) + if (kFord15[i].ts == ms) return static_cast(i); + return -1; +} + +// TradingView's exit signals: "Close entry(s) order X" for strategy.close("X"), +// "Close position order" for strategy.close_all. +std::string exit_signal(const FlatExitHost::Row& r) { + if (r.exit_id.rfind("__close__", 0) == 0 && r.exit_id.size() > 9) + return "Close entry(s) order " + r.exit_id.substr(9); + return r.exit_id.rfind("__", 0) == 0 ? "Close position order" : r.exit_id; +} + +bool near(double a, double b) { return std::fabs(a - b) <= 1e-9; } + +void replay(const Variant& v) { + std::printf("-- %s\n", v.slug); + static const std::vector fifteen = feed(kFord15); + FlatExitHost host(v); + host.run(fifteen.data(), static_cast(fifteen.size()), "15", "15", false); + // Every run completes: the pending crossed stop used to end it. + CHECK(host.last_error().empty()); + if (!host.last_error().empty()) std::printf(" error: %s\n", host.last_error().c_str()); + const auto tape = read_tape(v.slug); + const auto rows = host.rows(); + CHECK(!tape.empty()); + CHECK(rows.size() == tape.size()); + CHECK(host.open_units() == 0.0); + int differing = 0; + for (std::size_t i = 0; i < rows.size() && i < tape.size(); ++i) { + const TapeTrade& tv = tape[i]; + const FlatExitHost::Row& r = rows[i]; + const bool ok = r.is_long == tv.is_long && r.entry_ms == tv.entry_ms + && r.exit_ms == tv.exit_ms && near(r.entry_price, tv.entry_price) + && near(r.exit_price, tv.exit_price) && r.qty == tv.qty + && r.entry_id == tv.entry_signal && exit_signal(r) == tv.exit_signal + && r.entry_bar == chart_bar_of(tv.entry_ms) && r.exit_bar == chart_bar_of(tv.exit_ms); + const bool divergence = (v.recorded & (1u << (i + 1))) != 0; + CHECK(ok != divergence); + if (divergence) std::printf(" row %zu: RECORDED divergence\n", i + 1); + if (!ok) { + ++differing; + std::printf(" row %zu: TradingView %s %s bar %d @%.2f -> bar %d @%.2f %s\n" + " engine %s %s bar %d @%.2f -> bar %d @%.2f %s\n", + i + 1, tv.entry_signal.c_str(), tv.is_long ? "L" : "S", + chart_bar_of(tv.entry_ms), tv.entry_price, chart_bar_of(tv.exit_ms), + tv.exit_price, tv.exit_signal.c_str(), r.entry_id.c_str(), + r.is_long ? "L" : "S", r.entry_bar, r.entry_price, r.exit_bar, + r.exit_price, exit_signal(r).c_str()); + } + } + std::printf(" %zu rows, %d differ\n", rows.size(), differing); +} + +unsigned rows(std::initializer_list list) { + unsigned mask = 0; + for (const int row : list) mask |= 1u << row; + return mask; +} + +} // namespace + +int main() { + const Variant variants[] = { + {"pa2-i1-pend-xf-pooc", Shape::PendingBracket, false, true, 1, rows({6})}, + {"pa2-i1-pend-xs-crossed-pooc", Shape::PendingCrossed, false, true, 1, rows({2, 4, 6})}, + {"pa2-i1-xf-qty-pooc", Shape::NoOrderLong, true, true, 1, rows({3, 6})}, + {"pa2-i1-xf-qty-pyr2", Shape::NoOrderLong, true, false, 2, rows({6})}, + {"pa2-i1-xf-dyn-pooc", Shape::NoOrderLong, false, true, 1, 0u}, + {"pa2-i1-xs-dyn-pooc", Shape::NoOrderShort, false, true, 1, 0u}, + {"pa2-i1-xs-dyn", Shape::NoOrderShort, false, false, 1, 0u}, + {"pa2-i1-xs-qty-pooc", Shape::NoOrderShort, true, true, 1, rows({3, 9})}, + {"pa2-i1-xs-qty", Shape::NoOrderShort, true, false, 1, rows({3, 9})}, + {"pa2-i1-xf-tight-qty-pooc", Shape::NoOrderTight, true, true, 1, rows({3, 6, 9})}, + {"pa2-i1-xf-tight-qty-nocoof-pooc", Shape::NoOrderTightBar, true, true, 1, 0u}, + }; + for (const Variant& v : variants) replay(v); + std::printf("\n%s flat recalculation exit tapes: %d checks, %d failures\n", + tests_failed == 0 ? "PASS" : "FAIL", tests_passed + tests_failed, tests_failed); + return tests_failed == 0 ? 0 : 1; +} From d8c69f3df72d318ca3caec82327ee47ace9920c4 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:27:50 +0800 Subject: [PATCH 34/83] Adapter: a recalculation's market order at the matcher's fill on a bar's second extreme fills there; a fill forced onto it still advances (R5 lane PAR-ORDERS-2, item 2, H-MEASURE Finding 6d) defer_coof_tail() and entry()'s coof_market_next_open advanced every calc_on_order_fills fill booked AT the bar's second extreme to the close point, which admits no cascade order, so the recalculation's market entry filled at the next open (or the process_orders_on_close close) and its exits were held off that bar. TradingView tells two such fills apart: - the matcher's fill of a resting request at its own level ends the leg to the extreme: the recalculation's market orders and exits are live from the extreme (row 5 of hm-chart-diff-v2 / -v3 / -v7, 11.72 / 11.72 / 11.74; lab tv pa2-i2-exit-w2-{long,short}{,-pooc}); - a fill the adapter forced onto the extreme (an order an earlier recalculation of the bar placed, gap-filled there: forced_execution_price) starts the next leg: its recalculation's market orders still advance (lab tv pa2-i2-point-w2-short{,-pooc}; corpus bracket-rivet-calc-on-fill-01 trades 350 and 516, which PAR-ORDERS' dfix3 experiment -- the first rule for both -- moved away from TradingView). begin_coof_recalc records which (coof_fill_forced_, a bool in the padding before the next 8-byte member: sizeof(PineExecutionAdapter) 3768 and sizeof(PineStrategyHost) 7704 unchanged, folded into the state hash only inside a recalculation, like coof_fill_cursor_t_); coof_fill_at_second_extreme names the point; entry() fills a matcher fill's market entry at the extreme and keeps the base route for a forced one; defer_coof_tail keeps deferring only a forced fill's tail. Pinned rows moved: the frozen L0 oracle tests/oracle/test_oracle_coof.cpp (twin test_native_oracle_coof_l2) and its copy tests/test_coof_cascade_eligibility.cpp, row R2 -- expectation corrected: the cascade re-entry's lot 96 (bar 2's open) -> 90 (bar 1's W2), because X's stop is the matcher's fill at its own level on the leg to W2 (the CHECK count is unchanged; tests/oracle.sha256 re-pinned: test_oracle_coof.cpp a175d444 -> 647f8a24, tree 7e128176 -> f39b54fe). Rows: test_magnified_aggregated_tape row 5 of v3 / v7 un-recorded and hm-chart-diff-v2 replayed (chart and aggregated, exact; 6 failures on the base); test_second_extreme_order_tapes (new, 100 checks; 13 failures on the base), whose boundary tapes pa2-i2-level-w1-* and pa2-i2-point-w2-long* pin two older divergences as RECORDED rows (the lane's base answers them alike). Release floor 681. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 4985dcaa408ccabe3236671e6291f9bb5125498a) INT26 pick note: the release floor is summed (696 -> 697) and its two citations name the assignment line. The design page's P4 row keeps this branch's symbol citations (`PineExecutionAdapter::hash_state`, `PineScheduler::hash_state`, H-DOCGATES' "a content pin accompanies a claim, never replaces it"), so the lane's re-hash of the retired content pin has nothing to update; the sentence itself is unchanged. The new flag's fold moves hash values only; they are re-harvested once, in the INT26 v19 hash re-pin. --- CONTRIBUTING.md | 2 +- docs/adr/0001-kernel-adapter-boundary.md | 34 +- docs/design/native-feature-parity.md | 158 ++++--- docs/pages/contributing-llm.md | 4 +- docs/pages/native-engine.md | 2 +- include/pineforge/source/pine_adapter.hpp | 6 + scripts/ci_verify.py | 4 +- src/source/pine_adapter.cpp | 67 ++- src/source/pine_state_hash.cpp | 1 + tests/CMakeLists.txt | 5 +- tests/fixtures/magnified_aggregated/README.md | 7 + .../hm-chart-diff-v2/meta.json | 17 + .../hm-chart-diff-v2/metrics.json | 40 ++ .../hm-chart-diff-v2/strategy.pine | 43 ++ .../hm-chart-diff-v2/tv_trades.csv | 25 + .../fixtures/second_extreme_orders/README.md | 40 ++ .../pa2-i2-exit-w2-long-pooc/meta.json | 16 + .../pa2-i2-exit-w2-long-pooc/metrics.json | 40 ++ .../pa2-i2-exit-w2-long-pooc/strategy.pine | 39 ++ .../pa2-i2-exit-w2-long-pooc/tv_trades.csv | 17 + .../pa2-i2-exit-w2-long/meta.json | 16 + .../pa2-i2-exit-w2-long/metrics.json | 40 ++ .../pa2-i2-exit-w2-long/strategy.pine | 39 ++ .../pa2-i2-exit-w2-long/tv_trades.csv | 17 + .../pa2-i2-exit-w2-short-pooc/meta.json | 16 + .../pa2-i2-exit-w2-short-pooc/metrics.json | 40 ++ .../pa2-i2-exit-w2-short-pooc/strategy.pine | 39 ++ .../pa2-i2-exit-w2-short-pooc/tv_trades.csv | 15 + .../pa2-i2-exit-w2-short/meta.json | 16 + .../pa2-i2-exit-w2-short/metrics.json | 40 ++ .../pa2-i2-exit-w2-short/strategy.pine | 39 ++ .../pa2-i2-exit-w2-short/tv_trades.csv | 15 + .../pa2-i2-level-w1-long-pooc/meta.json | 16 + .../pa2-i2-level-w1-long-pooc/metrics.json | 40 ++ .../pa2-i2-level-w1-long-pooc/strategy.pine | 40 ++ .../pa2-i2-level-w1-long-pooc/tv_trades.csv | 13 + .../pa2-i2-level-w1-long/meta.json | 16 + .../pa2-i2-level-w1-long/metrics.json | 40 ++ .../pa2-i2-level-w1-long/strategy.pine | 40 ++ .../pa2-i2-level-w1-long/tv_trades.csv | 13 + .../pa2-i2-level-w1-short-pooc/meta.json | 16 + .../pa2-i2-level-w1-short-pooc/metrics.json | 40 ++ .../pa2-i2-level-w1-short-pooc/strategy.pine | 40 ++ .../pa2-i2-level-w1-short-pooc/tv_trades.csv | 11 + .../pa2-i2-level-w1-short/meta.json | 16 + .../pa2-i2-level-w1-short/metrics.json | 40 ++ .../pa2-i2-level-w1-short/strategy.pine | 40 ++ .../pa2-i2-level-w1-short/tv_trades.csv | 13 + .../pa2-i2-point-w2-long-pooc/meta.json | 16 + .../pa2-i2-point-w2-long-pooc/metrics.json | 40 ++ .../pa2-i2-point-w2-long-pooc/strategy.pine | 44 ++ .../pa2-i2-point-w2-long-pooc/tv_trades.csv | 13 + .../pa2-i2-point-w2-long/meta.json | 16 + .../pa2-i2-point-w2-long/metrics.json | 40 ++ .../pa2-i2-point-w2-long/strategy.pine | 44 ++ .../pa2-i2-point-w2-long/tv_trades.csv | 13 + .../pa2-i2-point-w2-short-pooc/meta.json | 16 + .../pa2-i2-point-w2-short-pooc/metrics.json | 40 ++ .../pa2-i2-point-w2-short-pooc/strategy.pine | 44 ++ .../pa2-i2-point-w2-short-pooc/tv_trades.csv | 13 + .../pa2-i2-point-w2-short/meta.json | 16 + .../pa2-i2-point-w2-short/metrics.json | 40 ++ .../pa2-i2-point-w2-short/strategy.pine | 44 ++ .../pa2-i2-point-w2-short/tv_trades.csv | 13 + tests/oracle.sha256 | 4 +- tests/oracle/test_oracle_coof.cpp | 15 +- tests/test_coof_cascade_eligibility.cpp | 15 +- tests/test_magnified_aggregated_tape.cpp | 25 +- tests/test_second_extreme_order_tapes.cpp | 440 ++++++++++++++++++ 69 files changed, 2154 insertions(+), 130 deletions(-) create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v2/meta.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v2/metrics.json create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v2/strategy.pine create mode 100644 tests/fixtures/magnified_aggregated/hm-chart-diff-v2/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/README.md create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/tv_trades.csv create mode 100644 tests/test_second_extreme_order_tapes.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22560beb..b10a0944 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -153,7 +153,7 @@ What each gate refuses: `ci_verify.py` counts the CTest rows that actually **ran** and fails below a floor — `KERNEL_MIN_TESTS` ci_verify.py:255 and `RELEASE_MIN_TESTS` -ci_verify.py:389. +ci_verify.py:391. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index c5c8a0d4..fa1a2539 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -209,8 +209,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:677`) for TV sizing and fill spelling; `validate_execution_precommit` (`pine_strategy_host.cpp:702`), whose - `validate_precommit` (`pine_adapter.cpp:13076`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:13321`) to own the opening margin decision; `owns_lot_excursions() = true` + `validate_precommit` (`pine_adapter.cpp:13111`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:13356`) to own the opening margin decision; `owns_lot_excursions() = true` (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:787`), so the Pine host keeps its own per-lot MFE/MAE, `ab9714be`'s model (RULING A48). Measured against TradingView by R5 lane H-MEASURE (`tests/test_e19_excursion_tape.cpp`, `lab tv` tapes): where @@ -221,7 +221,7 @@ kernel and own these quirks, each at its site: divergence; `on_native_tick` (`pine_strategy_host.cpp:381`) / `on_native_applied` (`pine_strategy_host.cpp:477`) for `calc_on_order_fills` re-entry. `process_orders_on_close` becomes - `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2318-2319`); + `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2319-2320`); calc cadence and language publication are `PineScheduler`'s (`pine_scheduler.hpp:20`). - **Batching and open-order priority.** Same-bar command batching and its deferred queues (`PendingSameBarCommand` pine_adapter.hpp:1601, `pending_bracket_legs_` pine_adapter.hpp:1985-2003); the retained parent-before-child ordering of live handles — an @@ -232,9 +232,9 @@ kernel and own these quirks, each at its site: (`select_exit_activation` src/compat/pine/exit_activation.cpp:42, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:10, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:46) 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:9621-9626`); the half-tick + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:9656-9661`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:333) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11713`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11748`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:454-460`, 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 @@ -245,12 +245,12 @@ kernel and own these quirks, each at its site: quantities leave no remnant row on a `lab tv` tape, which the Pine host books row for row while a bare host books four 2.8e-17-unit dust rows beside them. - **Close reservations.** `strategy.close` callsite batching, two-call provenance and the entry-id - ledger (`close_logical_units_` `pine_adapter.hpp:2013-2024`); the POOC reservation-growth population predicate + ledger (`close_logical_units_` `pine_adapter.hpp:2024-2035`); 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:14111`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14147`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14146`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:14182`) 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 @@ -261,7 +261,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:16879`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16914`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -282,9 +282,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:8312`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:8347`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:680`), and the two fills Pine refuses to - recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5641`). + recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5645`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the three session flags, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` @@ -337,7 +337,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:454-460`). `strategy.close` batching and the entry-id ledger - are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2013-2024`) and + are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2024-2035`) 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:424`); what enables the generic model is the presence of `NativeRunSpec::margin`. So is the string-sentinel decoding of adapter-written comments: @@ -643,12 +643,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:342`), `source_level_on_price_grid` (`:328`), `nearest_tick` (`:266`) / `source_bar_fill_tick` (`:287`) / `directional_tick` (`:311`), 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`, `tests/test_native_price_grid.cpp` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13909`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14964`), `chart_day_key` (`pine_adapter.cpp:13806`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13268-13269`): 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:2360-2362`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13944`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14999`), `chart_day_key` (`pine_adapter.cpp:13841`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13303-13304`): 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:2361-2363`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2388-2396`, `margin.maintenance_long` `pine_adapter.cpp:2403`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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:2325-2327`) | 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` | +| `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:2326-2328`) | 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:1584`): 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:1565`) 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 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 close ending within it of an interior lot boundary is one selected `Flatten` of whole lots (`source_fifo_prefix_openings`, `src/source/pine_adapter.cpp`), 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 | `tests/test_native_quantity_tolerance.cpp` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index b50e2dff..c1b3b446 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,19 +47,19 @@ 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:2737-2748; `entry()` pine_adapter.cpp:5289, `order()` pine_adapter.cpp:10510) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | 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:8373 → `Flatten{}` pine_adapter.cpp:8402-8403; `Reduce{ExplicitUnits}` pine_adapter.cpp:7520-7529; `ReverseTo` pine_adapter.cpp:6354-6355) | **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:2707-3914 → `host.replace` pine_adapter.cpp:2723-2803) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4722-4768, `cancel_all` pine_adapter.cpp:18107-18122; `live_by_source_key_` pine_adapter.hpp:1985; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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:4189; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7205-7556) | **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:5513-5548, `cohort_for` pine_adapter.cpp:2501; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7503-7520) | **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:1270, `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:2746-2752) | **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:2738-2749; `entry()` pine_adapter.cpp:5290, `order()` pine_adapter.cpp:10545) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | 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:8408 → `Flatten{}` pine_adapter.cpp:8437-8438; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2708-3915 → `host.replace` pine_adapter.cpp:2724-2804) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4723-4769, `cancel_all` pine_adapter.cpp:18142-18157; `live_by_source_key_` pine_adapter.hpp:1996; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5514-5549, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7538-7555) | **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:1270, `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:2747-2753) | **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:1092-1093 | 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:13046; lowering `submit_one` pine_adapter.cpp:9070-9082, `BindCohort` pine_adapter.cpp:9571-9577, `submit_leg` pine_adapter.cpp:9595-9619; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8675-8678; `materialize_relative_exits` pine_adapter.cpp:17397-17438, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19158-19187) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10910-10934), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15673, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15907) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:684-686 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:13081; lowering `submit_one` pine_adapter.cpp:9105-9117, `BindCohort` pine_adapter.cpp:9606-9612, `submit_leg` pine_adapter.cpp:9630-9654; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8710-8713; `materialize_relative_exits` pine_adapter.cpp:17432-17473, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19193-19222) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1998-2016); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10945-10969), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15708, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15942) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:684-686 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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:5692-5714; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1080 | 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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11821-11833) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11734-11871) stays A. | -| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4021-4067) | **partial** — the read exists `trail_state` native_host.hpp:1080; `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:287-289) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9621-9626, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11734-11871, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8645-8871, `trail_one_shot` pine_adapter.cpp:9710-9839 | 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:11856-11868) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11769-11906) stays A. | +| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4022-4068) | **partial** — the read exists `trail_state` native_host.hpp:1080; `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:287-289) is A. | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9656-9661, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11769-11906, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8680-8906, `trail_one_shot` pine_adapter.cpp:9745-9874 | 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:5902-5906, pine_adapter.cpp:5902) | **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:10945-12149) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4922-4956 | 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:2595-2650) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4957-4959 **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: rounded equity `source_money_round` pine_adapter.cpp:2547, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | -| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2556-2558) | **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:2608, `typed_entry_units` pine_adapter.cpp:2626; `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:2587; `percent_commission_live_equity` pine_adapter.cpp:4328-4368) | **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. | -| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2513-2528, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6675-6701) | **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:5925-5929, pine_adapter.cpp:5925) | **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:10980-12184) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4922-4956 | 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:2596-2651) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4957-4959 **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: rounded equity `source_money_round` pine_adapter.cpp:2548, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | +| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2557-2559) | **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:2609, `typed_entry_units` pine_adapter.cpp:2627; `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:2588; `percent_commission_live_equity` pine_adapter.cpp:4329-4369) | **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. | +| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2514-2529, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6710-6736) | **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:591, 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:4973-4976; 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:12520-12548, `compute_exit_reservation` pine_adapter.cpp:4411-4532) | **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:4463-4786, `reserved_exit_qty` pine_adapter.cpp:8997-9307) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4443-4458) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6736), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7943), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12777), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13373, pine_adapter.cpp:13426) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15721) | 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:2595), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2608, `typed_entry_units` pine_adapter.cpp:2626), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18753) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7205-7556; ledger `close_batch_bar_` pine_adapter.hpp:2038-2049; 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:12555-12583, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:9032-9342) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4444-4459) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | +| 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:6771), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7978), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12812), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13408, pine_adapter.cpp:13461) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15756) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18788) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7240-7591; ledger `close_batch_bar_` pine_adapter.hpp:2039-2050; 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:1074) | 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:2361) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13268-13271) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6207-6256, cap left unset `max_open_lots` pine_adapter.cpp:2360-2362) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13322-13387 and leaves the kernel gate unset pine_adapter.cpp:13321-13323) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13977-13978, `margin_pct` pine_adapter.cpp:13385) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14077-14255, `submit_margin_call_units` pine_adapter.cpp:14216-14262, `schedule_margin_call_path` pine_adapter.cpp:18260-18337) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14008-14044) | **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:983 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. | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2362) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13303-13306) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6230-6279, cap left unset `max_open_lots` pine_adapter.cpp:2361-2363) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13357-13422 and leaves the kernel gate unset pine_adapter.cpp:13356-13358) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:14012-14013, `margin_pct` pine_adapter.cpp:13420) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14112-14290, `submit_margin_call_units` pine_adapter.cpp:14251-14297, `schedule_margin_call_path` pine_adapter.cpp:18295-18372) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14043-14079) | **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:983 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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1308 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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16325-16341, `apply_fx_opening_margin_slice` pine_adapter.cpp:18338-18366) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16849-16888) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13357-13360) | **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:13938-13952, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14964-14994) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15007-15241) | **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:13748-13827) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:13966, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14025, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14733-14773, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14257-14384, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14257) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, the restore's `+1e-6` floor and the frozen signal-time units (M10); `margin_check_allowed` never admits the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low — a recorded divergence, open (M7). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3817-3884, `record_market_review` pine_adapter.cpp:15245-15303) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16360-16376, `apply_fx_opening_margin_slice` pine_adapter.cpp:18373-18401) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16884-16923) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13392-13395) | **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:13973-13987, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14999-15029) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15042-15276) | **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:13783-13862) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:14001, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14060, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14768-14808, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14292-14419, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14292) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, the restore's `+1e-6` floor and the frozen signal-time units (M10); `margin_check_allowed` never admits the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low — a recorded divergence, open (M7). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15280-15338) | 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:881, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2318-2319) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7523-7527 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16160) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7065-7069; `execute_current` legal native_host.hpp:1092 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7523-7527 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16195) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7065-7069; `execute_current` legal native_host.hpp:1092 | 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:7008 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7059 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1061 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:863, `NativeInputContext` native_host.hpp:761-766 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16303, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6919, native_execution_consumer.cpp:6919-6949; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5611, `flush_coof_tail` pine_adapter.cpp:16338, `end_coof_recalc` pine_adapter.cpp:5634) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6919, native_execution_consumer.cpp:6919-6949; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:866 fires per accepted realtime print native_execution_consumer.cpp:7147-7151; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | | 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:7517-7634 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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:2448-2504) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | +| CT9 | Intrabar (magnifier) matching path + path-order policy (`volume_weighted_cap` pine_adapter.cpp:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:234-320) 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:5757), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1659-1666 | 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:5780), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1659-1666 | 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:5685-5732, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6268-6276; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11988-11997) is A | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12023-12032) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:472-479) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:12004-12013, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:12039-12048, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:1190 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:8705). **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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9062-9067, `exit_limit_trigger` pine_adapter.cpp:9596-9617), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11713) | 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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9097-9102, `exit_limit_trigger` pine_adapter.cpp:9631-9652), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11748) | 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:1856, refused in-run by `guard_native_mutation` engine.hpp:1860) | **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:2254-2255, `chart_timezone` pine_adapter.cpp:2295-2297) | **yes** — spec fields `ticker` native_run_spec.hpp:567-577; `parse_session` native_calendar.hpp:173-419; 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:2255-2256, `chart_timezone` pine_adapter.cpp:2296-2298) | **yes** — spec fields `ticker` native_run_spec.hpp:567-577; `parse_session` native_calendar.hpp:173-419; 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:2158, `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:896, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:940, probe `strategy_execution_contract` c_abi.cpp:873, stream `strategy_stream_begin` c_abi.cpp:624 **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:548 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:907 among them (F §2.iii) | @@ -204,14 +204,14 @@ 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:82-91) and of the rich `BacktestEngine::run` overload (engine.hpp:1780-1785, engine_consumer.cpp:81-93). The kernel only forwards it as `NativeBeginArgs::overrides_opaque` native_host.hpp:746; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:292-295 | 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) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:29 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:356-359 (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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8766`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2308-2309. 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:260-261. **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:741) | 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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8766`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2309-2310. 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:260-261. **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:741) | 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:454-460) | 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:6420 | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19158-19187 | 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:522, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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 | +| 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:19193-19222 | 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:523, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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:603. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1273 | 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) | | m | `request.security` machinery | `register_security_eval` engine_security.cpp:27, `set_native_security_feed` engine_aux_security.cpp:76-127, `SecurityEvalState` engine.hpp:1156-1360 carry Pine semantics (lookahead, gaps, Heikin-Ashi, KI-55 range start) and are driven only by the source layer | HASH-VISIBLE / measured path | F planned to move both TUs and the state into the source host after native HTF. **R5-6 narrows this:** L6 reuses the feed store and routing, so those stay kernel; only the Pine-only evaluator semantics remain candidates. Do last. **Status (lane L12b): done** — the evaluator semantics are `source::PineSecurityEvalState` + `src/source/pine_security_eval.cpp`; the kernel keeps the registry, aggregator, feed store and one generic step | F S | @@ -324,7 +324,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:1730, `guard_native_mutation` engine_aux_security.cpp:78 | 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). No kernel TU includes a source header | `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13 | 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:739 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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549; the ledger `close_logical_units_` pine_adapter.hpp:2013-2024; `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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549; the ledger `close_logical_units_` pine_adapter.hpp:2024-2035; `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) | 58 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 58 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:13933f70987f6bfe37d3015ae065a8d5ee5937c37431d48e2ecd1f17a4c29958` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:862, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:590-709, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:515-567, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:494-511, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:802-833, c_abi.cpp:929 | 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 | @@ -350,7 +350,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:2204-2337) 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:1641-1645). 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 Cloud Run campaign sweep on the final tree (fixed population, 4190 probes, zero tolerance on the hard band — native-refactor-progress.md:41-47; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:139-140). 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:2205-2338) 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:1641-1645). 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 Cloud Run campaign sweep on the final tree (fixed population, 4190 probes, zero tolerance on the hard band — native-refactor-progress.md:41-47; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:139-140). 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 drive the adapter and 14 the native host (O), 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. @@ -408,7 +408,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:2357, 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:2358, 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:5611-5615) and `drain_applied_notifications` (native_execution_consumer.cpp:6272-6285) 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) | @@ -521,12 +521,12 @@ line numbers are this tree's (683a82f); kernel semantics are | 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:2361); 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:13268-13269; 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:16849, `update_risk_state` pine_adapter.cpp:13909-13936): 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:13261), 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.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2362); 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:13303-13304; 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:16884, `update_risk_state` pine_adapter.cpp:13944-13971): 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:13296), 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:1200-1208): +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:17724-17733); 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:16319); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16431), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14994-15003, `submit_intraday_loss_close` pine_adapter.cpp:14964-14992), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17206); 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:5961, `:7650`, `:8374`, `:8502`, `:11491`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18105-18108) | `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:13900-13902); 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:13806-13827, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16354); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16466), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15029-15038, `submit_intraday_loss_close` pine_adapter.cpp:14999-15027), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17241); 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:5984, `:7685`, `:8409`, `:8537`, `:11526`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18140-18143) | `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:13935-13937); 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:13841-13862, `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, @@ -950,6 +950,20 @@ this lane re-pins the affected v19 witnesses once. now held to the lots open (`entry()`); the row asserts the cap on all eight variants and three paths, the v3 magnified run booking TradingView's 12 rows and ending flat. + **A market order a recalculation places on the bar's second extreme is + closed too (R5 lane PAR-ORDERS-2, H-MEASURE Finding 6d).** When the + matcher's fill of a resting request at its own level ends the leg to the + bar's second extreme, the `calc_on_order_fills` recalculation's market + entry and the exits it places are live from that extreme, as TradingView + books them (row 5 of `hm-chart-diff-v2` / `-v3` / `-v7`, the + `pa2-i2-exit-w2-*` tapes of `tests/test_second_extreme_order_tapes.cpp`): + the entry used to advance to the close point, which admits no cascade + order, and so to the next open. A fill the adapter itself forced onto that + extreme -- an order an earlier recalculation of the bar placed, gap-filled + there (`coof_fill_forced_`) -- starts the next leg instead, and its + recalculation's market orders still advance, as TradingView books them too + (the `pa2-i2-point-w2-*` tapes; corpus `bracket-rivet-calc-on-fill-01`, + trades 350 and 516, byte-identical). - The per-bar broker hash of an aggregated run moves because the kernel now folds its script coordinate and named input coordinate. Those v19 witnesses are re-pinned once in `tests/test_native_point_checks.cpp`; no pre-release @@ -984,9 +998,9 @@ key, since replaced by the walk of the retained rows, `placement_` pine_state_hash.cpp:287) — one per accepted request, cleared only by `reset_for_run` pine_adapter.cpp:2109 — and reflected the whole admission journal (since replaced by a running digest of its events, `admission_journal` -pine_state_hash.cpp:574). `PineScheduler::hash_state` pine_state_hash.cpp:612 +pine_state_hash.cpp:575). `PineScheduler::hash_state` pine_state_hash.cpp:613 re-folded the consumed source prefix too (`consumed` -pine_state_hash.cpp:619-641). Measured with recording on and off, in two +pine_state_hash.cpp:620-642). Measured with recording on and off, in two shapes: - the audit's order-and-cancel probe (one `strategy_order` accepted and @@ -1029,10 +1043,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:8786). Full-table scans + (`has_full_entry_bracket` pine_adapter.cpp:8821). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1696, `revive_brackets_after_margin` pine_adapter.cpp:1918, - `preserved_by_close_all` pine_adapter.cpp:11522), and + `preserved_by_close_all` pine_adapter.cpp:11557), 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. @@ -1120,13 +1134,13 @@ retires every leg of each exit row it names and drops such a parked stop (`retire_cancelled_exits` pine_adapter.cpp:1844), 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:2930). Both shapes now cost ×1.89 to ×2.12 per doubling up +pine_adapter.cpp:2931). 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:3361); a lot opened +it off (`release_closed_cohort_origins` pine_adapter.cpp:3362); 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 @@ -1275,8 +1289,8 @@ unchanged after it. | E1 | **Spec hashing.** `hash_spec` folds every field unconditionally (native_execution_consumer.cpp:234-256) | 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:1620); 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5757) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290); 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:16160-16189) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5780) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290); 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:16195-16224) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | @@ -1321,7 +1335,7 @@ unchanged after it. | **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:16325-16341) 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:2645) 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:16360-16376) 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:2646) 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. | @@ -1344,7 +1358,7 @@ NativeMetricsView native_metrics() const; // S: net_pro ``` - `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` + `record_equity_point(script_open_ms)` (engine.hpp:1380, engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7191, native_execution_consumer.cpp:7191, native_execution_consumer.cpp:7191) 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:2357), 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:2358), 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:1011). Same semantics, but it costs a host-epoch bump, so the spec form was chosen; the virtual was never added, and `NativeReportPolicy` is what landed (§3.4 row 3). - TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5055-5097), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. @@ -1397,7 +1411,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:13179-13200). 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:2575), 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:13214-13235). 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:2575), 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:4654-4667 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). @@ -1507,7 +1521,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:1317) (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:373), 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:13732-13830); 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:13767-13865); 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:4654-4667 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index f68abf5b..d25a3995 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -118,7 +118,7 @@ finding to report, not a step to take. 10. **A test row never silently disappears.** Each profile counts the rows that *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:255 and - `RELEASE_MIN_TESTS` ci_verify.py:389. Adding rows means raising the floor + `RELEASE_MIN_TESTS` ci_verify.py:391. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -264,7 +264,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:255, `RELEASE_MIN_TESTS` ci_verify.py:389). It +(`KERNEL_MIN_TESTS` ci_verify.py:255, `RELEASE_MIN_TESTS` ci_verify.py:391). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index a2286552..af947868 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1458,7 +1458,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:14111-14135`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:14146-14170`), 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 3bc46231..78e48c2d 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -1847,6 +1847,7 @@ class PineExecutionAdapter { void stage_flat_children_before_parent(const SourceId&, std::int32_t, std::int64_t); bool defer_coof_tail() const noexcept; + bool coof_fill_at_second_extreme() const noexcept; bool source_path_uses_high_first(const Bar&) const noexcept; bool coof_fill_on_path_point() const noexcept; bool coof_fill_at_path_point(double waypoint) const noexcept; @@ -2057,6 +2058,11 @@ class PineExecutionAdapter { bool source_batch_mutated_ = false; bool coof_recalc_active_ = false; bool coof_first_open_ = false; + // Whether the recalculating fill booked the forced_execution_price the + // adapter set on its request (a fill AT a path point), not the matcher's + // own price (R5 lane PAR-ORDERS-2). It sits in the padding before the + // next 8-byte member, so the class layout does not move. + bool coof_fill_forced_ = false; std::uint64_t coof_market_entry_recalc_incarnation_ = 0; std::uint64_t coof_market_entry_recalc_fill_seq_ = 0; std::uint64_t coof_current_fill_seq_ = 0; diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 7d0324a5..eeb7588c 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -386,7 +386,9 @@ # summed). # 696 = those 695 plus R5 lane PAR-ORDERS-2's test_flat_coof_exit_tapes # (source-bound; INT26 pick, summed). -RELEASE_MIN_TESTS = 696 +# 697 = those 696 plus R5 lane PAR-ORDERS-2's test_second_extreme_order_tapes +# (source-bound; INT26 pick, summed). +RELEASE_MIN_TESTS = 697 # R5 lane H-DOCGATES (AUDIT4-opus perf N-7) raises both floors by one more: # test_native_runtime_ambient_lifo, the runtime blocks' LIFO death row, is # source-free, so it registers and runs in the kernel profile (279 -> 280) and diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index b7b5a82b..8e48c9b6 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -2171,6 +2171,7 @@ void PineExecutionAdapter::reset_for_run() { source_batch_mutated_ = false; coof_recalc_active_ = false; coof_first_open_ = false; + coof_fill_forced_ = false; coof_market_entry_recalc_incarnation_ = 0; coof_market_entry_recalc_fill_seq_ = 0; coof_current_fill_seq_ = 0; @@ -5617,20 +5618,23 @@ void PineExecutionAdapter::begin_coof_recalc( coof_fill_cursor_t_ = event.cursor.t; coof_market_entry_recalc_fill_seq_ = source_fill_sequence; coof_market_entry_recalc_incarnation_ = 0; + const auto placement = placement_.find(event.handle().incarnation); if (event.opened_units != 0.0 && std::holds_alternative(event.request().trigger)) { - const auto placement = placement_.find(event.handle().incarnation); if (placement != placement_.end() && placement->second.family == PineOrderFamily::Entry) { coof_market_entry_recalc_incarnation_ = event.handle().incarnation; } } + coof_fill_forced_ = placement != placement_.end() + && finite_positive(placement->second.forced_execution_price); coof_context_ = context; } void PineExecutionAdapter::end_coof_recalc() noexcept { coof_recalc_active_ = false; coof_first_open_ = false; + coof_fill_forced_ = false; coof_market_entry_recalc_incarnation_ = 0; coof_market_entry_recalc_fill_seq_ = 0; coof_current_fill_seq_ = 0; @@ -5683,13 +5687,32 @@ bool PineExecutionAdapter::defer_coof_tail() const noexcept { const auto phase = coof_context_.coordinate.path_phase; if (phase == NativePathPhase::Close || phase == NativePathPhase::None) return true; - if (!coof_script_bar_valid_) return false; + // A fill the adapter forced onto the second extreme leaves the + // recalculation the close point alone; the matcher's own fill there + // does not (coof_fill_at_second_extreme, H-MEASURE Finding 6d). + return coof_fill_forced_ && coof_fill_at_second_extreme(); +} + +// The recalculating fill sits AT the bar's second extreme, the end of its +// approach leg (the high of a low-first bar, the low of a high-first one). +// Two fills book that point, and TradingView answers the recalculation's +// market orders differently for them. The matcher's fill of a resting +// request at its own level there ends the approach leg: those orders fill at +// the extreme (hm-chart-diff-v2 / -v3 / -v6 / -v7 bar 19, lab tv +// pa2-i2-exit-w2-* and pa2-i4-close-w2-*). A request an earlier +// recalculation of the bar placed, which the adapter forced onto the extreme +// it gap-fills at (coof_fill_forced_: forced_execution_price), fills at the +// point itself: they advance to the close, as ab9714be's endpoint rule has +// it (corpus bracket-rivet-calc-on-fill-01 trades 350 and 516; R5 lane +// PAR-ORDERS-2). +bool PineExecutionAdapter::coof_fill_at_second_extreme() const noexcept { + if (!coof_recalc_active_ || coof_first_open_ || !coof_script_bar_valid_) return false; const bool high_first = source_path_uses_high_first(coof_script_bar_); const NativePathPhase second = high_first ? NativePathPhase::Low : NativePathPhase::High; - const double endpoint = high_first - ? coof_script_bar_.low : coof_script_bar_.high; - return phase == second && coof_fill_at_path_point(endpoint); + const double endpoint = high_first ? coof_script_bar_.low : coof_script_bar_.high; + return coof_context_.coordinate.path_phase == second + && coof_fill_at_path_point(endpoint); } bool PineExecutionAdapter::coof_fill_on_path_point() const noexcept { @@ -6363,14 +6386,23 @@ void PineExecutionAdapter::entry(const SourceId& id, bool is_long, double limit_ const auto coof_native_state = detail::run_state(require_host()); const bool coof_lower_path = coof_native_state.spec && coof_native_state.spec->intrabar.lower(); + // A matcher fill AT the bar's second extreme: the recalculation's market + // entry fills there, at that extreme. ab9714be advanced every endpoint + // fill to the following waypoint, the close, which admits no cascade + // order, and filled the entry at the next open; TradingView books it on + // the fill's own bar at the extreme (hm-chart-diff-v2 / -v6 bar 19: 11.72 + // / 11.74 at the high, and under process_orders_on_close -v3 / -v7 alike + // -- R5 lane PAR-ORDERS). A fill the adapter forced there still advances + // (coof_fill_at_second_extreme; R5 lane PAR-ORDERS-2). bool coof_market_next_open = false; - if (coof_recalc_active_ && !coof_first_open_ && !coof_lower_path && !priced) { - const bool high_first = source_path_uses_high_first(coof_script_bar_); - const NativePathPhase second = high_first - ? NativePathPhase::Low : NativePathPhase::High; - const double endpoint = high_first ? coof_script_bar_.low : coof_script_bar_.high; - coof_market_next_open = coof_context_.coordinate.path_phase == second - && coof_fill_at_path_point(endpoint); + bool coof_market_at_second_extreme = false; + double coof_second_extreme = kNaN; + if (coof_recalc_active_ && !coof_first_open_ && !coof_lower_path && !priced + && coof_fill_at_second_extreme()) { + coof_second_extreme = source_path_uses_high_first(coof_script_bar_) + ? coof_script_bar_.low : coof_script_bar_.high; + coof_market_next_open = coof_fill_forced_; + coof_market_at_second_extreme = !coof_fill_forced_; } double native_limit = limit_price; double native_stop = stop_price; @@ -6399,13 +6431,16 @@ void PineExecutionAdapter::entry(const SourceId& id, bool is_long, double limit_ if (coof_recalc_active_ && !coof_first_open_ && !coof_market_next_open && coof_script_bar_valid_ && std::holds_alternative(request.trigger) - && !defer_coof_tail()) { + && (coof_market_at_second_extreme || !defer_coof_tail())) { // ab9714be pine_scheduler.cpp:398-619: a MARKET request born by a // non-first-open fill recalc waits for the next unconsumed waypoint. // A mid-segment fill retains that segment's endpoint; an endpoint - // fill advances to the following waypoint. + // fill advances to the following waypoint -- but the second + // extreme's own (above). int next_extreme_index = -1; - const double next_extreme = coof_next_waypoint(&next_extreme_index); + const double next_extreme = coof_market_at_second_extreme + ? coof_second_extreme : coof_next_waypoint(&next_extreme_index); + if (coof_market_at_second_extreme) next_extreme_index = 2; const auto point = detail::callback_point(require_host()); const double current_quote = point ? point->price : kNaN; coof_market_fill = source_bar_fill_tick( @@ -7123,7 +7158,7 @@ void PineExecutionAdapter::entry(const SourceId& id, bool is_long, double limit_ {std::move(request), std::move(snapshot), id, true, 0, true}); return; } - if (defer_coof_tail()) { + if (defer_coof_tail() && !coof_market_at_second_extreme) { pending_coof_requests_.push_back({std::move(request), std::move(snapshot), id, true, 0}); return; } diff --git a/src/source/pine_state_hash.cpp b/src/source/pine_state_hash.cpp index 6404f077..4fed1d23 100644 --- a/src/source/pine_state_hash.cpp +++ b/src/source/pine_state_hash.cpp @@ -469,6 +469,7 @@ void source::PineExecutionAdapter::hash_state(BrokerStateHashSink& f) const { // Live only inside a fill recalculation (NaN otherwise); folded then so // the idle digest keeps its prior form. if (coof_recalc_active_) f.d(coof_fill_cursor_t_); + if (coof_recalc_active_) f.b(coof_fill_forced_); const auto& coof_coord = coof_context_.coordinate; f.u(coof_coord.ordinal); f.i(coof_coord.interval_index); f.i(coof_coord.open_ms); f.i(coof_coord.eligible_open_ms); f.i(coof_coord.last_traded_close_ms); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 461da244..a4505cd5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1371,6 +1371,8 @@ list(APPEND TEST_SOURCES test_native_current_cohort_refusal) # Pine adapter. All source-bound. # A calc_on_order_fills exit placed while flat, for a pending or a filled parent. list(APPEND TEST_SOURCES test_flat_coof_exit_tapes) +# An order a recalculation places on a bar's second extreme. +list(APPEND TEST_SOURCES test_second_extreme_order_tapes) # R4-D L4b-fix restores every removed ShortSeed, affordability, sizing and # deferred-close oracle as an individually compiled native-route twin. Keep @@ -2114,7 +2116,8 @@ foreach(_pf_hm_row IN ITEMS "test_magnified_aggregated_tape|PINEFORGE_HM_MAG_FIXTURE_DIR|magnified_aggregated" "test_adapter_margin_schedule_differential|PINEFORGE_HM_M7A_FIXTURE_DIR|margin_entry_bar" # R5 lane PAR-ORDERS-2's tape rows. - "test_flat_coof_exit_tapes|PINEFORGE_PA2_FLAT_COOF_FIXTURE_DIR|flat_coof_exit") + "test_flat_coof_exit_tapes|PINEFORGE_PA2_FLAT_COOF_FIXTURE_DIR|flat_coof_exit" + "test_second_extreme_order_tapes|PINEFORGE_PA2_SECOND_EXTREME_FIXTURE_DIR|second_extreme_orders") string(REPLACE "|" ";" _pf_hm_fields "${_pf_hm_row}") list(GET _pf_hm_fields 0 _pf_hm_target) list(GET _pf_hm_fields 1 _pf_hm_macro) diff --git a/tests/fixtures/magnified_aggregated/README.md b/tests/fixtures/magnified_aggregated/README.md index 69dd6b3b..2c3050a9 100644 --- a/tests/fixtures/magnified_aggregated/README.md +++ b/tests/fixtures/magnified_aggregated/README.md @@ -23,6 +23,7 @@ window 2025-07-01 .. 2025-07-08), byte-identical. | `hm-chart-diff-v1` | process_orders_on_close, magnifier off | 12 | `27174a6b560b84c0c8f7ce541b66a6e0db648d4c1880d684059b6b050087d12c` | `tv-tape-hm-chart-diff-v1-27174a6b` | | `hm-chart-diff-v4` | slippage 2, pyramiding 3, magnifier off | 14 | `5293cc0c2bc88dabba65ff73a024d88b28292845c537f0f4ccf58922e4d502a1` | `tv-tape-hm-chart-diff-v4-5293cc0c` | | `hm-chart-diff-v5` | process_orders_on_close, slippage 2, pyramiding 3, magnifier off | 14 | `0080877d820ae0df81ec7eedab24b7d3a20df9f819f1e6101ff51b6049a6b44d` | `tv-tape-hm-chart-diff-v5-0080877d` | +| `hm-chart-diff-v2` | calc_on_order_fills, magnifier off | 12 | `3b3fb4c3dee2309df5f6340b5c9aef2580e62787020e82bbde8ec77fe8d4a5a3` | `tv-tape-hm-chart-diff-v2-3b3fb4c3` | | `hm-chart-diff-v3` | process_orders_on_close, calc_on_order_fills, magnifier off | 12 | `4d45cbcd76d2949a706469307ecba90e5e5b878d84033ac8b704ec906152cdae` | `tv-tape-hm-chart-diff-v3-4d45cbcd` | | `hm-chart-diff-v7` | process_orders_on_close, calc_on_order_fills, slippage 2, pyramiding 3, magnifier off | 14 | `f5a88e1e51e749a95eef40b094fe44b4637f4426e2b43796a9f7c5242457c581` | `tv-tape-hm-chart-diff-v7-f5a88e1e` | @@ -36,3 +37,9 @@ flat book, where the engine once opened 85 lots on one bar with pyramiding=1. the same source) witness the process_orders_on_close stop entry that its placing close already reached: TradingView fills it at that close on every path, with or without calc_on_order_fills. + +`hm-chart-diff-v2` (added by R5 lane PAR-ORDERS-2 from lane PAR-ORDERS' evidence, +`exec/PAR-ORDERS-scratch/mag/tapes`) and row 5 of `hm-chart-diff-v3` / `-v7` witness +H-MEASURE Finding 6d: the market entry a calc_on_order_fills recalculation places at +the matcher's fill on bar 19's high fills at that high, with or without +process_orders_on_close. diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v2/meta.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v2/meta.json new file mode 100644 index 00000000..585dbe40 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v2/meta.json @@ -0,0 +1,17 @@ +{ + "title": "HM CHART diff v2 pooc0 coof1 slip0 pyr1", + "slug": "hm-chart-diff-v2", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T18:48:51.244Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "timeoutMs": 120000 + }, + "pine_sha256": "8d7a0bc3245a8a77127960b1c89d914a29e42f6cdefa926bf8d70dc980c3986b", + "tv_trades_sha256": "3b3fb4c3dee2309df5f6340b5c9aef2580e62787020e82bbde8ec77fe8d4a5a3", + "tv_trades_rows": 24 +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v2/metrics.json b/tests/fixtures/magnified_aggregated/hm-chart-diff-v2/metrics.json new file mode 100644 index 00000000..5568cdfa --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v2/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hm-chart-diff-v2", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 12, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "8d7a0bc3245a8a77127960b1c89d914a29e42f6cdefa926bf8d70dc980c3986b", + "tvTradesCsvHash": "3b3fb4c3dee2309df5f6340b5c9aef2580e62787020e82bbde8ec77fe8d4a5a3", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74397", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "c0434b24c505b3dc56f71813fb08d29a307f296d46d7a5db7cd7cb60e05e9f11", + "decodedReportSha256": "c160fb80d43eb4b0eb3b38fb9ef407f6d10111bbab6a52c8e3aa11f2a02fa48d", + "closedTrades": 12, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T18:48:51.244Z" +} diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v2/strategy.pine b/tests/fixtures/magnified_aggregated/hm-chart-diff-v2/strategy.pine new file mode 100644 index 00000000..99a99c41 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v2/strategy.pine @@ -0,0 +1,43 @@ +//@version=6 +// H-MEASURE F1 magnified residual (R5 lane H-MEASURE, research group agg): lane F1's +// differential script (exec/F1-scratch/f1-scratch/diff_probe.cpp) as Pine, variant 2: +// process_orders_on_close=0, calc_on_order_fills=1, slippage=0, pyramiding=1, +// with the bar magnifier OFF (the chart-path control). NYSE:F 15m, the three replayed days of +// tests/fixtures/session_islastbar (2025-07-02, 07-03 half day, 07-07). The script's bar +// counter k is the chart bar since 2025-07-02 09:30 ET (the fixture's first bar), so +// k == bar_index of the replay. No new order after the last replayed bar (07-07 15:45 ET); +// whatever is open is closed on the next bar, outside the replay. +strategy("HM CHART diff v2 pooc0 coof1 slip0 pyr1", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +if k >= 0 and time < tEnd + // priced long entry with a same-bar bracket + if k % 7 == 3 and units == 0 + strategy.entry("PL", strategy.long, stop=high - 0.02) + strategy.exit("PX", "PL", limit=close + 0.06, stop=close - 0.06) + // limit short entry with a bracket + if k % 9 == 4 and units == 0 + strategy.entry("PS", strategy.short, limit=close + 0.02) + strategy.exit("SX", "PS", limit=close - 0.05, stop=close + 0.07) + // market long, then a market pyramid add under a priced from_entry leg + if k % 13 == 6 and units == 0 + strategy.entry("ML", strategy.long) + strategy.exit("MX", "ML", limit=close + 0.08, stop=close - 0.08) + if k % 13 == 7 and units > 0 + strategy.entry("ML", strategy.long) + // close whatever is left every 17 bars + if k % 17 == 16 and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/magnified_aggregated/hm-chart-diff-v2/tv_trades.csv b/tests/fixtures/magnified_aggregated/hm-chart-diff-v2/tv_trades.csv new file mode 100644 index 00000000..33e1efe5 --- /dev/null +++ b/tests/fixtures/magnified_aggregated/hm-chart-diff-v2/tv_trades.csv @@ -0,0 +1,25 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:00,PX,11.53,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,2 +1,Entry long,2025-07-02 22:30,PL,11.47,100,1147,5.77,0.50,0.23,5.8853,0.51,-3.1147,-0.27,5.77,0.01,2 +2,Exit long,2025-07-02 23:15,MX,11.66,100,1158,7.7676,0.67,0.2324,7.8842,0.68,-0.1158,-0.01,13.5376,0.01,1 +2,Entry long,2025-07-02 23:00,ML,11.58,100,1158,7.7676,0.67,0.2324,7.8842,0.68,-0.1158,-0.01,13.5376,0.01,1 +3,Exit long,2025-07-03 01:45,Close position order,11.66,100,1167,-1.2333,-0.11,0.2333,0.8833,0.08,-3.1167,-0.27,12.3043,0.01,6 +3,Entry long,2025-07-03 00:15,PL,11.67,100,1167,-1.2333,-0.11,0.2333,0.8833,0.08,-3.1167,-0.27,12.3043,0.01,6 +4,Exit long,2025-07-03 02:15,PX,11.72,100,1167,4.7661,0.41,0.2339,4.8833,0.42,-3.1167,-0.27,17.0704,0.02,2 +4,Entry long,2025-07-03 01:45,PL,11.67,100,1167,4.7661,0.41,0.2339,4.8833,0.42,-3.1167,-0.27,17.0704,0.02,2 +5,Exit long,2025-07-03 21:30,MX,11.79,100,1172,6.7649,0.58,0.2351,6.8828,0.59,-1.1172,-0.10,23.8353,0.02,7 +5,Entry long,2025-07-03 02:15,ML,11.72,100,1172,6.7649,0.58,0.2351,6.8828,0.59,-1.1172,-0.10,23.8353,0.02,7 +6,Exit long,2025-07-03 23:30,Close position order,11.84,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,21.5983,0.02,2 +6,Entry long,2025-07-03 23:00,PL,11.86,100,1186,-2.237,-0.19,0.237,0,0.00,-6.1186,-0.52,21.5983,0.02,2 +7,Exit short,2025-07-04 00:30,SX,11.8,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,29.3615,0.03,3 +7,Entry short,2025-07-03 23:45,PS,11.88,100,1188,7.7632,0.65,0.2368,7.8812,0.66,-0.1188,-0.01,29.3615,0.03,3 +8,Exit long,2025-07-07 21:30,PX,11.62,100,1180,-18.2342,-1.55,0.2342,3.882,0.33,-18.118,-1.54,11.1273,0.01,2 +8,Entry long,2025-07-04 00:30,PL,11.8,100,1180,-18.2342,-1.55,0.2342,3.882,0.33,-18.118,-1.54,11.1273,0.01,2 +9,Exit short,2025-07-07 22:00,SX,11.75,100,1170,-5.2345,-0.45,0.2345,4.883,0.42,-5.117,-0.44,5.8928,0.01,2 +9,Entry short,2025-07-07 21:30,PS,11.7,100,1170,-5.2345,-0.45,0.2345,4.883,0.42,-5.117,-0.44,5.8928,0.01,2 +10,Exit long,2025-07-07 23:45,MX,11.58,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,-3.3397,0.00,3 +10,Entry long,2025-07-07 23:00,ML,11.67,100,1167,-9.2325,-0.79,0.2325,0,0.00,-9.1167,-0.78,-3.3397,0.00,3 +11,Exit long,2025-07-08 02:15,PX,11.51,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-9.5705,-0.01,6 +11,Entry long,2025-07-08 00:45,PL,11.57,100,1157,-6.2308,-0.54,0.2308,3.8843,0.34,-6.1157,-0.53,-9.5705,-0.01,6 +12,Exit long,2025-07-08 02:45,PX,11.58,100,1154,3.7688,0.33,0.2312,3.8846,0.34,0,0.00,-5.8017,-0.01,1 +12,Entry long,2025-07-08 02:30,PL,11.54,100,1154,3.7688,0.33,0.2312,3.8846,0.34,0,0.00,-5.8017,-0.01,1 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/README.md b/tests/fixtures/second_extreme_orders/README.md new file mode 100644 index 00000000..cd9b5225 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/README.md @@ -0,0 +1,40 @@ +# Second-extreme order tapes (R5 lane PAR-ORDERS-2, item 2) + +TradingView's own trades for orders a `calc_on_order_fills` recalculation places +exactly on a bar's SECOND extreme (H-MEASURE Finding 6d): lot A is open; lot B is a +stop entry placed exactly on bar j's second extreme (the high of a low-first bar, +the low of a high-first one), so its fill starts the recalculation there with A still +open (pyramiding 2), and that recalculation places an exit for A reached on the +extreme -> close leg. Long cycles j = 7, 19, 26, 42; +short j = 5, 30, 46, 51. `tests/test_second_extreme_order_tapes.cpp` replays each +tape through the Pine adapter on the 15m chart (the short tapes on their first +cycle: TradingView fills B's off-grid 11.795 sell stop on bar 38, not on bar 30). + +The `pa2-i2-point-w2-*` and `pa2-i2-level-w1-*` tapes are the boundary of that rule, one +flat lot at a time (pyramiding 1): a fill booked AT the second extreme because a +recalculation's order gap-fills there (point-w2: P's stop exit fills on the leg to the first +extreme, the market entry A its recalculation places fills on that extreme, the exit AX A's +recalculation places between the extremes gap-fills at the second, and AX's recalculation +places the market entry C -- long j = 3, 41, short j = 2, 30), and a limit entry A AT the +first extreme with the same AX and C (level-w1 -- long j = 4, 25, 40, short j = 2, 32, 44). + +Each directory is one `lab tv` export (pineforge-workflow, channel `ws-report-v1`, +`rangeProof: covered`, NYSE:F 15, window 2025-07-01 .. 2025-07-08), byte-identical: +`strategy.pine`, `tv_trades.csv` (times UTC+8), `meta.json`, `metrics.json`. The +scripts count chart bars from 2025-07-02 09:30 ET, the first bar of +`tests/fixtures/session_islastbar/bars.inc`, whose 15m and 1m bars the rows replay. + +| probe | variant | trades | tvTradesSha256 | note | +|---|---|---|---|---| +| `pa2-i2-exit-w2-long` | exit AX of lot A, long, stop between close and high | 8 | `b3ccb9c57b66baf6dd130b676e52cce418a1f64b4d109cca3725616e5a313530` | `tv-tape-pa2-i2-exit-w2-long-b3ccb9c5` | +| `pa2-i2-exit-w2-long-pooc` | the same, process_orders_on_close | 8 | `1b21b85bc50a052cee2749d2745c5d67cb434c9c8eb534aa1b22648bd00abc03` | `tv-tape-pa2-i2-exit-w2-long-pooc-1b21b85b` | +| `pa2-i2-exit-w2-short` | exit AX of lot A, short, stop between low and close | 7 | `6864d39da5adfd779657801af3be47c7940783b14c4df0bc8af3e58f4624dd39` | `tv-tape-pa2-i2-exit-w2-short-6864d39d` | +| `pa2-i2-exit-w2-short-pooc` | the same, process_orders_on_close | 7 | `05f4980868362562d407a5bff8e203c87edbfe6c519fe8fdf6a4e7d646fe8fec` | `tv-tape-pa2-i2-exit-w2-short-pooc-05f49808` | +| `pa2-i2-point-w2-long` | point fill on the second extreme, long | 6 | `88db2d9cd2a7f2dd4d7692007935c6e165b8476ed418fc9f293f6f691c8728ea` | `tv-tape-pa2-i2-point-w2-long-88db2d9c` | +| `pa2-i2-point-w2-long-pooc` | the same, process_orders_on_close | 6 | `1fce1bed23df68231ed7546fe4fe13760116654e945a4cf1cdb02a9d755d6ca5` | `tv-tape-pa2-i2-point-w2-long-pooc-1fce1bed` | +| `pa2-i2-point-w2-short` | point fill on the second extreme, short | 6 | `e292e70d771691a3fd44670abc9a699270d5ad677b3d480ff7402a0421fa7abd` | `tv-tape-pa2-i2-point-w2-short-e292e70d` | +| `pa2-i2-point-w2-short-pooc` | the same, process_orders_on_close | 6 | `03c75065f9522e4e339594fe6c8e703e898ef35878e4351522db6622f372fb1f` | `tv-tape-pa2-i2-point-w2-short-pooc-03c75065` | +| `pa2-i2-level-w1-long` | limit entry at the first extreme, long | 6 | `9a22c1daf2574a168815188429caaa9de881a9ff5ab637712b468828085b0499` | `tv-tape-pa2-i2-level-w1-long-9a22c1da` | +| `pa2-i2-level-w1-long-pooc` | the same, process_orders_on_close | 6 | `7f7a332d07202c8a1814a7119b9f6cd70dda335fb563d40403d1e7e23ab3be89` | `tv-tape-pa2-i2-level-w1-long-pooc-7f7a332d` | +| `pa2-i2-level-w1-short` | limit entry at the first extreme, short | 6 | `8d4893ade201e54203b7723abe267e211a26c92480620fea341d73882f12014d` | `tv-tape-pa2-i2-level-w1-short-8d4893ad` | +| `pa2-i2-level-w1-short-pooc` | the same, process_orders_on_close | 5 | `c092e35ad56281fdc114e0923e9e24e3c47ea7f7a93ddce883b2cde07d69fd99` | `tv-tape-pa2-i2-level-w1-short-pooc-c092e35a` | diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/meta.json b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/meta.json new file mode 100644 index 00000000..230ab686 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I2 exit-w2 long-pooc", + "slug": "pa2-i2-exit-w2-long-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:09:42.099Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "35d3a5e1d3809676d319833f2bfdb882a63967786739882f225f7376d4c801e5", + "tv_trades_sha256": "1b21b85bc50a052cee2749d2745c5d67cb434c9c8eb534aa1b22648bd00abc03", + "tv_trades_rows": 16 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/metrics.json new file mode 100644 index 00000000..8408b51e --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i2-exit-w2-long-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 8, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "35d3a5e1d3809676d319833f2bfdb882a63967786739882f225f7376d4c801e5", + "tvTradesCsvHash": "1b21b85bc50a052cee2749d2745c5d67cb434c9c8eb534aa1b22648bd00abc03", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.76115", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "5b7585ab46dcee12145c163ff715aa73624ae1676fe935fa2f472ba4c1064312", + "decodedReportSha256": "1dd529e05b64783d6e80487dc45229881e3a8df159e5b77d8881edc6b24545de", + "closedTrades": 8, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:09:42.097Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/strategy.pine new file mode 100644 index 00000000..4aa5d836 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/strategy.pine @@ -0,0 +1,39 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 2: a strategy.exit placed in the shape of H-MEASURE +// Finding 6d. Lot A is open; lot B is a stop entry placed exactly on bar j's SECOND extreme +// (a buy stop at the high of a low-first bar), +// so the calc_on_order_fills recalculation B's fill starts runs on that point with A still +// open, and places strategy.exit("AX", "A", stop, qty=100) with the stop between bar j's close +// and that extreme: reached on the extreme->close leg. pyramiding=2; everything is closed on +// bar j+1. Cycles j = 7, 19, 26, 42 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I2 exit-w2 long-pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=2, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +bStop = k == 6 ? 11.665 : k == 18 ? 11.715 : k == 25 ? 11.96 : k == 41 ? 11.785 : na +axStop = k == 7 ? 11.66 : k == 19 ? 11.71 : k == 26 ? 11.95 : k == 42 ? 11.77 : na +if k >= 0 and time < tEnd + if (k == 5 or k == 17 or k == 24 or k == 40) and units == 0 + strategy.entry("A", strategy.long) + if not na(bStop) + strategy.entry("B", strategy.long, stop=bStop) + if (k == 8 or k == 20 or k == 27 or k == 43) and units != 0 + strategy.close_all() + if math.abs(units) > 150 and not na(axStop) + strategy.exit("AX", "A", stop=axStop, qty=100) +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/tv_trades.csv new file mode 100644 index 00000000..811455eb --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long-pooc/tv_trades.csv @@ -0,0 +1,17 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:15,AX,11.66,100,1151,14.7683,1.28,0.2317,15.8849,1.38,-1.1151,-0.10,14.6516,0.01,2 +1,Entry long,2025-07-02 22:45,A,11.51,100,1151,14.7683,1.28,0.2317,15.8849,1.38,-1.1151,-0.10,14.6516,0.01,2 +2,Exit long,2025-07-02 23:30,Close position order,11.66,100,1167,-1.2333,-0.11,0.2333,2.8833,0.25,-2.1167,-0.18,13.535,0.01,1 +2,Entry long,2025-07-02 23:15,B,11.67,100,1167,-1.2333,-0.11,0.2333,2.8833,0.25,-2.1167,-0.18,13.535,0.01,1 +3,Exit long,2025-07-03 02:15,AX,11.71,100,1166,4.7663,0.41,0.2337,5.8834,0.50,-2.1166,-0.18,18.1841,0.02,2 +3,Entry long,2025-07-03 01:45,A,11.66,100,1166,4.7663,0.41,0.2337,5.8834,0.50,-2.1166,-0.18,18.1841,0.02,2 +4,Exit long,2025-07-03 02:30,Close position order,11.73,100,1172,0.7655,0.07,0.2345,0.8828,0.08,-1.1172,-0.10,19.0668,0.02,1 +4,Entry long,2025-07-03 02:15,B,11.72,100,1172,0.7655,0.07,0.2345,0.8828,0.08,-1.1172,-0.10,19.0668,0.02,1 +5,Exit long,2025-07-03 21:30,AX,11.95,100,1175,19.763,1.68,0.237,20.8825,1.78,-2.1175,-0.18,38.7102,0.04,2 +5,Entry long,2025-07-03 03:30,A,11.75,100,1175,19.763,1.68,0.237,20.8825,1.78,-2.1175,-0.18,38.7102,0.04,2 +6,Exit long,2025-07-03 21:45,Close position order,11.85,100,1196,-11.2381,-0.94,0.2381,0,0.00,-12.1196,-1.01,27.5917,0.03,1 +6,Entry long,2025-07-03 21:30,B,11.96,100,1196,-11.2381,-0.94,0.2381,0,0.00,-12.1196,-1.01,27.5917,0.03,1 +7,Exit long,2025-07-07 22:00,AX,11.77,100,1168,8.7655,0.75,0.2345,10.8832,0.93,-3.1168,-0.27,36.2393,0.04,2 +7,Entry long,2025-07-07 21:30,A,11.68,100,1168,8.7655,0.75,0.2345,10.8832,0.93,-3.1168,-0.27,36.2393,0.04,2 +8,Exit long,2025-07-07 22:15,Close position order,11.7,100,1179,-9.2349,-0.78,0.2349,0,0.00,-9.1179,-0.77,27.1223,0.03,1 +8,Entry long,2025-07-07 22:00,B,11.79,100,1179,-9.2349,-0.78,0.2349,0,0.00,-9.1179,-0.77,27.1223,0.03,1 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/meta.json b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/meta.json new file mode 100644 index 00000000..045b3c47 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I2 exit-w2 long", + "slug": "pa2-i2-exit-w2-long", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:09:37.640Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "5134f202f32bf6f1afecf375dde1f674782bbbd25d2c679f601da7404b9309f3", + "tv_trades_sha256": "b3ccb9c57b66baf6dd130b676e52cce418a1f64b4d109cca3725616e5a313530", + "tv_trades_rows": 16 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/metrics.json new file mode 100644 index 00000000..5820765c --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i2-exit-w2-long", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 8, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "5134f202f32bf6f1afecf375dde1f674782bbbd25d2c679f601da7404b9309f3", + "tvTradesCsvHash": "b3ccb9c57b66baf6dd130b676e52cce418a1f64b4d109cca3725616e5a313530", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.76110", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "310fb1c2c1d2942433f2e12221f1d91b2190c69186981cbe46c8ad416ca36ce8", + "decodedReportSha256": "f6fb357d8b3522574b0b8e668005701bd610670fddf94352b898f49cef3495fa", + "closedTrades": 8, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:09:37.639Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/strategy.pine new file mode 100644 index 00000000..3e221ef5 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/strategy.pine @@ -0,0 +1,39 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 2: a strategy.exit placed in the shape of H-MEASURE +// Finding 6d. Lot A is open; lot B is a stop entry placed exactly on bar j's SECOND extreme +// (a buy stop at the high of a low-first bar), +// so the calc_on_order_fills recalculation B's fill starts runs on that point with A still +// open, and places strategy.exit("AX", "A", stop, qty=100) with the stop between bar j's close +// and that extreme: reached on the extreme->close leg. pyramiding=2; everything is closed on +// bar j+1. Cycles j = 7, 19, 26, 42 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I2 exit-w2 long", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=2, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +bStop = k == 6 ? 11.665 : k == 18 ? 11.715 : k == 25 ? 11.96 : k == 41 ? 11.785 : na +axStop = k == 7 ? 11.66 : k == 19 ? 11.71 : k == 26 ? 11.95 : k == 42 ? 11.77 : na +if k >= 0 and time < tEnd + if (k == 5 or k == 17 or k == 24 or k == 40) and units == 0 + strategy.entry("A", strategy.long) + if not na(bStop) + strategy.entry("B", strategy.long, stop=bStop) + if (k == 8 or k == 20 or k == 27 or k == 43) and units != 0 + strategy.close_all() + if math.abs(units) > 150 and not na(axStop) + strategy.exit("AX", "A", stop=axStop, qty=100) +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/tv_trades.csv new file mode 100644 index 00000000..fc3d26b5 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-long/tv_trades.csv @@ -0,0 +1,17 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:15,AX,11.66,100,1150,15.7684,1.37,0.2316,16.885,1.47,-0.115,-0.01,15.6517,0.02,1 +1,Entry long,2025-07-02 23:00,A,11.5,100,1150,15.7684,1.37,0.2316,16.885,1.47,-0.115,-0.01,15.6517,0.02,1 +2,Exit long,2025-07-02 23:45,Close position order,11.66,100,1167,-1.2333,-0.11,0.2333,2.8833,0.25,-2.1167,-0.18,14.5351,0.01,2 +2,Entry long,2025-07-02 23:15,B,11.67,100,1167,-1.2333,-0.11,0.2333,2.8833,0.25,-2.1167,-0.18,14.5351,0.01,2 +3,Exit long,2025-07-03 02:15,AX,11.71,100,1165,5.7664,0.49,0.2336,6.8835,0.59,-1.1165,-0.10,20.1843,0.02,1 +3,Entry long,2025-07-03 02:00,A,11.65,100,1165,5.7664,0.49,0.2336,6.8835,0.59,-1.1165,-0.10,20.1843,0.02,1 +4,Exit long,2025-07-03 02:45,Close position order,11.73,100,1172,0.7655,0.07,0.2345,0.8828,0.08,-1.1172,-0.10,21.067,0.02,2 +4,Entry long,2025-07-03 02:15,B,11.72,100,1172,0.7655,0.07,0.2345,0.8828,0.08,-1.1172,-0.10,21.067,0.02,2 +5,Exit long,2025-07-03 21:30,AX,11.95,100,1175,19.763,1.68,0.237,20.8825,1.78,-2.1175,-0.18,40.7104,0.04,1 +5,Entry long,2025-07-03 03:45,A,11.75,100,1175,19.763,1.68,0.237,20.8825,1.78,-2.1175,-0.18,40.7104,0.04,1 +6,Exit long,2025-07-03 22:00,Close position order,11.85,100,1196,-11.2381,-0.94,0.2381,0,0.00,-12.1196,-1.01,29.5919,0.03,2 +6,Entry long,2025-07-03 21:30,B,11.96,100,1196,-11.2381,-0.94,0.2381,0,0.00,-12.1196,-1.01,29.5919,0.03,2 +7,Exit long,2025-07-07 22:00,AX,11.77,100,1168,8.7655,0.75,0.2345,10.8832,0.93,-3.1168,-0.27,38.2395,0.04,1 +7,Entry long,2025-07-07 21:45,A,11.68,100,1168,8.7655,0.75,0.2345,10.8832,0.93,-3.1168,-0.27,38.2395,0.04,1 +8,Exit long,2025-07-07 22:30,Close position order,11.71,100,1179,-8.235,-0.70,0.235,0,0.00,-9.1179,-0.77,30.1224,0.03,2 +8,Entry long,2025-07-07 22:00,B,11.79,100,1179,-8.235,-0.70,0.235,0,0.00,-9.1179,-0.77,30.1224,0.03,2 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/meta.json b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/meta.json new file mode 100644 index 00000000..191684af --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I2 exit-w2 short-pooc", + "slug": "pa2-i2-exit-w2-short-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:09:50.892Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "12b9fe6c3efb7e880747a77fce7ea0134f0f572dd4afc1f53b25becd52b57977", + "tv_trades_sha256": "05f4980868362562d407a5bff8e203c87edbfe6c519fe8fdf6a4e7d646fe8fec", + "tv_trades_rows": 14 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/metrics.json new file mode 100644 index 00000000..2c1fecd4 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i2-exit-w2-short-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 7, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "12b9fe6c3efb7e880747a77fce7ea0134f0f572dd4afc1f53b25becd52b57977", + "tvTradesCsvHash": "05f4980868362562d407a5bff8e203c87edbfe6c519fe8fdf6a4e7d646fe8fec", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.76126", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "1e8fa3e6bfb98e6a3261f4f1cd48a49670fb3c48dca7891430c5de13209fb85c", + "decodedReportSha256": "1efc86551269e66a03c17bdabafc3bf658f1c58b9e8b90247989c0c26e7753c5", + "closedTrades": 7, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:09:50.891Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/strategy.pine new file mode 100644 index 00000000..b8408c2f --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/strategy.pine @@ -0,0 +1,39 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 2: a strategy.exit placed in the shape of H-MEASURE +// Finding 6d. Lot A is open; lot B is a stop entry placed exactly on bar j's SECOND extreme +// (a sell stop at the low of a high-first bar), +// so the calc_on_order_fills recalculation B's fill starts runs on that point with A still +// open, and places strategy.exit("AX", "A", stop, qty=100) with the stop between bar j's close +// and that extreme: reached on the extreme->close leg. pyramiding=2; everything is closed on +// bar j+1. Cycles j = 5, 30, 46, 51 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I2 exit-w2 short-pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=2, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +bStop = k == 4 ? 11.44 : k == 29 ? 11.795 : k == 45 ? 11.61 : k == 50 ? 11.49 : na +axStop = k == 5 ? 11.47 : k == 30 ? 11.83 : k == 46 ? 11.615 : k == 51 ? 11.52 : na +if k >= 0 and time < tEnd + if (k == 3 or k == 28 or k == 44 or k == 49) and units == 0 + strategy.entry("A", strategy.short) + if not na(bStop) + strategy.entry("B", strategy.short, stop=bStop) + if (k == 6 or k == 31 or k == 47 or k == 52) and units != 0 + strategy.close_all() + if math.abs(units) > 150 and not na(axStop) + strategy.exit("AX", "A", stop=axStop, qty=100) +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/tv_trades.csv new file mode 100644 index 00000000..72828c5b --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short-pooc/tv_trades.csv @@ -0,0 +1,15 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-02 22:45,AX,11.47,100,1147,-0.2294,-0.02,0.2294,2.8853,0.25,-5.1147,-0.45,-0.3438,0.00,2 +1,Entry short,2025-07-02 22:15,A,11.47,100,1147,-0.2294,-0.02,0.2294,2.8853,0.25,-5.1147,-0.45,-0.3438,0.00,2 +2,Exit short,2025-07-02 23:00,Close position order,11.58,100,1144,-14.2302,-1.24,0.2302,0,0.00,-14.1144,-1.23,-14.4596,-0.01,1 +2,Entry short,2025-07-02 22:45,B,11.44,100,1144,-14.2302,-1.24,0.2302,0,0.00,-14.1144,-1.23,-14.4596,-0.01,1 +3,Exit short,2025-07-03 22:45,Close position order,11.86,100,1188,1.7626,0.15,0.2374,7.8812,0.66,-0.1188,-0.01,-12.697,-0.01,3 +3,Entry short,2025-07-03 22:00,A,11.88,100,1188,1.7626,0.15,0.2374,7.8812,0.66,-0.1188,-0.01,-12.697,-0.01,3 +4,Exit short,2025-07-07 23:15,Close position order,11.62,100,1179,16.7659,1.42,0.2341,17.8821,1.52,-5.1179,-0.43,3.9528,0.00,9 +4,Entry short,2025-07-04 00:30,B,11.79,100,1179,16.7659,1.42,0.2341,17.8821,1.52,-5.1179,-0.43,3.9528,0.00,9 +5,Exit short,2025-07-07 23:15,Close position order,11.62,100,1161,-1.2323,-0.11,0.2323,0,0.00,-2.1161,-0.18,2.8366,0.00,1 +5,Entry short,2025-07-07 23:00,B,11.61,100,1161,-1.2323,-0.11,0.2323,0,0.00,-2.1161,-0.18,2.8366,0.00,1 +6,Exit short,2025-07-08 00:15,AX,11.52,100,1160,7.7688,0.67,0.2312,10.884,0.94,-1.116,-0.10,10.4905,0.01,2 +6,Entry short,2025-07-07 23:45,A,11.6,100,1160,7.7688,0.67,0.2312,10.884,0.94,-1.116,-0.10,10.4905,0.01,2 +7,Exit short,2025-07-08 00:30,Close position order,11.57,100,1149,-8.2306,-0.72,0.2306,0,0.00,-8.1149,-0.71,2.3748,0.00,1 +7,Entry short,2025-07-08 00:15,B,11.49,100,1149,-8.2306,-0.72,0.2306,0,0.00,-8.1149,-0.71,2.3748,0.00,1 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/meta.json b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/meta.json new file mode 100644 index 00000000..357d9c94 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I2 exit-w2 short", + "slug": "pa2-i2-exit-w2-short", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:09:46.799Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "1d05b28d250c7f27df151cf1d49434fae2e773a8a655b50da61f04b10991aab1", + "tv_trades_sha256": "6864d39da5adfd779657801af3be47c7940783b14c4df0bc8af3e58f4624dd39", + "tv_trades_rows": 14 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/metrics.json new file mode 100644 index 00000000..b59b8189 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i2-exit-w2-short", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 7, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "1d05b28d250c7f27df151cf1d49434fae2e773a8a655b50da61f04b10991aab1", + "tvTradesCsvHash": "6864d39da5adfd779657801af3be47c7940783b14c4df0bc8af3e58f4624dd39", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.76121", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "b457d5b04d54242b5139a57cd76d6ac431690433d5fcbebf35c507d533859181", + "decodedReportSha256": "e810b6c8c94ba5a50b3e9fd93d2346119c3278f2dd359083d99fd5e1015c7cc4", + "closedTrades": 7, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:09:46.798Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/strategy.pine new file mode 100644 index 00000000..0b60370b --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/strategy.pine @@ -0,0 +1,39 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 2: a strategy.exit placed in the shape of H-MEASURE +// Finding 6d. Lot A is open; lot B is a stop entry placed exactly on bar j's SECOND extreme +// (a sell stop at the low of a high-first bar), +// so the calc_on_order_fills recalculation B's fill starts runs on that point with A still +// open, and places strategy.exit("AX", "A", stop, qty=100) with the stop between bar j's close +// and that extreme: reached on the extreme->close leg. pyramiding=2; everything is closed on +// bar j+1. Cycles j = 5, 30, 46, 51 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I2 exit-w2 short", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=2, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +bStop = k == 4 ? 11.44 : k == 29 ? 11.795 : k == 45 ? 11.61 : k == 50 ? 11.49 : na +axStop = k == 5 ? 11.47 : k == 30 ? 11.83 : k == 46 ? 11.615 : k == 51 ? 11.52 : na +if k >= 0 and time < tEnd + if (k == 3 or k == 28 or k == 44 or k == 49) and units == 0 + strategy.entry("A", strategy.short) + if not na(bStop) + strategy.entry("B", strategy.short, stop=bStop) + if (k == 6 or k == 31 or k == 47 or k == 52) and units != 0 + strategy.close_all() + if math.abs(units) > 150 and not na(axStop) + strategy.exit("AX", "A", stop=axStop, qty=100) +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/tv_trades.csv new file mode 100644 index 00000000..573d6fac --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-exit-w2-short/tv_trades.csv @@ -0,0 +1,15 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-02 22:45,AX,11.47,100,1147,-0.2294,-0.02,0.2294,2.8853,0.25,-5.1147,-0.45,-0.3438,0.00,1 +1,Entry short,2025-07-02 22:30,A,11.47,100,1147,-0.2294,-0.02,0.2294,2.8853,0.25,-5.1147,-0.45,-0.3438,0.00,1 +2,Exit short,2025-07-02 23:15,Close position order,11.58,100,1144,-14.2302,-1.24,0.2302,0,0.00,-14.1144,-1.23,-14.4596,-0.01,2 +2,Entry short,2025-07-02 22:45,B,11.44,100,1144,-14.2302,-1.24,0.2302,0,0.00,-14.1144,-1.23,-14.4596,-0.01,2 +3,Exit short,2025-07-03 23:00,Close position order,11.86,100,1187,0.7627,0.06,0.2373,6.8813,0.58,-0.1187,-0.01,-13.6969,-0.01,3 +3,Entry short,2025-07-03 22:15,A,11.87,100,1187,0.7627,0.06,0.2373,6.8813,0.58,-0.1187,-0.01,-13.6969,-0.01,3 +4,Exit short,2025-07-07 23:30,Close position order,11.62,100,1179,16.7659,1.42,0.2341,17.8821,1.52,-5.1179,-0.43,2.9529,0.00,10 +4,Entry short,2025-07-04 00:30,B,11.79,100,1179,16.7659,1.42,0.2341,17.8821,1.52,-5.1179,-0.43,2.9529,0.00,10 +5,Exit short,2025-07-07 23:30,Close position order,11.62,100,1161,-1.2323,-0.11,0.2323,0,0.00,-2.1161,-0.18,1.8367,0.00,2 +5,Entry short,2025-07-07 23:00,B,11.61,100,1161,-1.2323,-0.11,0.2323,0,0.00,-2.1161,-0.18,1.8367,0.00,2 +6,Exit short,2025-07-08 00:15,AX,11.52,100,1160,7.7688,0.67,0.2312,10.884,0.94,-1.116,-0.10,9.4906,0.01,1 +6,Entry short,2025-07-08 00:00,A,11.6,100,1160,7.7688,0.67,0.2312,10.884,0.94,-1.116,-0.10,9.4906,0.01,1 +7,Exit short,2025-07-08 00:45,Close position order,11.57,100,1149,-8.2306,-0.72,0.2306,0,0.00,-8.1149,-0.71,1.3749,0.00,2 +7,Entry short,2025-07-08 00:15,B,11.49,100,1149,-8.2306,-0.72,0.2306,0,0.00,-8.1149,-0.71,1.3749,0.00,2 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/meta.json b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/meta.json new file mode 100644 index 00000000..7cf1a85f --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I2 level-w1 long pooc", + "slug": "pa2-i2-level-w1-long-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:54:27.711Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "46891f2f6bd9c777384a285a1b8e8f437c2b05c5066556eef3115a84210f20d6", + "tv_trades_sha256": "7f7a332d07202c8a1814a7119b9f6cd70dda335fb563d40403d1e7e23ab3be89", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/metrics.json new file mode 100644 index 00000000..201fab77 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i2-level-w1-long-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "46891f2f6bd9c777384a285a1b8e8f437c2b05c5066556eef3115a84210f20d6", + "tvTradesCsvHash": "7f7a332d07202c8a1814a7119b9f6cd70dda335fb563d40403d1e7e23ab3be89", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78404", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "b8a6a22565b627d56b9b6b09eeb3fdf0e192f1e9e10720f09d69d17bcfa63eb6", + "decodedReportSha256": "6a4fada0b775a1601fa55696a3255a9a0ef81de4c0bdda7723aa2b9371fe4850", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:54:27.710Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/strategy.pine new file mode 100644 index 00000000..e599d1d1 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/strategy.pine @@ -0,0 +1,40 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 2, the boundary of H-MEASURE Finding 6d: a LEVEL fill +// on a bar's FIRST extreme. Lot A is a limit entry at bar j's low, the first +// extreme of a low-first bar, so A's fill ends the leg to it; the calc_on_order_fills +// recalculation it starts places strategy.exit("AX", "A", limit) with the limit +// between the two extremes; the recalculation AX's fill starts places the market +// entry C. Everything is closed on bar j+2. +// Cycles j = 4, 25, 40 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I2 level-w1 long pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +closedNow = strategy.closedtrades - nz(strategy.closedtrades[1]) +aLimit = k == 3 ? 11.46 : k == 24 ? 11.73 : k == 39 ? 11.61 : na +axLimit = k == 4 ? 11.50 : k == 25 ? 11.76 : k == 40 ? 11.70 : na +if k >= 0 and time < tEnd + if not na(aLimit) and units == 0 + strategy.entry("A", strategy.long, limit=aLimit) + if not na(axLimit) and units > 0 + strategy.exit("AX", "A", limit=axLimit) + if not na(axLimit) and units == 0 and closedNow == 1 + strategy.entry("C", strategy.long) + if (k == 6 or k == 27 or k == 42) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/tv_trades.csv new file mode 100644 index 00000000..1d2fb911 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long-pooc/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:30,AX,11.5,100,1146,3.7704,0.33,0.2296,3.8854,0.34,-0.1146,-0.01,3.7704,0.00,0 +1,Entry long,2025-07-02 22:30,A,11.46,100,1146,3.7704,0.33,0.2296,3.8854,0.34,-0.1146,-0.01,3.7704,0.00,0 +2,Exit long,2025-07-02 23:00,Close position order,11.58,100,1152,5.769,0.50,0.231,5.8848,0.51,-8.1152,-0.70,9.5394,0.01,2 +2,Entry long,2025-07-02 22:30,C,11.52,100,1152,5.769,0.50,0.231,5.8848,0.51,-8.1152,-0.70,9.5394,0.01,2 +3,Exit long,2025-07-03 03:45,AX,11.76,100,1173,2.7651,0.24,0.2349,2.8827,0.25,-0.1173,-0.01,12.3045,0.01,0 +3,Entry long,2025-07-03 03:45,A,11.73,100,1173,2.7651,0.24,0.2349,2.8827,0.25,-0.1173,-0.01,12.3045,0.01,0 +4,Exit long,2025-07-03 21:45,Close position order,11.85,100,1178,6.7637,0.57,0.2363,17.8822,1.52,-3.1178,-0.26,19.0682,0.02,2 +4,Entry long,2025-07-03 03:45,C,11.78,100,1178,6.7637,0.57,0.2363,17.8822,1.52,-3.1178,-0.26,19.0682,0.02,2 +5,Exit long,2025-07-07 21:30,AX,11.7,100,1161,8.7669,0.76,0.2331,8.8839,0.77,-0.1161,-0.01,27.8351,0.03,0 +5,Entry long,2025-07-07 21:30,A,11.61,100,1161,8.7669,0.76,0.2331,8.8839,0.77,-0.1161,-0.01,27.8351,0.03,0 +6,Exit long,2025-07-07 22:00,Close position order,11.76,100,1173,2.7651,0.24,0.2349,5.8827,0.50,-8.1173,-0.69,30.6002,0.03,2 +6,Entry long,2025-07-07 21:30,C,11.73,100,1173,2.7651,0.24,0.2349,5.8827,0.50,-8.1173,-0.69,30.6002,0.03,2 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/meta.json b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/meta.json new file mode 100644 index 00000000..464d62aa --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I2 level-w1 long", + "slug": "pa2-i2-level-w1-long", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:54:23.192Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "3095c46d581bcdbe02e55186de9b3f3fc6570bc52bfd266dfb9f43959006487e", + "tv_trades_sha256": "9a22c1daf2574a168815188429caaa9de881a9ff5ab637712b468828085b0499", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/metrics.json new file mode 100644 index 00000000..c7a7da4d --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i2-level-w1-long", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "3095c46d581bcdbe02e55186de9b3f3fc6570bc52bfd266dfb9f43959006487e", + "tvTradesCsvHash": "9a22c1daf2574a168815188429caaa9de881a9ff5ab637712b468828085b0499", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78400", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "481bbe4ff95b3d36944495a4d3af55e6e1472d15cb656a6e3bc1df7df9df7e07", + "decodedReportSha256": "916e4be9d1670cca7bef41493ff981a508e94dff47bdb0743851839771ecaf41", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:54:23.192Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/strategy.pine new file mode 100644 index 00000000..20561db0 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/strategy.pine @@ -0,0 +1,40 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 2, the boundary of H-MEASURE Finding 6d: a LEVEL fill +// on a bar's FIRST extreme. Lot A is a limit entry at bar j's low, the first +// extreme of a low-first bar, so A's fill ends the leg to it; the calc_on_order_fills +// recalculation it starts places strategy.exit("AX", "A", limit) with the limit +// between the two extremes; the recalculation AX's fill starts places the market +// entry C. Everything is closed on bar j+2. +// Cycles j = 4, 25, 40 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I2 level-w1 long", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +closedNow = strategy.closedtrades - nz(strategy.closedtrades[1]) +aLimit = k == 3 ? 11.46 : k == 24 ? 11.73 : k == 39 ? 11.61 : na +axLimit = k == 4 ? 11.50 : k == 25 ? 11.76 : k == 40 ? 11.70 : na +if k >= 0 and time < tEnd + if not na(aLimit) and units == 0 + strategy.entry("A", strategy.long, limit=aLimit) + if not na(axLimit) and units > 0 + strategy.exit("AX", "A", limit=axLimit) + if not na(axLimit) and units == 0 and closedNow == 1 + strategy.entry("C", strategy.long) + if (k == 6 or k == 27 or k == 42) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/tv_trades.csv new file mode 100644 index 00000000..0056cc5f --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-long/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:30,AX,11.5,100,1146,3.7704,0.33,0.2296,3.8854,0.34,-0.1146,-0.01,3.7704,0.00,0 +1,Entry long,2025-07-02 22:30,A,11.46,100,1146,3.7704,0.33,0.2296,3.8854,0.34,-0.1146,-0.01,3.7704,0.00,0 +2,Exit long,2025-07-02 23:15,Close position order,11.58,100,1152,5.769,0.50,0.231,5.8848,0.51,-8.1152,-0.70,9.5394,0.01,3 +2,Entry long,2025-07-02 22:30,C,11.52,100,1152,5.769,0.50,0.231,5.8848,0.51,-8.1152,-0.70,9.5394,0.01,3 +3,Exit long,2025-07-03 03:45,AX,11.76,100,1173,2.7651,0.24,0.2349,2.8827,0.25,-0.1173,-0.01,12.3045,0.01,0 +3,Entry long,2025-07-03 03:45,A,11.73,100,1173,2.7651,0.24,0.2349,2.8827,0.25,-0.1173,-0.01,12.3045,0.01,0 +4,Exit long,2025-07-03 22:00,Close position order,11.85,100,1178,6.7637,0.57,0.2363,17.8822,1.52,-3.1178,-0.26,19.0682,0.02,3 +4,Entry long,2025-07-03 03:45,C,11.78,100,1178,6.7637,0.57,0.2363,17.8822,1.52,-3.1178,-0.26,19.0682,0.02,3 +5,Exit long,2025-07-07 21:30,AX,11.7,100,1161,8.7669,0.76,0.2331,8.8839,0.77,-0.1161,-0.01,27.8351,0.03,0 +5,Entry long,2025-07-07 21:30,A,11.61,100,1161,8.7669,0.76,0.2331,8.8839,0.77,-0.1161,-0.01,27.8351,0.03,0 +6,Exit long,2025-07-07 22:15,Close position order,11.76,100,1173,2.7651,0.24,0.2349,5.8827,0.50,-8.1173,-0.69,30.6002,0.03,3 +6,Entry long,2025-07-07 21:30,C,11.73,100,1173,2.7651,0.24,0.2349,5.8827,0.50,-8.1173,-0.69,30.6002,0.03,3 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/meta.json b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/meta.json new file mode 100644 index 00000000..ee5e5d3e --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I2 level-w1 short pooc", + "slug": "pa2-i2-level-w1-short-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:54:34.844Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "05533896f2b23d374a53a7e6b1951f08ad44be782c1082dfc82972bc29e1802e", + "tv_trades_sha256": "c092e35ad56281fdc114e0923e9e24e3c47ea7f7a93ddce883b2cde07d69fd99", + "tv_trades_rows": 10 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/metrics.json new file mode 100644 index 00000000..b7d9f140 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i2-level-w1-short-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 5, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "05533896f2b23d374a53a7e6b1951f08ad44be782c1082dfc82972bc29e1802e", + "tvTradesCsvHash": "c092e35ad56281fdc114e0923e9e24e3c47ea7f7a93ddce883b2cde07d69fd99", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78407", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "6ed2bd84864ded65eb95d1938cf5b338fcac7facd55969408656793b99a074c7", + "decodedReportSha256": "288f583e84d642838b7b491c5d0b6a3bf3003e848ba031f59e2b65e61b404fce", + "closedTrades": 5, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:54:34.843Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/strategy.pine new file mode 100644 index 00000000..137942bb --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/strategy.pine @@ -0,0 +1,40 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 2, the boundary of H-MEASURE Finding 6d: a LEVEL fill +// on a bar's FIRST extreme. Lot A is a limit entry at bar j's high, the first +// extreme of a high-first bar, so A's fill ends the leg to it; the calc_on_order_fills +// recalculation it starts places strategy.exit("AX", "A", limit) with the limit +// between the two extremes; the recalculation AX's fill starts places the market +// entry C. Everything is closed on bar j+2. +// Cycles j = 2, 32, 44 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I2 level-w1 short pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +closedNow = strategy.closedtrades - nz(strategy.closedtrades[1]) +aLimit = k == 1 ? 11.48 : k == 31 ? 11.86 : k == 43 ? 11.72 : na +axLimit = k == 2 ? 11.43 : k == 32 ? 11.82 : k == 44 ? 11.69 : na +if k >= 0 and time < tEnd + if not na(aLimit) and units == 0 + strategy.entry("A", strategy.short, limit=aLimit) + if not na(axLimit) and units < 0 + strategy.exit("AX", "A", limit=axLimit) + if not na(axLimit) and units == 0 and closedNow == 1 + strategy.entry("C", strategy.short) + if (k == 4 or k == 34 or k == 46) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/tv_trades.csv new file mode 100644 index 00000000..9d8be1c8 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short-pooc/tv_trades.csv @@ -0,0 +1,11 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-02 22:00,AX,11.43,100,1148,4.7709,0.42,0.2291,4.8852,0.43,-0.1148,-0.01,4.7709,0.00,0 +1,Entry short,2025-07-02 22:00,A,11.48,100,1148,4.7709,0.42,0.2291,4.8852,0.43,-0.1148,-0.01,4.7709,0.00,0 +2,Exit short,2025-07-02 22:30,Close position order,11.48,100,1141,-7.2289,-0.63,0.2289,0,0.00,-11.1141,-0.97,-2.458,0.00,2 +2,Entry short,2025-07-02 22:00,C,11.41,100,1141,-7.2289,-0.63,0.2289,0,0.00,-11.1141,-0.97,-2.458,0.00,2 +3,Exit short,2025-07-03 23:00,AX,11.82,100,1186,3.7632,0.32,0.2368,5.8814,0.50,-0.1186,-0.01,1.3052,0.00,1 +3,Entry short,2025-07-03 22:45,A,11.86,100,1186,3.7632,0.32,0.2368,5.8814,0.50,-0.1186,-0.01,1.3052,0.00,1 +4,Exit short,2025-07-07 22:30,AX,11.69,100,1172,2.7659,0.24,0.2341,2.8828,0.25,-0.1172,-0.01,4.0711,0.00,0 +4,Entry short,2025-07-07 22:30,A,11.72,100,1172,2.7659,0.24,0.2341,2.8828,0.25,-0.1172,-0.01,4.0711,0.00,0 +5,Exit short,2025-07-07 23:00,Close position order,11.62,100,1167,4.7671,0.41,0.2329,5.8833,0.50,-4.1167,-0.35,8.8382,0.01,2 +5,Entry short,2025-07-07 22:30,C,11.67,100,1167,4.7671,0.41,0.2329,5.8833,0.50,-4.1167,-0.35,8.8382,0.01,2 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/meta.json b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/meta.json new file mode 100644 index 00000000..a6bccb9b --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I2 level-w1 short", + "slug": "pa2-i2-level-w1-short", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:54:31.502Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "c4c67c052c31d61dc41182bf504cfa77f0478a322cfa403b2dd25c3a6fbb2aec", + "tv_trades_sha256": "8d4893ade201e54203b7723abe267e211a26c92480620fea341d73882f12014d", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/metrics.json new file mode 100644 index 00000000..45b037f0 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i2-level-w1-short", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "c4c67c052c31d61dc41182bf504cfa77f0478a322cfa403b2dd25c3a6fbb2aec", + "tvTradesCsvHash": "8d4893ade201e54203b7723abe267e211a26c92480620fea341d73882f12014d", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78405", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "189db241334481103cc524b9c74adc52df46004b00cbd44686880d8f21be0a98", + "decodedReportSha256": "134191aca67aea546682216b1a165d23accd1b8f5bced09a1d1c30be48a2a640", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:54:31.501Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/strategy.pine new file mode 100644 index 00000000..76d3e5d0 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/strategy.pine @@ -0,0 +1,40 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 2, the boundary of H-MEASURE Finding 6d: a LEVEL fill +// on a bar's FIRST extreme. Lot A is a limit entry at bar j's high, the first +// extreme of a high-first bar, so A's fill ends the leg to it; the calc_on_order_fills +// recalculation it starts places strategy.exit("AX", "A", limit) with the limit +// between the two extremes; the recalculation AX's fill starts places the market +// entry C. Everything is closed on bar j+2. +// Cycles j = 2, 32, 44 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I2 level-w1 short", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +closedNow = strategy.closedtrades - nz(strategy.closedtrades[1]) +aLimit = k == 1 ? 11.48 : k == 31 ? 11.86 : k == 43 ? 11.72 : na +axLimit = k == 2 ? 11.43 : k == 32 ? 11.82 : k == 44 ? 11.69 : na +if k >= 0 and time < tEnd + if not na(aLimit) and units == 0 + strategy.entry("A", strategy.short, limit=aLimit) + if not na(axLimit) and units < 0 + strategy.exit("AX", "A", limit=axLimit) + if not na(axLimit) and units == 0 and closedNow == 1 + strategy.entry("C", strategy.short) + if (k == 4 or k == 34 or k == 46) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/tv_trades.csv new file mode 100644 index 00000000..88ecc6b7 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-level-w1-short/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-02 22:00,AX,11.43,100,1148,4.7709,0.42,0.2291,4.8852,0.43,-0.1148,-0.01,4.7709,0.00,0 +1,Entry short,2025-07-02 22:00,A,11.48,100,1148,4.7709,0.42,0.2291,4.8852,0.43,-0.1148,-0.01,4.7709,0.00,0 +2,Exit short,2025-07-02 22:45,Close position order,11.48,100,1141,-7.2289,-0.63,0.2289,0,0.00,-11.1141,-0.97,-2.458,0.00,3 +2,Entry short,2025-07-02 22:00,C,11.41,100,1141,-7.2289,-0.63,0.2289,0,0.00,-11.1141,-0.97,-2.458,0.00,3 +3,Exit short,2025-07-03 23:00,AX,11.82,100,1186,3.7632,0.32,0.2368,3.8814,0.33,-0.1186,-0.01,1.3052,0.00,0 +3,Entry short,2025-07-03 23:00,A,11.86,100,1186,3.7632,0.32,0.2368,3.8814,0.33,-0.1186,-0.01,1.3052,0.00,0 +4,Exit short,2025-07-03 23:45,Close position order,11.87,100,1180,-7.2367,-0.61,0.2367,0,0.00,-7.118,-0.60,-5.9315,-0.01,3 +4,Entry short,2025-07-03 23:00,C,11.8,100,1180,-7.2367,-0.61,0.2367,0,0.00,-7.118,-0.60,-5.9315,-0.01,3 +5,Exit short,2025-07-07 22:30,AX,11.69,100,1172,2.7659,0.24,0.2341,2.8828,0.25,-0.1172,-0.01,-3.1656,0.00,0 +5,Entry short,2025-07-07 22:30,A,11.72,100,1172,2.7659,0.24,0.2341,2.8828,0.25,-0.1172,-0.01,-3.1656,0.00,0 +6,Exit short,2025-07-07 23:15,Close position order,11.62,100,1167,4.7671,0.41,0.2329,5.8833,0.50,-4.1167,-0.35,1.6015,0.00,3 +6,Entry short,2025-07-07 22:30,C,11.67,100,1167,4.7671,0.41,0.2329,5.8833,0.50,-4.1167,-0.35,1.6015,0.00,3 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/meta.json b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/meta.json new file mode 100644 index 00000000..8d69235a --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I2 point-w2 long pooc", + "slug": "pa2-i2-point-w2-long-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:54:42.745Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "8da8e156ac2786f990bed1c876c7a4083b057663d45a38debe29343337851406", + "tv_trades_sha256": "1fce1bed23df68231ed7546fe4fe13760116654e945a4cf1cdb02a9d755d6ca5", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/metrics.json new file mode 100644 index 00000000..c40e0e9d --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i2-point-w2-long-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "8da8e156ac2786f990bed1c876c7a4083b057663d45a38debe29343337851406", + "tvTradesCsvHash": "1fce1bed23df68231ed7546fe4fe13760116654e945a4cf1cdb02a9d755d6ca5", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78411", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "341b2285758af80c6f2c724031c0ba171f87421416b69f4553239d7ffe91d9cb", + "decodedReportSha256": "9d9dec1fa5368e83daad0a8102a094235025fe0acc7a7357e8b2de4583c62f05", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:54:42.745Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/strategy.pine new file mode 100644 index 00000000..b2fa91ef --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/strategy.pine @@ -0,0 +1,44 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 2, the boundary of H-MEASURE Finding 6d: a POINT fill +// on a bar's SECOND extreme. Lot P is open with a stop exit PX between bar j's open +// and its low, the first extreme of a low-first bar: PX fills on the leg to it, +// and the calc_on_order_fills recalculation it starts places the market entry A, +// which fills at the low itself; the recalculation A's fill starts places +// strategy.exit("AX", "A", limit) with the limit between the two extremes; the +// recalculation AX's fill starts places the market entry C. Everything is closed on +// bar j+2. Cycles j = 3, 41 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I2 point-w2 long pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +closedNow = strategy.closedtrades - nz(strategy.closedtrades[1]) +pxStop = k == 2 ? 11.44 : k == 40 ? 11.66 : na +axLimit = k == 3 ? 11.47 : k == 41 ? 11.70 : na +if k >= 0 and time < tEnd + if not na(pxStop) and units == 0 + strategy.entry("P", strategy.long) + strategy.exit("PX", "P", stop=pxStop) + if not na(axLimit) and units == 0 and closedNow == 1 + strategy.entry("A", strategy.long) + if not na(axLimit) and units > 0 and strategy.opentrades.entry_id(0) == "A" + strategy.exit("AX", "A", limit=axLimit) + if not na(axLimit) and units == 0 and closedNow == 2 + strategy.entry("C", strategy.long) + if (k == 5 or k == 43) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/tv_trades.csv new file mode 100644 index 00000000..057358db --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long-pooc/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:15,PX,11.44,100,1145,-1.2289,-0.11,0.2289,0,0.00,-1.1145,-0.10,-1.2289,0.00,1 +1,Entry long,2025-07-02 22:00,P,11.45,100,1145,-1.2289,-0.11,0.2289,0,0.00,-1.1145,-0.10,-1.2289,0.00,1 +2,Exit long,2025-07-02 22:15,AX,11.49,100,1143,5.7708,0.50,0.2292,5.8857,0.51,0,0.00,4.5419,0.00,0 +2,Entry long,2025-07-02 22:15,A,11.43,100,1143,5.7708,0.50,0.2292,5.8857,0.51,0,0.00,4.5419,0.00,0 +3,Exit long,2025-07-02 22:45,Close position order,11.51,100,1147,3.7702,0.33,0.2298,4.8853,0.43,-3.1147,-0.27,8.3121,0.01,2 +3,Entry long,2025-07-02 22:15,C,11.47,100,1147,3.7702,0.33,0.2298,4.8853,0.43,-3.1147,-0.27,8.3121,0.01,2 +4,Exit long,2025-07-07 21:45,PX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,6.0787,0.01,1 +4,Entry long,2025-07-07 21:30,P,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,6.0787,0.01,1 +5,Exit long,2025-07-07 21:45,AX,11.72,100,1165,6.7663,0.58,0.2337,6.8835,0.59,0,0.00,12.845,0.01,0 +5,Entry long,2025-07-07 21:45,A,11.65,100,1165,6.7663,0.58,0.2337,6.8835,0.59,0,0.00,12.845,0.01,0 +6,Exit long,2025-07-07 22:15,Close position order,11.7,100,1172,-2.2342,-0.19,0.2342,6.8828,0.59,-2.1172,-0.18,10.6108,0.01,2 +6,Entry long,2025-07-07 21:45,C,11.72,100,1172,-2.2342,-0.19,0.2342,6.8828,0.59,-2.1172,-0.18,10.6108,0.01,2 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/meta.json b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/meta.json new file mode 100644 index 00000000..85ab5eea --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I2 point-w2 long", + "slug": "pa2-i2-point-w2-long", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:54:38.722Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "28878315c15fe31c6940010ac235e5fd859ddacb676bc0eb1a827d969234be88", + "tv_trades_sha256": "88db2d9cd2a7f2dd4d7692007935c6e165b8476ed418fc9f293f6f691c8728ea", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/metrics.json new file mode 100644 index 00000000..4eb06b2a --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i2-point-w2-long", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "28878315c15fe31c6940010ac235e5fd859ddacb676bc0eb1a827d969234be88", + "tvTradesCsvHash": "88db2d9cd2a7f2dd4d7692007935c6e165b8476ed418fc9f293f6f691c8728ea", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78410", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "6313b9b0db9ccf796cce35c616bfed9111b4a1fb18eeeb9b083ece307962dbce", + "decodedReportSha256": "b013df1363ba3914b9e9e6f05735c98e84e5054b71096d72b3ec9c31db70f5c1", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:54:38.722Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/strategy.pine new file mode 100644 index 00000000..2ef9ce28 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/strategy.pine @@ -0,0 +1,44 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 2, the boundary of H-MEASURE Finding 6d: a POINT fill +// on a bar's SECOND extreme. Lot P is open with a stop exit PX between bar j's open +// and its low, the first extreme of a low-first bar: PX fills on the leg to it, +// and the calc_on_order_fills recalculation it starts places the market entry A, +// which fills at the low itself; the recalculation A's fill starts places +// strategy.exit("AX", "A", limit) with the limit between the two extremes; the +// recalculation AX's fill starts places the market entry C. Everything is closed on +// bar j+2. Cycles j = 3, 41 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I2 point-w2 long", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +closedNow = strategy.closedtrades - nz(strategy.closedtrades[1]) +pxStop = k == 2 ? 11.44 : k == 40 ? 11.66 : na +axLimit = k == 3 ? 11.47 : k == 41 ? 11.70 : na +if k >= 0 and time < tEnd + if not na(pxStop) and units == 0 + strategy.entry("P", strategy.long) + strategy.exit("PX", "P", stop=pxStop) + if not na(axLimit) and units == 0 and closedNow == 1 + strategy.entry("A", strategy.long) + if not na(axLimit) and units > 0 and strategy.opentrades.entry_id(0) == "A" + strategy.exit("AX", "A", limit=axLimit) + if not na(axLimit) and units == 0 and closedNow == 2 + strategy.entry("C", strategy.long) + if (k == 5 or k == 43) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/tv_trades.csv new file mode 100644 index 00000000..f6588042 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-long/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:15,PX,11.44,100,1145,-1.2289,-0.11,0.2289,0,0.00,-1.1145,-0.10,-1.2289,0.00,0 +1,Entry long,2025-07-02 22:15,P,11.45,100,1145,-1.2289,-0.11,0.2289,0,0.00,-1.1145,-0.10,-1.2289,0.00,0 +2,Exit long,2025-07-02 22:15,AX,11.49,100,1143,5.7708,0.50,0.2292,5.8857,0.51,0,0.00,4.5419,0.00,0 +2,Entry long,2025-07-02 22:15,A,11.43,100,1143,5.7708,0.50,0.2292,5.8857,0.51,0,0.00,4.5419,0.00,0 +3,Exit long,2025-07-02 23:00,Close position order,11.5,100,1147,2.7703,0.24,0.2297,4.8853,0.43,-3.1147,-0.27,7.3122,0.01,2 +3,Entry long,2025-07-02 22:30,C,11.47,100,1147,2.7703,0.24,0.2297,4.8853,0.43,-3.1147,-0.27,7.3122,0.01,2 +4,Exit long,2025-07-07 21:45,PX,11.66,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,5.0788,0.01,0 +4,Entry long,2025-07-07 21:45,P,11.68,100,1168,-2.2334,-0.19,0.2334,0,0.00,-2.1168,-0.18,5.0788,0.01,0 +5,Exit long,2025-07-07 21:45,AX,11.72,100,1165,6.7663,0.58,0.2337,6.8835,0.59,0,0.00,11.8451,0.01,0 +5,Entry long,2025-07-07 21:45,A,11.65,100,1165,6.7663,0.58,0.2337,6.8835,0.59,0,0.00,11.8451,0.01,0 +6,Exit long,2025-07-07 22:30,Close position order,11.71,100,1172,-1.2343,-0.11,0.2343,6.8828,0.59,-2.1172,-0.18,10.6108,0.01,2 +6,Entry long,2025-07-07 22:00,C,11.72,100,1172,-1.2343,-0.11,0.2343,6.8828,0.59,-2.1172,-0.18,10.6108,0.01,2 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/meta.json b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/meta.json new file mode 100644 index 00000000..73fcd317 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I2 point-w2 short pooc", + "slug": "pa2-i2-point-w2-short-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:54:50.254Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "eb02c32e5ae293f91820d407e0b36ae1648eca58abf6e357f50431252880ee9b", + "tv_trades_sha256": "03c75065f9522e4e339594fe6c8e703e898ef35878e4351522db6622f372fb1f", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/metrics.json new file mode 100644 index 00000000..2e342ae8 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i2-point-w2-short-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "eb02c32e5ae293f91820d407e0b36ae1648eca58abf6e357f50431252880ee9b", + "tvTradesCsvHash": "03c75065f9522e4e339594fe6c8e703e898ef35878e4351522db6622f372fb1f", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78417", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "4f9d30540c40e73e8a2536a71b082a75b2de38e9b900ea0a3bd4e22cc5251277", + "decodedReportSha256": "54aa5a44943b072dc809ab9ab3fddb8693b351053c9f5e876968781961e83aa9", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:54:50.254Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/strategy.pine new file mode 100644 index 00000000..ebc72b65 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/strategy.pine @@ -0,0 +1,44 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 2, the boundary of H-MEASURE Finding 6d: a POINT fill +// on a bar's SECOND extreme. Lot P is open with a stop exit PX between bar j's open +// and its high, the first extreme of a high-first bar: PX fills on the leg to it, +// and the calc_on_order_fills recalculation it starts places the market entry A, +// which fills at the high itself; the recalculation A's fill starts places +// strategy.exit("AX", "A", limit) with the limit between the two extremes; the +// recalculation AX's fill starts places the market entry C. Everything is closed on +// bar j+2. Cycles j = 2, 30 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I2 point-w2 short pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +closedNow = strategy.closedtrades - nz(strategy.closedtrades[1]) +pxStop = k == 1 ? 11.46 : k == 29 ? 11.85 : na +axLimit = k == 2 ? 11.43 : k == 30 ? 11.82 : na +if k >= 0 and time < tEnd + if not na(pxStop) and units == 0 + strategy.entry("P", strategy.short) + strategy.exit("PX", "P", stop=pxStop) + if not na(axLimit) and units == 0 and closedNow == 1 + strategy.entry("A", strategy.short) + if not na(axLimit) and units < 0 and strategy.opentrades.entry_id(0) == "A" + strategy.exit("AX", "A", limit=axLimit) + if not na(axLimit) and units == 0 and closedNow == 2 + strategy.entry("C", strategy.short) + if (k == 4 or k == 32) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/tv_trades.csv new file mode 100644 index 00000000..0c67296f --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short-pooc/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-02 22:00,PX,11.46,100,1145,-1.2291,-0.11,0.2291,0,0.00,-1.1145,-0.10,-1.2291,0.00,1 +1,Entry short,2025-07-02 21:45,P,11.45,100,1145,-1.2291,-0.11,0.2291,0,0.00,-1.1145,-0.10,-1.2291,0.00,1 +2,Exit short,2025-07-02 22:00,AX,11.41,100,1148,6.7711,0.59,0.2289,6.8852,0.60,0,0.00,5.542,0.01,0 +2,Entry short,2025-07-02 22:00,A,11.48,100,1148,6.7711,0.59,0.2289,6.8852,0.60,0,0.00,5.542,0.01,0 +3,Exit short,2025-07-02 22:30,Close position order,11.48,100,1145,-3.2293,-0.28,0.2293,1.8855,0.16,-7.1145,-0.62,2.3127,0.00,2 +3,Entry short,2025-07-02 22:00,C,11.45,100,1145,-3.2293,-0.28,0.2293,1.8855,0.16,-7.1145,-0.62,2.3127,0.00,2 +4,Exit short,2025-07-03 22:30,PX,11.85,100,1184,-1.2369,-0.10,0.2369,0,0.00,-1.1184,-0.09,1.0758,0.00,1 +4,Entry short,2025-07-03 22:15,P,11.84,100,1184,-1.2369,-0.10,0.2369,0,0.00,-1.1184,-0.09,1.0758,0.00,1 +5,Exit short,2025-07-03 22:30,AX,11.8,100,1187,6.7633,0.57,0.2367,6.8813,0.58,0,0.00,7.8391,0.01,0 +5,Entry short,2025-07-03 22:30,A,11.87,100,1187,6.7633,0.57,0.2367,6.8813,0.58,0,0.00,7.8391,0.01,0 +6,Exit short,2025-07-03 23:00,Close position order,11.82,100,1186,3.7632,0.32,0.2368,5.8814,0.50,-1.1186,-0.09,11.6023,0.01,2 +6,Entry short,2025-07-03 22:30,C,11.86,100,1186,3.7632,0.32,0.2368,5.8814,0.50,-1.1186,-0.09,11.6023,0.01,2 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/meta.json b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/meta.json new file mode 100644 index 00000000..3303a2ab --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I2 point-w2 short", + "slug": "pa2-i2-point-w2-short", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:54:46.406Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "75d344832eb051c8892d02e10076746e7a1667213c8140994ea65b9e44337cf8", + "tv_trades_sha256": "e292e70d771691a3fd44670abc9a699270d5ad677b3d480ff7402a0421fa7abd", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/metrics.json new file mode 100644 index 00000000..35348f63 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i2-point-w2-short", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "75d344832eb051c8892d02e10076746e7a1667213c8140994ea65b9e44337cf8", + "tvTradesCsvHash": "e292e70d771691a3fd44670abc9a699270d5ad677b3d480ff7402a0421fa7abd", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78415", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "85f4cd0d78d662ab29a5861f73ca50ea70da512dd24bd3bf97983a3f2308bcd2", + "decodedReportSha256": "4cd44932c9e7bd4b8c4efa5a5161b4f9a293742e394b859b9eaebd9943e6c508", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:54:46.405Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/strategy.pine new file mode 100644 index 00000000..aeee66a1 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/strategy.pine @@ -0,0 +1,44 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 2, the boundary of H-MEASURE Finding 6d: a POINT fill +// on a bar's SECOND extreme. Lot P is open with a stop exit PX between bar j's open +// and its high, the first extreme of a high-first bar: PX fills on the leg to it, +// and the calc_on_order_fills recalculation it starts places the market entry A, +// which fills at the high itself; the recalculation A's fill starts places +// strategy.exit("AX", "A", limit) with the limit between the two extremes; the +// recalculation AX's fill starts places the market entry C. Everything is closed on +// bar j+2. Cycles j = 2, 30 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I2 point-w2 short", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +closedNow = strategy.closedtrades - nz(strategy.closedtrades[1]) +pxStop = k == 1 ? 11.46 : k == 29 ? 11.85 : na +axLimit = k == 2 ? 11.43 : k == 30 ? 11.82 : na +if k >= 0 and time < tEnd + if not na(pxStop) and units == 0 + strategy.entry("P", strategy.short) + strategy.exit("PX", "P", stop=pxStop) + if not na(axLimit) and units == 0 and closedNow == 1 + strategy.entry("A", strategy.short) + if not na(axLimit) and units < 0 and strategy.opentrades.entry_id(0) == "A" + strategy.exit("AX", "A", limit=axLimit) + if not na(axLimit) and units == 0 and closedNow == 2 + strategy.entry("C", strategy.short) + if (k == 4 or k == 32) and units != 0 + strategy.close_all() +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/tv_trades.csv new file mode 100644 index 00000000..e0e0b76c --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i2-point-w2-short/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-02 22:00,PX,11.46,100,1145,-1.2291,-0.11,0.2291,0,0.00,-1.1145,-0.10,-1.2291,0.00,0 +1,Entry short,2025-07-02 22:00,P,11.45,100,1145,-1.2291,-0.11,0.2291,0,0.00,-1.1145,-0.10,-1.2291,0.00,0 +2,Exit short,2025-07-02 22:00,AX,11.41,100,1148,6.7711,0.59,0.2289,6.8852,0.60,0,0.00,5.542,0.01,0 +2,Entry short,2025-07-02 22:00,A,11.48,100,1148,6.7711,0.59,0.2289,6.8852,0.60,0,0.00,5.542,0.01,0 +3,Exit short,2025-07-02 22:45,Close position order,11.48,100,1145,-3.2293,-0.28,0.2293,1.8855,0.16,-7.1145,-0.62,2.3127,0.00,2 +3,Entry short,2025-07-02 22:15,C,11.45,100,1145,-3.2293,-0.28,0.2293,1.8855,0.16,-7.1145,-0.62,2.3127,0.00,2 +4,Exit short,2025-07-03 22:30,PX,11.85,100,1184,-1.2369,-0.10,0.2369,0,0.00,-1.1184,-0.09,1.0758,0.00,0 +4,Entry short,2025-07-03 22:30,P,11.84,100,1184,-1.2369,-0.10,0.2369,0,0.00,-1.1184,-0.09,1.0758,0.00,0 +5,Exit short,2025-07-03 22:30,AX,11.8,100,1187,6.7633,0.57,0.2367,6.8813,0.58,0,0.00,7.8391,0.01,0 +5,Entry short,2025-07-03 22:30,A,11.87,100,1187,6.7633,0.57,0.2367,6.8813,0.58,0,0.00,7.8391,0.01,0 +6,Exit short,2025-07-03 23:15,Close position order,11.82,100,1186,3.7632,0.32,0.2368,5.8814,0.50,-1.1186,-0.09,11.6023,0.01,2 +6,Entry short,2025-07-03 22:45,C,11.86,100,1186,3.7632,0.32,0.2368,5.8814,0.50,-1.1186,-0.09,11.6023,0.01,2 \ No newline at end of file diff --git a/tests/oracle.sha256 b/tests/oracle.sha256 index a154af0b..2ad92cd7 100644 --- a/tests/oracle.sha256 +++ b/tests/oracle.sha256 @@ -1,7 +1,7 @@ { "files": { "README.md": "36b94c5f566702ca6de6b0a3a9ccdaf44e2218786d4306e489f17b10b827468b", - "test_oracle_coof.cpp": "a175d4449f45a23a850217996cc528515ec6f673bb4951ffc3ab9b8583bbe681", + "test_oracle_coof.cpp": "647f8a2474b50570c2e24e993065bb905ace9c84f65591caae0b998dc0729c98", "test_oracle_day_key.cpp": "14d3a55499d5cd951d34fca121b8ca2574f44ebd12691968612132d91a243ea0", "test_oracle_deferred_any_witnesses.cpp": "c7fc11a952d45971eb42a602bf5a37328b6527aabb1c1948e197383ff7727ab3", "test_oracle_deferred_birth.cpp": "1532354df0ffafacb892072d2721f29fb185bce3a53f52ed975f6e0cd5173774", @@ -22,5 +22,5 @@ "test_oracle_stop_snapshot.cpp": "d3a14a771538873144c8f03a9d8cf391256e9967ad31a03e29e1ed46359daf1d" }, "schema": "pineforge-r4-d-oracle-tree/v1", - "treeSha256": "7e128176f39565736b9dc020f097ef6178b7b1081efce4a89e93158c0c3433dc" + "treeSha256": "f39b54fe652e61fbe0f870d2a7c924e4777389893bf530fbd18d244f06da0312" } diff --git a/tests/oracle/test_oracle_coof.cpp b/tests/oracle/test_oracle_coof.cpp index b139f4d8..b809cdeb 100644 --- a/tests/oracle/test_oracle_coof.cpp +++ b/tests/oracle/test_oracle_coof.cpp @@ -148,7 +148,8 @@ void test_r1_cascade_bracket_does_not_exact_fill_on_w2_c_segment() { // A cascade MARKET re-entry born at W2 (only C remains). Path (H near): // O=100 -> W1=101 -> W2=90 -> C=95. E@100 (bar-open) -> stop bracket @90 exits // at W2 -> re-enter E (cascade market): the fixed-budget engine fills it at -// the C tick (95); the new rule rolls it to the next bar's open (96). +// the C tick (95); ab9714be's rule rolled it to the next bar's open (96), and +// it fills at W2 (90) since R5 lane PAR-ORDERS-2 (below). class CascadeMarketRollProbe final : public CoofBase { public: void on_source_bar(const Bar&) override { @@ -185,11 +186,17 @@ void test_r2_cascade_market_only_c_remains_rolls_to_next_open() { CHECK(near(p.get_trade(0).entry_price, 100.0)); CHECK(near(p.get_trade(0).exit_price, 90.0)); } - // The rolled cascade re-entry fills at bar 2's OPEN (96), not the bar-1 C - // tick (95) the fixed budget would have used. + // The pin of this row a later lane moved on purpose (R5 lane PAR-ORDERS-2, + // H-MEASURE Finding 6d; tests/test_second_extreme_order_tapes.cpp): + // expectation corrected: the re-entry's lot 96 (bar 2's open) -> 90 (bar + // 1's W2), because X's stop is the matcher's fill at its own level on the + // leg to W2, and TradingView fills the market order such a fill's + // recalculation places at that extreme (lab tv pa2-i2-exit-w2-*, + // hm-chart-diff-v2 / -v3 / -v7 row 5) -- neither at the C tick (95) the + // fixed budget used nor at the next open. CHECK(p.open_lot_count() == 1); if (p.open_lot_count() == 1) { - CHECK(near(p.open_lot_prices().front(), 96.0)); // RED vs budget (95.0) + CHECK(near(p.open_lot_prices().front(), 90.0)); } CHECK(near(p.signed_size(), 1.0)); } diff --git a/tests/test_coof_cascade_eligibility.cpp b/tests/test_coof_cascade_eligibility.cpp index 785f84ba..4e98905c 100644 --- a/tests/test_coof_cascade_eligibility.cpp +++ b/tests/test_coof_cascade_eligibility.cpp @@ -142,7 +142,8 @@ void test_r1_cascade_bracket_does_not_exact_fill_on_w2_c_segment() { // A cascade MARKET re-entry born at W2 (only C remains). Path (H near): // O=100 -> W1=101 -> W2=90 -> C=95. E@100 (bar-open) -> stop bracket @90 exits // at W2 -> re-enter E (cascade market): the fixed-budget engine fills it at -// the C tick (95); the new rule rolls it to the next bar's open (96). +// the C tick (95); ab9714be's rule rolled it to the next bar's open (96), and +// it fills at W2 (90) since R5 lane PAR-ORDERS-2 (below). class CascadeMarketRollProbe final : public CoofBase { public: void on_source_bar(const Bar&) override { @@ -179,11 +180,17 @@ void test_r2_cascade_market_only_c_remains_rolls_to_next_open() { CHECK(near(p.get_trade(0).entry_price, 100.0)); CHECK(near(p.get_trade(0).exit_price, 90.0)); } - // The rolled cascade re-entry fills at bar 2's OPEN (96), not the bar-1 C - // tick (95) the fixed budget would have used. + // The pin of this row a later lane moved on purpose (R5 lane PAR-ORDERS-2, + // H-MEASURE Finding 6d; tests/test_second_extreme_order_tapes.cpp): + // expectation corrected: the re-entry's lot 96 (bar 2's open) -> 90 (bar + // 1's W2), because X's stop is the matcher's fill at its own level on the + // leg to W2, and TradingView fills the market order such a fill's + // recalculation places at that extreme (lab tv pa2-i2-exit-w2-*, + // hm-chart-diff-v2 / -v3 / -v7 row 5) -- neither at the C tick (95) the + // fixed budget used nor at the next open. CHECK(p.open_lot_count() == 1); if (p.open_lot_count() == 1) { - CHECK(near(p.open_lot_prices().front(), 96.0)); // RED vs budget (95.0) + CHECK(near(p.open_lot_prices().front(), 90.0)); } CHECK(near(p.signed_size(), 1.0)); } diff --git a/tests/test_magnified_aggregated_tape.cpp b/tests/test_magnified_aggregated_tape.cpp index 783ffb00..f7389a40 100644 --- a/tests/test_magnified_aggregated_tape.cpp +++ b/tests/test_magnified_aggregated_tape.cpp @@ -28,7 +28,7 @@ * v0 (plain), v1 (process_orders_on_close), v2 (calc_on_order_fills), v4 * (slippage 2, pyramiding 3), v5 (process_orders_on_close, slippage 2, * pyramiding 3); controls: the chart and plain-aggregated paths against - * hm-chart-diff-v0 / v1 / v4 / v5. + * hm-chart-diff-v0 / v1 / v2 / v4 / v5. * * v1 and v5 (R5 lane PAR-ORDERS): under process_orders_on_close a stop entry * whose stop the placing close already reached fills at that close, the @@ -38,12 +38,18 @@ * quiet-bar gate read the script-bar index where the pass reads the input * slot, and skipped it on every such run. The same stop entries under * calc_on_order_fills fill there too: v3 (all three paths) and v7 (chart and - * aggregated) book them at TradingView's bar and price. Their other rows are - * RECORDED divergences, each asserted to differ, so a fix flips them - * deliberately: - * - v3 / v7 row 5 (chart, aggregated): the calc_on_order_fills market entry - * the fill at bar 19's high re-issues books the close (11.71 / 11.73) - * where TradingView books that high (11.72 / 11.74); + * aggregated) book them at TradingView's bar and price. + * + * Row 5 of v2, v3 and v7 (chart, aggregated; R5 lane PAR-ORDERS-2, H-MEASURE + * Finding 6d): the calc_on_order_fills market entry the fill at bar 19's high + * re-issues books that high (11.72 / 11.72 / 11.74), as TradingView does: the + * fill is the matcher's, PX's limit at its own level on the leg to the bar's + * second extreme, and a market order its recalculation places fills at that + * extreme. It used to book the close (11.71 / 11.73) under + * process_orders_on_close and the next open without it. + * + * The other rows below are RECORDED divergences, each asserted to differ, so a + * fix flips them deliberately: * - v7 rows 8 and 9 (chart, aggregated): after bar 33's close_all and * same-bar add TradingView keeps the add (ML 11.86) and fills bar 38's * stop entry at 11.83, the engine drops the add and books 11.82; @@ -391,8 +397,9 @@ int main() { replay("hm-chart-diff-v1", v1, path); replay("hm-chart-diff-v4", v4, path); replay("hm-chart-diff-v5", v5, path); - replay("hm-chart-diff-v3", v3, path, rows({5})); - replay("hm-chart-diff-v7", v7, path, rows({5, 8, 9})); + replay("hm-chart-diff-v2", v2, path); + replay("hm-chart-diff-v3", v3, path); + replay("hm-chart-diff-v7", v7, path, rows({8, 9})); } // 3. the pyramiding cap pyramiding_cap_holds(all); diff --git a/tests/test_second_extreme_order_tapes.cpp b/tests/test_second_extreme_order_tapes.cpp new file mode 100644 index 00000000..c3f07ed8 --- /dev/null +++ b/tests/test_second_extreme_order_tapes.cpp @@ -0,0 +1,440 @@ +/* + * test_second_extreme_order_tapes.cpp -- R5 lane PAR-ORDERS-2 (item 2). + * + * Under calc_on_order_fills, the recalculation a fill starts may sit exactly + * on the bar's SECOND extreme -- the end of the leg that approaches it, the + * high of a low-first bar or the low of a high-first one. H-MEASURE Finding + * 6d: TradingView fills a market entry that recalculation places at that + * extreme, where ab9714be (and this engine until lane PAR-ORDERS-2) advanced + * it to the close, which admits no cascade order, and so to the next open + * (tests/test_magnified_aggregated_tape.cpp row 5). The same holds for the + * orders beside it: an exit the recalculation places is live from that point. + * + * The lane's own synthetic script, NYSE:F 15m over the three days of + * tests/fixtures/session_islastbar (`lab tv`, ws-report-v1, rangeProof covered; + * tests/fixtures/second_extreme_orders, whose README names each tape): lot A + * is open; lot B is a stop entry placed exactly on bar j's second extreme, so + * B's fill starts the recalculation there with A still open (pyramiding 2), + * and that recalculation + * pa2-i2-exit-w2-* places strategy.exit("AX", "A", stop, qty=100) with the + * stop between the bar's close and the extreme: reached on + * the extreme -> close leg. + * Long lots on low-first bars (cycles j = 7, 19, 26, 42), short lots on + * high-first ones (j = 5, 30, 46, 51); with and without + * process_orders_on_close. Everything is closed on bar j+1. + * + * TradingView, on every long cycle and the first short one: A's exit fills on + * bar j. The short + * tapes are compared on their first cycle only: on bar 30 TradingView never + * fills B's sell stop at the off-grid 11.795 (the bar's low) and fills it on + * bar 38 instead, while this engine fills it on bar 30 -- a separate stop-touch + * question on an off-grid level, which shifts every later short cycle. + * + * The boundary: B's fill is the matcher's, at B's own level, which ENDS the + * leg to the extreme. A fill the calc_on_order_fills path books AT the extreme + * itself -- an order a recalculation of the bar placed, gap-filled there -- + * STARTS the next leg, and the market orders its recalculation places advance + * to the close (process_orders_on_close) or the next open, as ab9714be has it + * (corpus bracket-rivet-calc-on-fill-01 trades 350 and 516). Two more probe + * bodies, one flat lot at a time (pyramiding 1), on every cycle: + * pa2-i2-point-w2-* P's stop exit PX fills on the leg to the FIRST extreme; + * the recalculation places the market entry A, which + * fills on that extreme; A's recalculation places + * strategy.exit("AX", "A", limit) between the extremes, + * which gap-fills at the second; AX's recalculation + * places the market entry C (long j = 3, 41; short + * j = 2, 30); + * pa2-i2-level-w1-* A is a limit entry AT the first extreme, AX and C as + * above (long j = 4, 25, 40; short j = 2, 32, 44). + * TradingView: point-w2 books AX at the second extreme and C at the close / + * next open; level-w1 books AX at its own level on the leg to the second + * extreme and C at that extreme. The short point-w2 tapes are exact. + * + * What the boundary rows record (RECORDED divergences, older than this lane: + * the lane's base answers every one of them alike): + * - level-w1, rows 1-6 (long) and 1, 2, 5, 6 (short): the exit a + * recalculation places on the matcher's fill AT the first extreme is + * forced onto the second one (exit(): the in-flight remainder of a + * leg-end fill is the whole next leg), so AX books that extreme instead + * of its level and C, placed at a forced fill, the close / next open; + * the process_orders_on_close short tape is compared on its first cycle: + * on bar 31 TradingView fills A's sell limit at the close its high + * already reached, where the engine fills it on bar 32; + * - point-w2 long, rows 2, 3, 5, 6 (row 6 agrees under + * process_orders_on_close, where the close is the extreme): A books the + * off-grid low's tick (11.425 -> 11.43, 11.645 -> 11.65), which + * next_source_path_waypoint reads as a fill short of the low, so AX is a + * plain limit at its level and C fills at the second extreme. + * + * What the rows record: + * - the exit tapes' rows 5 and 7 (long, both variants) and row 1 (short, both + * variants): the exit fills on bar j, as on TradingView, but at the close's + * tick where TradingView books the stop's own level (11.95 / 11.77 / + * 11.47): a stop born on the extreme is matched at the close point, not + * on the leg to it. RECORDED divergences, asserted to differ. + * Everything else is exact: side, instants, prices, quantity, signals. + * + * Fail-before, this TU against the lane's base (6945fc19): every row of the + * long exit tapes (8 of 8; 7 of 8 with process_orders_on_close) exits A at the + * next open. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; + +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +#ifndef PINEFORGE_PA2_SECOND_EXTREME_FIXTURE_DIR +#error "PINEFORGE_PA2_SECOND_EXTREME_FIXTURE_DIR must name tests/fixtures/second_extreme_orders" +#endif + +namespace { + +struct FeedBar { + std::int64_t ts; + double open, high, low, close, volume; +}; + +#include "fixtures/session_islastbar/bars.inc" + +constexpr std::int64_t kMinute = 60'000; +const double kNaN = std::numeric_limits::quiet_NaN(); + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +// "YYYY-MM-DD HH:MM" in the tape's UTC+8 -> UTC milliseconds. +std::int64_t tape_ms(const std::string& text) { + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + if (std::sscanf(text.c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) return -1; + const std::int64_t days = days_from_civil(y, static_cast(mo), + static_cast(d)); + return ((days * 24 + h - 8) * 60 + mi) * kMinute; +} + +struct TapeTrade { + bool is_long = true; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = 0.0, exit_price = 0.0, qty = 0.0; + std::string entry_signal, exit_signal; +}; + +std::vector read_tape(const std::string& slug) { + std::ifstream in(std::string(PINEFORGE_PA2_SECOND_EXTREME_FIXTURE_DIR) + "/" + slug + + "/tv_trades.csv"); + std::vector trades; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 17) continue; + const std::size_t n = static_cast(std::stoi(cell[0])); + if (trades.size() < n) trades.resize(n); + TapeTrade& t = trades[n - 1]; + const bool entry = cell[1].rfind("Entry", 0) == 0; + t.is_long = cell[1].find("long") != std::string::npos; + (entry ? t.entry_ms : t.exit_ms) = tape_ms(cell[2]); + (entry ? t.entry_signal : t.exit_signal) = cell[3]; + (entry ? t.entry_price : t.exit_price) = std::stod(cell[4]); + t.qty = std::stod(cell[5]); + } + return trades; +} + +// Which probe body a tape ran: lot B's stop on the second extreme (items 2 +// and 4), or the boundary pair -- A's limit on the FIRST extreme (a level +// fill) or A's market fill ON it (a point fill), each followed by the exit AX +// and the market entry C. +enum class Probe { SecondExtremeStop, LevelFirstExtreme, PointSecondExtreme }; + +struct Variant { + const char* slug; + bool is_long; + bool exit_leg; // strategy.exit (items 2) or strategy.close (item 4) + bool pooc; + std::size_t compared; // rows compared (the short tapes: their first cycle) + unsigned recorded; // bit i+1: row i+1 is a RECORDED divergence + Probe probe = Probe::SecondExtremeStop; +}; + +// The Pine probe's body (tests/fixtures/second_extreme_orders//strategy.pine). +class ExtremeHost final : public source::PineStrategyHost { +public: + explicit ExtremeHost(const Variant& v) : v_(v) { + set_syminfo_session("0930-1600"); + set_syminfo_timezone("America/New_York"); + source::PineStrategyConfig c; + c.initial_capital = 100000; + c.default_qty_type = static_cast(QtyType::FIXED); + c.default_qty_value = 100; + c.pyramiding = v.probe == Probe::SecondExtremeStop ? 2 : 1; + c.process_orders_on_close = v.pooc; + c.calc_on_order_fills = true; + c.slippage = 0; + c.commission_type = static_cast(CommissionType::PERCENT); + c.commission_value = 0.01; + c.margin_long = 100; + c.margin_short = 100; + configure_pine_strategy(c); + syminfo_mintick_ = 0.01; + } + void on_source_bar(const Bar&) override { + const int k = bar_index_; // the replay's first bar is the Pine counter's origin + const double units = signed_position_size(); + if (v_.probe != Probe::SecondExtremeStop) { + boundary(k, units); + return; + } + static const int kLong[] = {7, 19, 26, 42}; + static const double kLongStop[] = {11.665, 11.715, 11.96, 11.785}; + static const double kLongAx[] = {11.66, 11.71, 11.95, 11.77}; + static const int kShort[] = {5, 30, 46, 51}; + static const double kShortStop[] = {11.44, 11.795, 11.61, 11.49}; + static const double kShortAx[] = {11.47, 11.83, 11.615, 11.52}; + const int* js = v_.is_long ? kLong : kShort; + const double* stops = v_.is_long ? kLongStop : kShortStop; + const double* ax = v_.is_long ? kLongAx : kShortAx; + for (int c = 0; c < 4; ++c) { + const int j = js[c]; + if (k == j - 2 && units == 0.0) strategy_entry("A", v_.is_long); + if (k == j - 1) strategy_entry("B", v_.is_long, kNaN, stops[c]); + if (k == j + 1 && units != 0.0) strategy_close_all(); + } + // B's fill opened the second lot: this is the recalculation on the + // second extreme (or, when A has not gone yet, the bar's close). + if (std::fabs(units) > 150.0) { + if (!v_.exit_leg) { + strategy_close("A"); + return; + } + for (int c = 0; c < 4; ++c) { + if (k == js[c]) + strategy_exit("AX", "A", kNaN, ax[c], kNaN, kNaN, kNaN, 100.0, {}, 100.0); + } + } + } + + struct Row { + bool is_long = true; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = 0.0, exit_price = 0.0, qty = 0.0; + int entry_bar = 0, exit_bar = 0; + std::string entry_id, exit_id; + }; + std::vector rows() const { + std::vector out; + for (int i = 0; i < trade_count(); ++i) { + Row r; + r.is_long = trades_[static_cast(i)].is_long; + r.entry_ms = closed_trade_entry_time(i); + r.exit_ms = closed_trade_exit_time(i); + r.entry_price = closed_trade_entry_price(i); + r.exit_price = closed_trade_exit_price(i); + r.qty = closed_trade_size(i); + r.entry_bar = closed_trade_entry_bar_index(i); + r.exit_bar = closed_trade_exit_bar_index(i); + r.entry_id = closed_trade_entry_id(i); + r.exit_id = closed_trade_exit_id(i); + out.push_back(r); + } + return out; + } + +private: + // strategy.closedtrades - strategy.closedtrades[1]: the history slot is + // the count the bar's last calculation saw. + int closed_now(int k) { + if (k != bar_) { + closed_before_ = closed_seen_; + bar_ = k; + } + closed_seen_ = trade_count(); + return closed_seen_ - closed_before_; + } + + void boundary(int k, double units) { + const bool level = v_.probe == Probe::LevelFirstExtreme; + static const int kLevelLong[] = {4, 25, 40}; + static const double kLevelLongA[] = {11.46, 11.73, 11.61}; + static const double kLevelLongAx[] = {11.50, 11.76, 11.70}; + static const int kLevelShort[] = {2, 32, 44}; + static const double kLevelShortA[] = {11.48, 11.86, 11.72}; + static const double kLevelShortAx[] = {11.43, 11.82, 11.69}; + static const int kPointLong[] = {3, 41}; + static const double kPointLongPx[] = {11.44, 11.66}; + static const double kPointLongAx[] = {11.47, 11.70}; + static const int kPointShort[] = {2, 30}; + static const double kPointShortPx[] = {11.46, 11.85}; + static const double kPointShortAx[] = {11.43, 11.82}; + const int cycles = level ? 3 : 2; + const int* js = level ? (v_.is_long ? kLevelLong : kLevelShort) + : (v_.is_long ? kPointLong : kPointShort); + const double* placed = level ? (v_.is_long ? kLevelLongA : kLevelShortA) + : (v_.is_long ? kPointLongPx : kPointShortPx); + const double* ax = level ? (v_.is_long ? kLevelLongAx : kLevelShortAx) + : (v_.is_long ? kPointLongAx : kPointShortAx); + const int closed = closed_now(k); + const bool live = v_.is_long ? units > 0.0 : units < 0.0; + for (int c = 0; c < cycles; ++c) { + const int j = js[c]; + if (k == j - 1 && units == 0.0) { + if (level) { + strategy_entry("A", v_.is_long, placed[c]); + } else { + strategy_entry("P", v_.is_long); + strategy_exit("PX", "P", kNaN, placed[c]); + } + } + if (k == j) { + if (level) { + if (live) strategy_exit("AX", "A", ax[c], kNaN); + if (units == 0.0 && closed == 1) strategy_entry("C", v_.is_long); + } else { + if (units == 0.0 && closed == 1) strategy_entry("A", v_.is_long); + if (live && open_trade_entry_id(0) == "A") + strategy_exit("AX", "A", ax[c], kNaN); + if (units == 0.0 && closed == 2) strategy_entry("C", v_.is_long); + } + } + if (k == j + 2 && units != 0.0) strategy_close_all(); + } + } + + Variant v_; + int bar_ = -1; + int closed_before_ = 0; + int closed_seen_ = 0; +}; + +template +std::vector feed(const FeedBar (&rows)[N]) { + std::vector bars; + for (const FeedBar& row : rows) { + Bar b{}; + b.timestamp = row.ts; + b.open = row.open; b.high = row.high; b.low = row.low; b.close = row.close; + b.volume = row.volume; + bars.push_back(b); + } + return bars; +} + +int chart_bar_of(std::int64_t ms) { + for (std::size_t i = 0; i < sizeof(kFord15) / sizeof(kFord15[0]); ++i) + if (kFord15[i].ts == ms) return static_cast(i); + return -1; +} + +// TradingView's exit signals: "Close entry(s) order X" for strategy.close("X"), +// "Close position order" for strategy.close_all. +std::string exit_signal(const ExtremeHost::Row& r) { + if (r.exit_id.rfind("__close__", 0) == 0 && r.exit_id.size() > 9) + return "Close entry(s) order " + r.exit_id.substr(9); + return r.exit_id.rfind("__", 0) == 0 ? "Close position order" : r.exit_id; +} + +bool near(double a, double b) { return std::fabs(a - b) <= 1e-9; } + +void replay(const Variant& v) { + std::printf("-- %s\n", v.slug); + static const std::vector fifteen = feed(kFord15); + ExtremeHost host(v); + host.run(fifteen.data(), static_cast(fifteen.size()), "15", "15", false); + CHECK(host.last_error().empty()); + const auto tape = read_tape(v.slug); + const auto rows = host.rows(); + CHECK(tape.size() >= v.compared); + CHECK(rows.size() >= v.compared); + int differing = 0; + for (std::size_t i = 0; i < v.compared && i < rows.size() && i < tape.size(); ++i) { + const TapeTrade& tv = tape[i]; + const ExtremeHost::Row& r = rows[i]; + const bool ok = r.is_long == tv.is_long && r.entry_ms == tv.entry_ms + && r.exit_ms == tv.exit_ms && near(r.entry_price, tv.entry_price) + && near(r.exit_price, tv.exit_price) && r.qty == tv.qty + && r.entry_id == tv.entry_signal && exit_signal(r) == tv.exit_signal + && r.entry_bar == chart_bar_of(tv.entry_ms) && r.exit_bar == chart_bar_of(tv.exit_ms); + const bool divergence = (v.recorded & (1u << (i + 1))) != 0; + CHECK(ok != divergence); + if (divergence) std::printf(" row %zu: RECORDED divergence\n", i + 1); + if (!ok) { + ++differing; + std::printf(" row %zu: TradingView %s %s bar %d @%.2f -> bar %d @%.2f %s\n" + " engine %s %s bar %d @%.2f -> bar %d @%.2f %s\n", + i + 1, tv.entry_signal.c_str(), tv.is_long ? "L" : "S", + chart_bar_of(tv.entry_ms), tv.entry_price, chart_bar_of(tv.exit_ms), + tv.exit_price, tv.exit_signal.c_str(), r.entry_id.c_str(), + r.is_long ? "L" : "S", r.entry_bar, r.entry_price, r.exit_bar, + r.exit_price, exit_signal(r).c_str()); + } + } + std::printf(" %zu rows compared, %d differ\n", v.compared, differing); +} + +unsigned rows(std::initializer_list list) { + unsigned mask = 0; + for (const int row : list) mask |= 1u << row; + return mask; +} + +} // namespace + +int main() { + const Variant variants[] = { + {"pa2-i2-exit-w2-long", true, true, false, 8, rows({5, 7})}, + {"pa2-i2-exit-w2-long-pooc", true, true, true, 8, rows({5, 7})}, + {"pa2-i2-exit-w2-short", false, true, false, 2, rows({1})}, + {"pa2-i2-exit-w2-short-pooc", false, true, true, 2, rows({1})}, + {"pa2-i2-level-w1-long", true, true, false, 6, rows({1, 2, 3, 4, 5, 6}), + Probe::LevelFirstExtreme}, + {"pa2-i2-level-w1-long-pooc", true, true, true, 6, rows({1, 2, 3, 4, 5, 6}), + Probe::LevelFirstExtreme}, + {"pa2-i2-level-w1-short", false, true, false, 6, rows({1, 2, 5, 6}), + Probe::LevelFirstExtreme}, + {"pa2-i2-level-w1-short-pooc", false, true, true, 2, rows({1, 2}), + Probe::LevelFirstExtreme}, + {"pa2-i2-point-w2-long", true, true, false, 6, rows({2, 3, 5, 6}), + Probe::PointSecondExtreme}, + {"pa2-i2-point-w2-long-pooc", true, true, true, 6, rows({2, 3, 5}), + Probe::PointSecondExtreme}, + {"pa2-i2-point-w2-short", false, true, false, 6, 0u, Probe::PointSecondExtreme}, + {"pa2-i2-point-w2-short-pooc", false, true, true, 6, 0u, Probe::PointSecondExtreme}, + }; + for (const Variant& v : variants) replay(v); + std::printf("\n%s second-extreme order tapes: %d checks, %d failures\n", + tests_failed == 0 ? "PASS" : "FAIL", tests_passed + tests_failed, tests_failed); + return tests_failed == 0 ? 0 : 1; +} From 0766a97d81864695b6ebfd9e9e6189e316816297 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:34:35 +0800 Subject: [PATCH 35/83] Adapter: a strategy.close a recalculation calls at the matcher's fill on a bar's second extreme fills there (R5 lane PAR-ORDERS-2, item 4) close() advanced a calc_on_order_fills close called at a fill booked AT the bar's second extreme to the next open (coof_close_next_open: Market pushed to pending_coof_requests_), or under process_orders_on_close to the bar's close. Four `lab tv` tapes of the lane's own script (lot A open, lot B's stop entry exactly on bar j's second extreme, B's recalculation calling strategy.close("A"); long and short, with and without process_orders_on_close) book the close at the extreme's tick on bar j, as the market entry of Finding 6d: the matcher's fill ends the leg to the extreme. A fill the adapter forced onto the extreme keeps the old route (coof_fill_at_second_extreme), as entry() does. Rows: test_second_extreme_order_tapes gains pa2-i4-close-w2-{long,short} {,-pooc}, all exact (132 checks, 0 failures); the lane's base fails every one of their compared rows (8 / 8 / 2 / 2: the close at the next open, or at the bar's close and then B by the re-issued close). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit cd6811640138be56658c8ddb033bff020ba72209) INT26 pick note: the docs conflicts were anchor digits only, merged by carrying this branch's numbers through the lane's line moves. --- docs/adr/0001-kernel-adapter-boundary.md | 20 ++--- docs/design/native-feature-parity.md | 89 ++++++++++--------- docs/pages/native-engine.md | 2 +- src/source/pine_adapter.cpp | 27 ++++-- .../fixtures/second_extreme_orders/README.md | 8 +- .../pa2-i4-close-w2-long-pooc/meta.json | 16 ++++ .../pa2-i4-close-w2-long-pooc/metrics.json | 40 +++++++++ .../pa2-i4-close-w2-long-pooc/strategy.pine | 38 ++++++++ .../pa2-i4-close-w2-long-pooc/tv_trades.csv | 17 ++++ .../pa2-i4-close-w2-long/meta.json | 16 ++++ .../pa2-i4-close-w2-long/metrics.json | 40 +++++++++ .../pa2-i4-close-w2-long/strategy.pine | 38 ++++++++ .../pa2-i4-close-w2-long/tv_trades.csv | 17 ++++ .../pa2-i4-close-w2-short-pooc/meta.json | 16 ++++ .../pa2-i4-close-w2-short-pooc/metrics.json | 40 +++++++++ .../pa2-i4-close-w2-short-pooc/strategy.pine | 38 ++++++++ .../pa2-i4-close-w2-short-pooc/tv_trades.csv | 15 ++++ .../pa2-i4-close-w2-short/meta.json | 16 ++++ .../pa2-i4-close-w2-short/metrics.json | 40 +++++++++ .../pa2-i4-close-w2-short/strategy.pine | 38 ++++++++ .../pa2-i4-close-w2-short/tv_trades.csv | 15 ++++ tests/test_second_extreme_order_tapes.cpp | 17 ++-- 22 files changed, 533 insertions(+), 70 deletions(-) create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/tv_trades.csv create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/meta.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/metrics.json create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/strategy.pine create mode 100644 tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/tv_trades.csv diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index fa1a2539..cdc751a0 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -209,8 +209,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:677`) for TV sizing and fill spelling; `validate_execution_precommit` (`pine_strategy_host.cpp:702`), whose - `validate_precommit` (`pine_adapter.cpp:13111`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:13356`) to own the opening margin decision; `owns_lot_excursions() = true` + `validate_precommit` (`pine_adapter.cpp:13120`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:13365`) to own the opening margin decision; `owns_lot_excursions() = true` (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:787`), so the Pine host keeps its own per-lot MFE/MAE, `ab9714be`'s model (RULING A48). Measured against TradingView by R5 lane H-MEASURE (`tests/test_e19_excursion_tape.cpp`, `lab tv` tapes): where @@ -232,9 +232,9 @@ kernel and own these quirks, each at its site: (`select_exit_activation` src/compat/pine/exit_activation.cpp:42, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:10, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:46) 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:9656-9661`); the half-tick + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:9665-9670`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:333) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11748`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11757`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:454-460`, 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 @@ -249,8 +249,8 @@ kernel and own these quirks, each at its site: (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9). - **Margin.** The kernel owns the margin *mechanism* — the level solve, the check points, the kernel request, its re-pricing, the receipt — and the adapter answers its three policy hooks - with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14146`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14182`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14155`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:14191`) 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 @@ -261,7 +261,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:16914`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16923`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -282,7 +282,7 @@ 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:8347`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:8356`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:680`), and the two fills Pine refuses to recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5645`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and @@ -643,8 +643,8 @@ 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:342`), `source_level_on_price_grid` (`:328`), `nearest_tick` (`:266`) / `source_bar_fill_tick` (`:287`) / `directional_tick` (`:311`), 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`, `tests/test_native_price_grid.cpp` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13944`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14999`), `chart_day_key` (`pine_adapter.cpp:13841`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13303-13304`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13953`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:15008`), `chart_day_key` (`pine_adapter.cpp:13850`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13312-13313`): 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:2361-2363`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2388-2396`, `margin.maintenance_long` `pine_adapter.cpp:2403`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index c1b3b446..b03bb5bd 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,19 +47,19 @@ 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:2738-2749; `entry()` pine_adapter.cpp:5290, `order()` pine_adapter.cpp:10545) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | 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:8408 → `Flatten{}` pine_adapter.cpp:8437-8438; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2738-2749; `entry()` pine_adapter.cpp:5290, `order()` pine_adapter.cpp:10554) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | 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:8417 → `Flatten{}` pine_adapter.cpp:8446-8447; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2708-3915 → `host.replace` pine_adapter.cpp:2724-2804) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4723-4769, `cancel_all` pine_adapter.cpp:18142-18157; `live_by_source_key_` pine_adapter.hpp:1996; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4723-4769, `cancel_all` pine_adapter.cpp:18151-18166; `live_by_source_key_` pine_adapter.hpp:1996; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5514-5549, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7538-7555) | **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:1270, `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:2747-2753) | **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:1092-1093 | 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:13081; lowering `submit_one` pine_adapter.cpp:9105-9117, `BindCohort` pine_adapter.cpp:9606-9612, `submit_leg` pine_adapter.cpp:9630-9654; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8710-8713; `materialize_relative_exits` pine_adapter.cpp:17432-17473, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19193-19222) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1998-2016); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10945-10969), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15708, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15942) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:684-686 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:13090; lowering `submit_one` pine_adapter.cpp:9114-9126, `BindCohort` pine_adapter.cpp:9615-9621, `submit_leg` pine_adapter.cpp:9639-9663; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8719-8722; `materialize_relative_exits` pine_adapter.cpp:17441-17482, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19202-19231) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1998-2016); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10954-10978), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15717, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15951) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:684-686 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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,25 +68,25 @@ 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:5692-5714; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1080 | 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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11856-11868) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11769-11906) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11865-11877) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11778-11915) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4022-4068) | **partial** — the read exists `trail_state` native_host.hpp:1080; `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:287-289) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9656-9661, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11769-11906, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8680-8906, `trail_one_shot` pine_adapter.cpp:9745-9874 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9665-9670, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11778-11915, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8689-8915, `trail_one_shot` pine_adapter.cpp:9754-9883 | 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:5925-5929, pine_adapter.cpp:5925) | **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:10980-12184) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4922-4956 | 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. | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:10989-12193) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4922-4956 | 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:2596-2651) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4957-4959 **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: rounded equity `source_money_round` pine_adapter.cpp:2548, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2557-2559) | **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:2609, `typed_entry_units` pine_adapter.cpp:2627; `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:2588; `percent_commission_live_equity` pine_adapter.cpp:4329-4369) | **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. | | SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2514-2529, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6710-6736) | **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:591, 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:4973-4976; 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:12555-12583, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:9032-9342) | **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. | +| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:12564-12592, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:9041-9351) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4444-4459) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6771), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7978), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12812), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13408, pine_adapter.cpp:13461) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15756) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18788) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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). | +| 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:6771), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7978), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12821), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13417, pine_adapter.cpp:13470) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15765) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18797) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7240-7591; ledger `close_batch_bar_` pine_adapter.hpp:2039-2050; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | @@ -95,21 +95,21 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2362) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13303-13306) is TV. | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13312-13315) is TV. | | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6230-6279, cap left unset `max_open_lots` pine_adapter.cpp:2361-2363) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13357-13422 and leaves the kernel gate unset pine_adapter.cpp:13356-13358) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:14012-14013, `margin_pct` pine_adapter.cpp:13420) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14112-14290, `submit_margin_call_units` pine_adapter.cpp:14251-14297, `schedule_margin_call_path` pine_adapter.cpp:18295-18372) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14043-14079) | **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:983 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. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13366-13431 and leaves the kernel gate unset pine_adapter.cpp:13365-13367) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:14021-14022, `margin_pct` pine_adapter.cpp:13429) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14121-14299, `submit_margin_call_units` pine_adapter.cpp:14260-14306, `schedule_margin_call_path` pine_adapter.cpp:18304-18381) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14052-14088) | **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:983 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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1308 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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16360-16376, `apply_fx_opening_margin_slice` pine_adapter.cpp:18373-18401) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16884-16923) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13392-13395) | **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:13973-13987, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14999-15029) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15042-15276) | **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:13783-13862) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:14001, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14060, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14768-14808, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14292-14419, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14292) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, the restore's `+1e-6` floor and the frozen signal-time units (M10); `margin_check_allowed` never admits the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low — a recorded divergence, open (M7). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15280-15338) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16369-16385, `apply_fx_opening_margin_slice` pine_adapter.cpp:18382-18410) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16893-16932) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13401-13404) | **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:13982-13996, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:15008-15038) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15051-15285) | **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:13792-13871) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:14010, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14069, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14777-14817, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14301-14428, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14301) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, the restore's `+1e-6` floor and the frozen signal-time units (M10); `margin_check_allowed` never admits the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low — a recorded divergence, open (M7). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15289-15347) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -117,10 +117,10 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:881, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7523-7527 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16195) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7065-7069; `execute_current` legal native_host.hpp:1092 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16204) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7065-7069; `execute_current` legal native_host.hpp:1092 | 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:7008 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7059 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1061 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:863, `NativeInputContext` native_host.hpp:761-766 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5611, `flush_coof_tail` pine_adapter.cpp:16338, `end_coof_recalc` pine_adapter.cpp:5634) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6919, native_execution_consumer.cpp:6919-6949; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5611, `flush_coof_tail` pine_adapter.cpp:16347, `end_coof_recalc` pine_adapter.cpp:5634) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6919, native_execution_consumer.cpp:6919-6949; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:866 fires per accepted realtime print native_execution_consumer.cpp:7147-7151; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | | 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:7517-7634 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | @@ -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:5685-5732, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6268-6276; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12023-12032) is A | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12032-12041) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:472-479) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:12039-12048, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:12048-12057, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:1190 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:8705). **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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9097-9102, `exit_limit_trigger` pine_adapter.cpp:9631-9652), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11748) | 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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9106-9111, `exit_limit_trigger` pine_adapter.cpp:9640-9661), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11757) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19193-19222 | 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:19202-19231 | 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:523, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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:603. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1273 | 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) | @@ -522,11 +522,11 @@ line numbers are this tree's (683a82f); kernel semantics are | 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:2362); 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:13303-13304; 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:16884, `update_risk_state` pine_adapter.cpp:13944-13971): 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:13296), 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_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:13312-13313; 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:16893, `update_risk_state` pine_adapter.cpp:13953-13980): 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:13305), 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:1200-1208): +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:17724-17733); 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:16354); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16466), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15029-15038, `submit_intraday_loss_close` pine_adapter.cpp:14999-15027), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17241); 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:5984, `:7685`, `:8409`, `:8537`, `:11526`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18140-18143) | `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:13935-13937); 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:13841-13862, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16363); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16475), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15038-15047, `submit_intraday_loss_close` pine_adapter.cpp:15008-15036), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17250); 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:5984, `:7685`, `:8418`, `:8546`, `:11535`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18149-18152) | `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:13944-13946); 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:13850-13871, `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, @@ -963,7 +963,10 @@ this lane re-pins the affected v19 witnesses once. there (`coof_fill_forced_`) -- starts the next leg instead, and its recalculation's market orders still advance, as TradingView books them too (the `pa2-i2-point-w2-*` tapes; corpus `bracket-rivet-calc-on-fill-01`, - trades 350 and 516, byte-identical). + trades 350 and 516, byte-identical). A `strategy.close` the same + recalculation calls fills at that extreme too, as TradingView books it + (`pa2-i4-close-w2-*`), where it used to wait for the next open, or under + `process_orders_on_close` for the bar's close. - The per-bar broker hash of an aggregated run moves because the kernel now folds its script coordinate and named input coordinate. Those v19 witnesses are re-pinned once in `tests/test_native_point_checks.cpp`; no pre-release @@ -1043,10 +1046,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:8821). Full-table scans + (`has_full_entry_bracket` pine_adapter.cpp:8830). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1696, `revive_brackets_after_margin` pine_adapter.cpp:1918, - `preserved_by_close_all` pine_adapter.cpp:11557), and + `preserved_by_close_all` pine_adapter.cpp:11566), 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. @@ -1290,7 +1293,7 @@ unchanged after it. | 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5780) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290); 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:16195-16224) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:16204-16233) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | @@ -1335,7 +1338,7 @@ unchanged after it. | **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:16360-16376) 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:2646) 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:16369-16385) 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:2646) 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. | @@ -1411,7 +1414,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:13214-13235). 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:2575), 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:13223-13244). 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:2575), 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:4654-4667 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). @@ -1521,7 +1524,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:1317) (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:373), 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:13767-13865); 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:13776-13874); 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:4654-4667 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index af947868..61ce8a79 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1458,7 +1458,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:14146-14170`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:14155-14179`), 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/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 8e48c9b6..6fdb1599 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -8164,22 +8164,31 @@ void PineExecutionAdapter::close(const SourceId& id, const std::string& comment, request.label = "__close__" + id; request.comment = comment; double coof_close_fill = kNaN; + // A matcher fill AT the bar's second extreme: the close fills there, at + // that extreme, as the market entry does (H-MEASURE Finding 6d). ab9714be + // advanced it to the close point, which admits no cascade order, and so to + // the next open; TradingView books it on the fill's own bar at the extreme + // (lab tv pa2-i4-close-w2-long / -pooc). A fill the adapter forced there + // still advances (coof_fill_at_second_extreme; R5 lane PAR-ORDERS-2). bool coof_close_next_open = false; + bool coof_close_at_second_extreme = false; if (coof_recalc_active_ && !coof_first_open_ && !immediately) { const auto state = detail::run_state(require_host()); const bool lower_path = state.spec && state.spec->intrabar.lower(); - if (!lower_path) { - const bool high_first = source_path_uses_high_first(coof_script_bar_); - const NativePathPhase second = high_first - ? NativePathPhase::Low : NativePathPhase::High; - const double endpoint = high_first - ? coof_script_bar_.low : coof_script_bar_.high; - coof_close_next_open = coof_context_.coordinate.path_phase == second - && coof_fill_at_path_point(endpoint); + if (!lower_path && coof_fill_at_second_extreme()) { + coof_close_next_open = coof_fill_forced_; + coof_close_at_second_extreme = !coof_fill_forced_; + if (coof_close_at_second_extreme) { + const double endpoint = source_path_uses_high_first(coof_script_bar_) + ? coof_script_bar_.low : coof_script_bar_.high; + const bool buy = current < 0.0; + coof_close_fill = source_bar_fill_tick(endpoint, staged_.syminfo.mintick) + + (buy ? 1.0 : -1.0) * config_.slippage * staged_.syminfo.mintick; + } } } if (coof_recalc_active_ && !coof_first_open_ && !immediately - && !coof_close_next_open + && !coof_close_next_open && !coof_close_at_second_extreme && coof_script_bar_valid_) { int next_waypoint_index = -1; const double next_waypoint = coof_next_waypoint(&next_waypoint_index); diff --git a/tests/fixtures/second_extreme_orders/README.md b/tests/fixtures/second_extreme_orders/README.md index cd9b5225..4be401ea 100644 --- a/tests/fixtures/second_extreme_orders/README.md +++ b/tests/fixtures/second_extreme_orders/README.md @@ -1,11 +1,11 @@ -# Second-extreme order tapes (R5 lane PAR-ORDERS-2, item 2) +# Second-extreme order tapes (R5 lane PAR-ORDERS-2, items 2 and 4) TradingView's own trades for orders a `calc_on_order_fills` recalculation places exactly on a bar's SECOND extreme (H-MEASURE Finding 6d): lot A is open; lot B is a stop entry placed exactly on bar j's second extreme (the high of a low-first bar, the low of a high-first one), so its fill starts the recalculation there with A still open (pyramiding 2), and that recalculation places an exit for A reached on the -extreme -> close leg. Long cycles j = 7, 19, 26, 42; +extreme -> close leg, or calls `strategy.close("A")`. Long cycles j = 7, 19, 26, 42; short j = 5, 30, 46, 51. `tests/test_second_extreme_order_tapes.cpp` replays each tape through the Pine adapter on the 15m chart (the short tapes on their first cycle: TradingView fills B's off-grid 11.795 sell stop on bar 38, not on bar 30). @@ -30,6 +30,10 @@ scripts count chart bars from 2025-07-02 09:30 ET, the first bar of | `pa2-i2-exit-w2-long-pooc` | the same, process_orders_on_close | 8 | `1b21b85bc50a052cee2749d2745c5d67cb434c9c8eb534aa1b22648bd00abc03` | `tv-tape-pa2-i2-exit-w2-long-pooc-1b21b85b` | | `pa2-i2-exit-w2-short` | exit AX of lot A, short, stop between low and close | 7 | `6864d39da5adfd779657801af3be47c7940783b14c4df0bc8af3e58f4624dd39` | `tv-tape-pa2-i2-exit-w2-short-6864d39d` | | `pa2-i2-exit-w2-short-pooc` | the same, process_orders_on_close | 7 | `05f4980868362562d407a5bff8e203c87edbfe6c519fe8fdf6a4e7d646fe8fec` | `tv-tape-pa2-i2-exit-w2-short-pooc-05f49808` | +| `pa2-i4-close-w2-long` | strategy.close("A"), long | 8 | `738a5955eb9e2b4bf930b0f8e3e70a952ecebdad38900b4a5a48d43149663ee3` | `tv-tape-pa2-i4-close-w2-long-738a5955` | +| `pa2-i4-close-w2-long-pooc` | the same, process_orders_on_close | 8 | `0130ee9f496a06c6afc48d8a96634be2697158deee75b8e2db42eb1eb1a788eb` | `tv-tape-pa2-i4-close-w2-long-pooc-0130ee9f` | +| `pa2-i4-close-w2-short` | strategy.close("A"), short | 7 | `777b55c471124e7a9747058be9f362e8a3bba618c93fced1358ecdd8846b3bc6` | `tv-tape-pa2-i4-close-w2-short-777b55c4` | +| `pa2-i4-close-w2-short-pooc` | the same, process_orders_on_close | 7 | `d88c061b3786fa2ef655082c7d9893900f5419aa871586dda0041b17cddb2eac` | `tv-tape-pa2-i4-close-w2-short-pooc-d88c061b` | | `pa2-i2-point-w2-long` | point fill on the second extreme, long | 6 | `88db2d9cd2a7f2dd4d7692007935c6e165b8476ed418fc9f293f6f691c8728ea` | `tv-tape-pa2-i2-point-w2-long-88db2d9c` | | `pa2-i2-point-w2-long-pooc` | the same, process_orders_on_close | 6 | `1fce1bed23df68231ed7546fe4fe13760116654e945a4cf1cdb02a9d755d6ca5` | `tv-tape-pa2-i2-point-w2-long-pooc-1fce1bed` | | `pa2-i2-point-w2-short` | point fill on the second extreme, short | 6 | `e292e70d771691a3fd44670abc9a699270d5ad677b3d480ff7402a0421fa7abd` | `tv-tape-pa2-i2-point-w2-short-e292e70d` | diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/meta.json b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/meta.json new file mode 100644 index 00000000..d897aba7 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I4 close-w2 long-pooc", + "slug": "pa2-i4-close-w2-long-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:56:44.108Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "93caf31a295736fcb7326008be0983c9db6a9a9ad13eec80edfeb35e5d1f08ab", + "tv_trades_sha256": "0130ee9f496a06c6afc48d8a96634be2697158deee75b8e2db42eb1eb1a788eb", + "tv_trades_rows": 16 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/metrics.json new file mode 100644 index 00000000..9bded0f0 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i4-close-w2-long-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 8, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "93caf31a295736fcb7326008be0983c9db6a9a9ad13eec80edfeb35e5d1f08ab", + "tvTradesCsvHash": "0130ee9f496a06c6afc48d8a96634be2697158deee75b8e2db42eb1eb1a788eb", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75563", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "52d6caaca4ee0a23597c358e5072a0e4171ea42d30769bcd8c61cf50ad0922b1", + "decodedReportSha256": "6cb6229d82962efc1afdd25b6cdae7b6e2be971d1b839b39d7c93a1cfd515517", + "closedTrades": 8, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:56:44.107Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/strategy.pine new file mode 100644 index 00000000..7bb1ec16 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/strategy.pine @@ -0,0 +1,38 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 4: strategy.close in the shape of H-MEASURE Finding 6d. +// Lot A is open; lot B is a stop entry placed exactly on bar j's SECOND extreme +// (a buy stop at the high of a low-first bar), +// so the calc_on_order_fills recalculation B's fill starts runs on that point with +// A still open, and calls strategy.close("A"). pyramiding=2; everything is closed +// on bar j+1. Cycles j = 7, 19, 26, 42 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I4 close-w2 long-pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=2, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +bStop = k == 6 ? 11.665 : k == 18 ? 11.715 : k == 25 ? 11.96 : k == 41 ? 11.785 : na +if k >= 0 and time < tEnd + if (k == 5 or k == 17 or k == 24 or k == 40) and units == 0 + strategy.entry("A", strategy.long) + if not na(bStop) + strategy.entry("B", strategy.long, stop=bStop) + if (k == 8 or k == 20 or k == 27 or k == 43) and units != 0 + strategy.close_all() + if math.abs(units) > 150 + strategy.close("A") +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/tv_trades.csv new file mode 100644 index 00000000..edbc5f67 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long-pooc/tv_trades.csv @@ -0,0 +1,17 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:15,Close entry(s) order A,11.67,100,1151,15.7682,1.37,0.2318,15.8849,1.38,-1.1151,-0.10,15.6515,0.02,2 +1,Entry long,2025-07-02 22:45,A,11.51,100,1151,15.7682,1.37,0.2318,15.8849,1.38,-1.1151,-0.10,15.6515,0.02,2 +2,Exit long,2025-07-02 23:30,Close position order,11.66,100,1167,-1.2333,-0.11,0.2333,2.8833,0.25,-2.1167,-0.18,14.5349,0.01,1 +2,Entry long,2025-07-02 23:15,B,11.67,100,1167,-1.2333,-0.11,0.2333,2.8833,0.25,-2.1167,-0.18,14.5349,0.01,1 +3,Exit long,2025-07-03 02:15,Close entry(s) order A,11.72,100,1166,5.7662,0.49,0.2338,5.8834,0.50,-2.1166,-0.18,20.1839,0.02,2 +3,Entry long,2025-07-03 01:45,A,11.66,100,1166,5.7662,0.49,0.2338,5.8834,0.50,-2.1166,-0.18,20.1839,0.02,2 +4,Exit long,2025-07-03 02:30,Close position order,11.73,100,1172,0.7655,0.07,0.2345,0.8828,0.08,-1.1172,-0.10,21.0666,0.02,1 +4,Entry long,2025-07-03 02:15,B,11.72,100,1172,0.7655,0.07,0.2345,0.8828,0.08,-1.1172,-0.10,21.0666,0.02,1 +5,Exit long,2025-07-03 21:30,Close entry(s) order A,11.96,100,1175,20.7629,1.77,0.2371,20.8825,1.78,-2.1175,-0.18,41.7099,0.04,2 +5,Entry long,2025-07-03 03:30,A,11.75,100,1175,20.7629,1.77,0.2371,20.8825,1.78,-2.1175,-0.18,41.7099,0.04,2 +6,Exit long,2025-07-03 21:45,Close position order,11.85,100,1196,-11.2381,-0.94,0.2381,0,0.00,-12.1196,-1.01,30.5914,0.03,1 +6,Entry long,2025-07-03 21:30,B,11.96,100,1196,-11.2381,-0.94,0.2381,0,0.00,-12.1196,-1.01,30.5914,0.03,1 +7,Exit long,2025-07-07 22:00,Close entry(s) order A,11.79,100,1168,10.7653,0.92,0.2347,10.8832,0.93,-3.1168,-0.27,41.2388,0.04,2 +7,Entry long,2025-07-07 21:30,A,11.68,100,1168,10.7653,0.92,0.2347,10.8832,0.93,-3.1168,-0.27,41.2388,0.04,2 +8,Exit long,2025-07-07 22:15,Close position order,11.7,100,1179,-9.2349,-0.78,0.2349,0,0.00,-9.1179,-0.77,32.1218,0.03,1 +8,Entry long,2025-07-07 22:00,B,11.79,100,1179,-9.2349,-0.78,0.2349,0,0.00,-9.1179,-0.77,32.1218,0.03,1 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/meta.json b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/meta.json new file mode 100644 index 00000000..71e12136 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I4 close-w2 long", + "slug": "pa2-i4-close-w2-long", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:56:40.099Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "5b7f9a8de0cd43fc4182b9ee164f4329db7ea823a1466f8e836eaa215100319b", + "tv_trades_sha256": "738a5955eb9e2b4bf930b0f8e3e70a952ecebdad38900b4a5a48d43149663ee3", + "tv_trades_rows": 16 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/metrics.json new file mode 100644 index 00000000..9614f633 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i4-close-w2-long", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 8, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "5b7f9a8de0cd43fc4182b9ee164f4329db7ea823a1466f8e836eaa215100319b", + "tvTradesCsvHash": "738a5955eb9e2b4bf930b0f8e3e70a952ecebdad38900b4a5a48d43149663ee3", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75559", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "dcfe1ea38a36fceaac0eb1d728f09585a4d066db563ab608f7b7e41ef08528d0", + "decodedReportSha256": "4b996fdddf9ea3d113c2bde1db4612931630099e9c9134ab8f7991a42eaaf5e0", + "closedTrades": 8, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:56:40.099Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/strategy.pine new file mode 100644 index 00000000..29dca3eb --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/strategy.pine @@ -0,0 +1,38 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 4: strategy.close in the shape of H-MEASURE Finding 6d. +// Lot A is open; lot B is a stop entry placed exactly on bar j's SECOND extreme +// (a buy stop at the high of a low-first bar), +// so the calc_on_order_fills recalculation B's fill starts runs on that point with +// A still open, and calls strategy.close("A"). pyramiding=2; everything is closed +// on bar j+1. Cycles j = 7, 19, 26, 42 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I4 close-w2 long", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=2, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +bStop = k == 6 ? 11.665 : k == 18 ? 11.715 : k == 25 ? 11.96 : k == 41 ? 11.785 : na +if k >= 0 and time < tEnd + if (k == 5 or k == 17 or k == 24 or k == 40) and units == 0 + strategy.entry("A", strategy.long) + if not na(bStop) + strategy.entry("B", strategy.long, stop=bStop) + if (k == 8 or k == 20 or k == 27 or k == 43) and units != 0 + strategy.close_all() + if math.abs(units) > 150 + strategy.close("A") +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/tv_trades.csv new file mode 100644 index 00000000..489c190e --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-long/tv_trades.csv @@ -0,0 +1,17 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:15,Close entry(s) order A,11.67,100,1150,16.7683,1.46,0.2317,16.885,1.47,-0.115,-0.01,16.6516,0.02,1 +1,Entry long,2025-07-02 23:00,A,11.5,100,1150,16.7683,1.46,0.2317,16.885,1.47,-0.115,-0.01,16.6516,0.02,1 +2,Exit long,2025-07-02 23:45,Close position order,11.66,100,1167,-1.2333,-0.11,0.2333,2.8833,0.25,-2.1167,-0.18,15.535,0.02,2 +2,Entry long,2025-07-02 23:15,B,11.67,100,1167,-1.2333,-0.11,0.2333,2.8833,0.25,-2.1167,-0.18,15.535,0.02,2 +3,Exit long,2025-07-03 02:15,Close entry(s) order A,11.72,100,1165,6.7663,0.58,0.2337,6.8835,0.59,-1.1165,-0.10,22.1841,0.02,1 +3,Entry long,2025-07-03 02:00,A,11.65,100,1165,6.7663,0.58,0.2337,6.8835,0.59,-1.1165,-0.10,22.1841,0.02,1 +4,Exit long,2025-07-03 02:45,Close position order,11.73,100,1172,0.7655,0.07,0.2345,0.8828,0.08,-1.1172,-0.10,23.0668,0.02,2 +4,Entry long,2025-07-03 02:15,B,11.72,100,1172,0.7655,0.07,0.2345,0.8828,0.08,-1.1172,-0.10,23.0668,0.02,2 +5,Exit long,2025-07-03 21:30,Close entry(s) order A,11.96,100,1175,20.7629,1.77,0.2371,20.8825,1.78,-2.1175,-0.18,43.7101,0.04,1 +5,Entry long,2025-07-03 03:45,A,11.75,100,1175,20.7629,1.77,0.2371,20.8825,1.78,-2.1175,-0.18,43.7101,0.04,1 +6,Exit long,2025-07-03 22:00,Close position order,11.85,100,1196,-11.2381,-0.94,0.2381,0,0.00,-12.1196,-1.01,32.5916,0.03,2 +6,Entry long,2025-07-03 21:30,B,11.96,100,1196,-11.2381,-0.94,0.2381,0,0.00,-12.1196,-1.01,32.5916,0.03,2 +7,Exit long,2025-07-07 22:00,Close entry(s) order A,11.79,100,1168,10.7653,0.92,0.2347,10.8832,0.93,-3.1168,-0.27,43.239,0.04,1 +7,Entry long,2025-07-07 21:45,A,11.68,100,1168,10.7653,0.92,0.2347,10.8832,0.93,-3.1168,-0.27,43.239,0.04,1 +8,Exit long,2025-07-07 22:30,Close position order,11.71,100,1179,-8.235,-0.70,0.235,0,0.00,-9.1179,-0.77,35.1219,0.04,2 +8,Entry long,2025-07-07 22:00,B,11.79,100,1179,-8.235,-0.70,0.235,0,0.00,-9.1179,-0.77,35.1219,0.04,2 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/meta.json b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/meta.json new file mode 100644 index 00000000..5fdbb2d1 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I4 close-w2 short-pooc", + "slug": "pa2-i4-close-w2-short-pooc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:56:52.545Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "79122576a1408b59ec175dd2b7154f84b907a3167074cab28dff54d5cbce401f", + "tv_trades_sha256": "d88c061b3786fa2ef655082c7d9893900f5419aa871586dda0041b17cddb2eac", + "tv_trades_rows": 14 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/metrics.json new file mode 100644 index 00000000..7e068c86 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i4-close-w2-short-pooc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 7, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "79122576a1408b59ec175dd2b7154f84b907a3167074cab28dff54d5cbce401f", + "tvTradesCsvHash": "d88c061b3786fa2ef655082c7d9893900f5419aa871586dda0041b17cddb2eac", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75573", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "e4d2753d268271c251a752adf32e621e3e1816cd6a9cceb806519e452f8adfdc", + "decodedReportSha256": "8ea5eb34ec585dcfae4bbf1a1d3549935db8875ed8fb98c0751f9dee29412d2c", + "closedTrades": 7, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:56:52.544Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/strategy.pine new file mode 100644 index 00000000..9b1541c8 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/strategy.pine @@ -0,0 +1,38 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 4: strategy.close in the shape of H-MEASURE Finding 6d. +// Lot A is open; lot B is a stop entry placed exactly on bar j's SECOND extreme +// (a sell stop at the low of a high-first bar), +// so the calc_on_order_fills recalculation B's fill starts runs on that point with +// A still open, and calls strategy.close("A"). pyramiding=2; everything is closed +// on bar j+1. Cycles j = 5, 30, 46, 51 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I4 close-w2 short-pooc", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=2, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +bStop = k == 4 ? 11.44 : k == 29 ? 11.795 : k == 45 ? 11.61 : k == 50 ? 11.49 : na +if k >= 0 and time < tEnd + if (k == 3 or k == 28 or k == 44 or k == 49) and units == 0 + strategy.entry("A", strategy.short) + if not na(bStop) + strategy.entry("B", strategy.short, stop=bStop) + if (k == 6 or k == 31 or k == 47 or k == 52) and units != 0 + strategy.close_all() + if math.abs(units) > 150 + strategy.close("A") +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/tv_trades.csv new file mode 100644 index 00000000..af29375e --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short-pooc/tv_trades.csv @@ -0,0 +1,15 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-02 22:45,Close entry(s) order A,11.44,100,1147,2.7709,0.24,0.2291,2.8853,0.25,-5.1147,-0.45,2.6565,0.00,2 +1,Entry short,2025-07-02 22:15,A,11.47,100,1147,2.7709,0.24,0.2291,2.8853,0.25,-5.1147,-0.45,2.6565,0.00,2 +2,Exit short,2025-07-02 23:00,Close position order,11.58,100,1144,-14.2302,-1.24,0.2302,0,0.00,-14.1144,-1.23,-11.4593,-0.01,1 +2,Entry short,2025-07-02 22:45,B,11.44,100,1144,-14.2302,-1.24,0.2302,0,0.00,-14.1144,-1.23,-11.4593,-0.01,1 +3,Exit short,2025-07-03 22:45,Close position order,11.86,100,1188,1.7626,0.15,0.2374,7.8812,0.66,-0.1188,-0.01,-9.6967,-0.01,3 +3,Entry short,2025-07-03 22:00,A,11.88,100,1188,1.7626,0.15,0.2374,7.8812,0.66,-0.1188,-0.01,-9.6967,-0.01,3 +4,Exit short,2025-07-07 23:15,Close position order,11.62,100,1179,16.7659,1.42,0.2341,17.8821,1.52,-5.1179,-0.43,6.9531,0.01,9 +4,Entry short,2025-07-04 00:30,B,11.79,100,1179,16.7659,1.42,0.2341,17.8821,1.52,-5.1179,-0.43,6.9531,0.01,9 +5,Exit short,2025-07-07 23:15,Close position order,11.62,100,1161,-1.2323,-0.11,0.2323,0,0.00,-2.1161,-0.18,5.8369,0.01,1 +5,Entry short,2025-07-07 23:00,B,11.61,100,1161,-1.2323,-0.11,0.2323,0,0.00,-2.1161,-0.18,5.8369,0.01,1 +6,Exit short,2025-07-08 00:15,Close entry(s) order A,11.49,100,1160,10.7691,0.93,0.2309,10.884,0.94,-1.116,-0.10,16.4911,0.02,2 +6,Entry short,2025-07-07 23:45,A,11.6,100,1160,10.7691,0.93,0.2309,10.884,0.94,-1.116,-0.10,16.4911,0.02,2 +7,Exit short,2025-07-08 00:30,Close position order,11.57,100,1149,-8.2306,-0.72,0.2306,0,0.00,-8.1149,-0.71,8.3754,0.01,1 +7,Entry short,2025-07-08 00:15,B,11.49,100,1149,-8.2306,-0.72,0.2306,0,0.00,-8.1149,-0.71,8.3754,0.01,1 \ No newline at end of file diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/meta.json b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/meta.json new file mode 100644 index 00000000..19844133 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I4 close-w2 short", + "slug": "pa2-i4-close-w2-short", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:56:48.085Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "871c54d093f7fde83665a0536cd76dd1e9684b4b35f870773cb8385133b1ea0c", + "tv_trades_sha256": "777b55c471124e7a9747058be9f362e8a3bba618c93fced1358ecdd8846b3bc6", + "tv_trades_rows": 14 +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/metrics.json b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/metrics.json new file mode 100644 index 00000000..40399a69 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i4-close-w2-short", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 7, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "871c54d093f7fde83665a0536cd76dd1e9684b4b35f870773cb8385133b1ea0c", + "tvTradesCsvHash": "777b55c471124e7a9747058be9f362e8a3bba618c93fced1358ecdd8846b3bc6", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75568", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "18f5abb27661825b6ccb923a45aa18b032a360985cc0f3f362c5de1410e72339", + "decodedReportSha256": "118265c984385ef89001962f293fc8009e052a5e319014da96201b44add8c9da", + "closedTrades": 7, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:56:48.084Z" +} diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/strategy.pine b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/strategy.pine new file mode 100644 index 00000000..b1460757 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/strategy.pine @@ -0,0 +1,38 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 4: strategy.close in the shape of H-MEASURE Finding 6d. +// Lot A is open; lot B is a stop entry placed exactly on bar j's SECOND extreme +// (a sell stop at the low of a high-first bar), +// so the calc_on_order_fills recalculation B's fill starts runs on that point with +// A still open, and calls strategy.close("A"). pyramiding=2; everything is closed +// on bar j+1. Cycles j = 5, 30, 46, 51 (levels read off those bars). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I4 close-w2 short", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=2, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=false, calc_on_order_fills=true, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +bStop = k == 4 ? 11.44 : k == 29 ? 11.795 : k == 45 ? 11.61 : k == 50 ? 11.49 : na +if k >= 0 and time < tEnd + if (k == 3 or k == 28 or k == 44 or k == 49) and units == 0 + strategy.entry("A", strategy.short) + if not na(bStop) + strategy.entry("B", strategy.short, stop=bStop) + if (k == 6 or k == 31 or k == 47 or k == 52) and units != 0 + strategy.close_all() + if math.abs(units) > 150 + strategy.close("A") +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/tv_trades.csv b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/tv_trades.csv new file mode 100644 index 00000000..eb5b5719 --- /dev/null +++ b/tests/fixtures/second_extreme_orders/pa2-i4-close-w2-short/tv_trades.csv @@ -0,0 +1,15 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-02 22:45,Close entry(s) order A,11.44,100,1147,2.7709,0.24,0.2291,2.8853,0.25,-5.1147,-0.45,2.6565,0.00,1 +1,Entry short,2025-07-02 22:30,A,11.47,100,1147,2.7709,0.24,0.2291,2.8853,0.25,-5.1147,-0.45,2.6565,0.00,1 +2,Exit short,2025-07-02 23:15,Close position order,11.58,100,1144,-14.2302,-1.24,0.2302,0,0.00,-14.1144,-1.23,-11.4593,-0.01,2 +2,Entry short,2025-07-02 22:45,B,11.44,100,1144,-14.2302,-1.24,0.2302,0,0.00,-14.1144,-1.23,-11.4593,-0.01,2 +3,Exit short,2025-07-03 23:00,Close position order,11.86,100,1187,0.7627,0.06,0.2373,6.8813,0.58,-0.1187,-0.01,-10.6966,-0.01,3 +3,Entry short,2025-07-03 22:15,A,11.87,100,1187,0.7627,0.06,0.2373,6.8813,0.58,-0.1187,-0.01,-10.6966,-0.01,3 +4,Exit short,2025-07-07 23:30,Close position order,11.62,100,1179,16.7659,1.42,0.2341,17.8821,1.52,-5.1179,-0.43,5.9532,0.01,10 +4,Entry short,2025-07-04 00:30,B,11.79,100,1179,16.7659,1.42,0.2341,17.8821,1.52,-5.1179,-0.43,5.9532,0.01,10 +5,Exit short,2025-07-07 23:30,Close position order,11.62,100,1161,-1.2323,-0.11,0.2323,0,0.00,-2.1161,-0.18,4.837,0.00,2 +5,Entry short,2025-07-07 23:00,B,11.61,100,1161,-1.2323,-0.11,0.2323,0,0.00,-2.1161,-0.18,4.837,0.00,2 +6,Exit short,2025-07-08 00:15,Close entry(s) order A,11.49,100,1160,10.7691,0.93,0.2309,10.884,0.94,-1.116,-0.10,15.4912,0.02,1 +6,Entry short,2025-07-08 00:00,A,11.6,100,1160,10.7691,0.93,0.2309,10.884,0.94,-1.116,-0.10,15.4912,0.02,1 +7,Exit short,2025-07-08 00:45,Close position order,11.57,100,1149,-8.2306,-0.72,0.2306,0,0.00,-8.1149,-0.71,7.3755,0.01,2 +7,Entry short,2025-07-08 00:15,B,11.49,100,1149,-8.2306,-0.72,0.2306,0,0.00,-8.1149,-0.71,7.3755,0.01,2 \ No newline at end of file diff --git a/tests/test_second_extreme_order_tapes.cpp b/tests/test_second_extreme_order_tapes.cpp index c3f07ed8..324bf687 100644 --- a/tests/test_second_extreme_order_tapes.cpp +++ b/tests/test_second_extreme_order_tapes.cpp @@ -1,5 +1,5 @@ /* - * test_second_extreme_order_tapes.cpp -- R5 lane PAR-ORDERS-2 (item 2). + * test_second_extreme_order_tapes.cpp -- R5 lane PAR-ORDERS-2 (items 2 and 4). * * Under calc_on_order_fills, the recalculation a fill starts may sit exactly * on the bar's SECOND extreme -- the end of the leg that approaches it, the @@ -8,7 +8,8 @@ * extreme, where ab9714be (and this engine until lane PAR-ORDERS-2) advanced * it to the close, which admits no cascade order, and so to the next open * (tests/test_magnified_aggregated_tape.cpp row 5). The same holds for the - * orders beside it: an exit the recalculation places is live from that point. + * orders beside it: an exit the recalculation places is live from that point, + * and a strategy.close fills there. * * The lane's own synthetic script, NYSE:F 15m over the three days of * tests/fixtures/session_islastbar (`lab tv`, ws-report-v1, rangeProof covered; @@ -18,13 +19,14 @@ * and that recalculation * pa2-i2-exit-w2-* places strategy.exit("AX", "A", stop, qty=100) with the * stop between the bar's close and the extreme: reached on - * the extreme -> close leg. + * the extreme -> close leg; + * pa2-i4-close-w2-* calls strategy.close("A"). * Long lots on low-first bars (cycles j = 7, 19, 26, 42), short lots on * high-first ones (j = 5, 30, 46, 51); with and without * process_orders_on_close. Everything is closed on bar j+1. * * TradingView, on every long cycle and the first short one: A's exit fills on - * bar j. The short + * bar j, and A's close fills at the extreme's booked tick on bar j. The short * tapes are compared on their first cycle only: on bar 30 TradingView never * fills B's sell stop at the off-grid 11.795 (the bar's low) and fills it on * bar 38 instead, while this engine fills it on bar 30 -- a separate stop-touch @@ -76,7 +78,8 @@ * * Fail-before, this TU against the lane's base (6945fc19): every row of the * long exit tapes (8 of 8; 7 of 8 with process_orders_on_close) exits A at the - * next open. + * next open, and every close tape row closes A at the next open (or, under + * process_orders_on_close, at the bar's close, then B by the re-issued close). */ #include @@ -418,6 +421,10 @@ int main() { {"pa2-i2-exit-w2-long-pooc", true, true, true, 8, rows({5, 7})}, {"pa2-i2-exit-w2-short", false, true, false, 2, rows({1})}, {"pa2-i2-exit-w2-short-pooc", false, true, true, 2, rows({1})}, + {"pa2-i4-close-w2-long", true, false, false, 8, 0u}, + {"pa2-i4-close-w2-long-pooc", true, false, true, 8, 0u}, + {"pa2-i4-close-w2-short", false, false, false, 2, 0u}, + {"pa2-i4-close-w2-short-pooc", false, false, true, 2, 0u}, {"pa2-i2-level-w1-long", true, true, false, 6, rows({1, 2, 3, 4, 5, 6}), Probe::LevelFirstExtreme}, {"pa2-i2-level-w1-long-pooc", true, true, true, 6, rows({1, 2, 3, 4, 5, 6}), From e4d3cd8d500013cb1ab559c8409e7a68d7c9b16a Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:36:50 +0800 Subject: [PATCH 36/83] Adapter: a process_orders_on_close stop entry placed while the book holds the other side fills at the close that reached it (R5 lane PAR-ORDERS-2, item 3) R5 lane PAR-ORDERS' close pass (flush_pooc_marketable_limit_entry_fills, H-MEASURE Finding 6b) fills a pure stop entry its placing close already reached -- but returned at once on any open book, so a stop entry placed against the held side (corpus probe 96's flips: strategy.close of the held id and a reached stop entry of the other side on one bar) waited for the next open, a bar late. Eight `lab tv` tapes of the lane's own synthetic script (long and short flips, a plain reversal, an unreached control; chart and magnified) book it at that close: after the same-bar close's fill when one is there, else as a reversal, sized as TradingView sizes it (the entry's quantity plus the side it was placed against). fill_pooc_close_entries now carries the pass: a flat-book row as before, and a reversing stop row (a pure stop entry against the book, not under calc_on_order_fills, no direction gate, no paired flat-market candidate) unless a same-bar Close/CloseAll row is still live -- then on_applied fills it right after that close's fill at the close tick. submit_or_replace recaptures the projection facts, so the placement's side and TradingView carry are restored before the current execution. Corpus: order-deferred-flip-pooc-cross-bar-01 moves toward its tape -- 57 of TradingView's 792 entries at its bar and price before (734 a bar late, one trade missing), all 792 trades exact after; its baseline line re-pinned (602febda -> cdf4e5cc). Rows: test_pooc_reversing_stop_tapes (new, 180 checks; 128 of 178 fail on the lane's previous commit). Release floor 682. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit e0b4d0b38c368574517b05c637bdec8ecb67814d) INT26 pick note: the docs conflicts were anchor digits only; the release floor is summed (697 -> 698) and its two citations name the assignment line. The probe-96 baseline line (order-deferred-flip-pooc-cross-bar-01, 602febda -> cdf4e5cc) is the lane's, accepted; H-THIN's excursions touch the same probe, so the line is re-derived once after every pick. --- CONTRIBUTING.md | 2 +- docs/adr/0001-kernel-adapter-boundary.md | 6 +- docs/design/native-feature-parity.md | 35 +- docs/pages/contributing-llm.md | 4 +- include/pineforge/source/pine_adapter.hpp | 4 + scripts/ci_verify.py | 4 +- scripts/corpus_parity_baseline.txt | 2 +- src/source/pine_adapter.cpp | 72 +++- tests/CMakeLists.txt | 5 +- tests/fixtures/pooc_reversing_stop/README.md | 32 ++ .../pa2-i3-flip-l-chart/meta.json | 16 + .../pa2-i3-flip-l-chart/metrics.json | 40 +++ .../pa2-i3-flip-l-chart/strategy.pine | 39 +++ .../pa2-i3-flip-l-chart/tv_trades.csv | 45 +++ .../pa2-i3-flip-l-mag/meta.json | 16 + .../pa2-i3-flip-l-mag/metrics.json | 40 +++ .../pa2-i3-flip-l-mag/strategy.pine | 39 +++ .../pa2-i3-flip-l-mag/tv_trades.csv | 45 +++ .../pa2-i3-flip-s-chart/meta.json | 16 + .../pa2-i3-flip-s-chart/metrics.json | 40 +++ .../pa2-i3-flip-s-chart/strategy.pine | 39 +++ .../pa2-i3-flip-s-chart/tv_trades.csv | 45 +++ .../pa2-i3-flip-s-mag/meta.json | 16 + .../pa2-i3-flip-s-mag/metrics.json | 40 +++ .../pa2-i3-flip-s-mag/strategy.pine | 39 +++ .../pa2-i3-flip-s-mag/tv_trades.csv | 45 +++ .../pa2-i3-flip-unreached-s-chart/meta.json | 16 + .../metrics.json | 40 +++ .../strategy.pine | 39 +++ .../tv_trades.csv | 23 ++ .../pa2-i3-flip-unreached-s-mag/meta.json | 16 + .../pa2-i3-flip-unreached-s-mag/metrics.json | 40 +++ .../pa2-i3-flip-unreached-s-mag/strategy.pine | 39 +++ .../pa2-i3-flip-unreached-s-mag/tv_trades.csv | 23 ++ .../pa2-i3-rev-s-chart/meta.json | 16 + .../pa2-i3-rev-s-chart/metrics.json | 40 +++ .../pa2-i3-rev-s-chart/strategy.pine | 35 ++ .../pa2-i3-rev-s-chart/tv_trades.csv | 45 +++ .../pa2-i3-rev-s-mag/meta.json | 16 + .../pa2-i3-rev-s-mag/metrics.json | 40 +++ .../pa2-i3-rev-s-mag/strategy.pine | 35 ++ .../pa2-i3-rev-s-mag/tv_trades.csv | 45 +++ tests/test_pooc_reversing_stop_tapes.cpp | 313 ++++++++++++++++++ 43 files changed, 1514 insertions(+), 33 deletions(-) create mode 100644 tests/fixtures/pooc_reversing_stop/README.md create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/meta.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/metrics.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/strategy.pine create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/tv_trades.csv create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/meta.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/metrics.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/strategy.pine create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/tv_trades.csv create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/meta.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/metrics.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/strategy.pine create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/tv_trades.csv create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/meta.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/metrics.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/strategy.pine create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/tv_trades.csv create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/meta.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/metrics.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/strategy.pine create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/tv_trades.csv create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/meta.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/metrics.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/strategy.pine create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/tv_trades.csv create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/meta.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/metrics.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/strategy.pine create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/tv_trades.csv create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/meta.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/metrics.json create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/strategy.pine create mode 100644 tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/tv_trades.csv create mode 100644 tests/test_pooc_reversing_stop_tapes.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b10a0944..c2991fb5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -153,7 +153,7 @@ What each gate refuses: `ci_verify.py` counts the CTest rows that actually **ran** and fails below a floor — `KERNEL_MIN_TESTS` ci_verify.py:255 and `RELEASE_MIN_TESTS` -ci_verify.py:391. +ci_verify.py:393. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index cdc751a0..eb555f24 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -245,7 +245,7 @@ kernel and own these quirks, each at its site: quantities leave no remnant row on a `lab tv` tape, which the Pine host books row for row while a bare host books four 2.8e-17-unit dust rows beside them. - **Close reservations.** `strategy.close` callsite batching, two-call provenance and the entry-id - ledger (`close_logical_units_` `pine_adapter.hpp:2024-2035`); the POOC reservation-growth population predicate + ledger (`close_logical_units_` `pine_adapter.hpp:2028-2039`); 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 @@ -261,7 +261,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:16923`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16963`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -337,7 +337,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:454-460`). `strategy.close` batching and the entry-id ledger - are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2024-2035`) and + are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2028-2039`) 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:424`); what enables the generic model is the presence of `NativeRunSpec::margin`. So is the string-sentinel decoding of adapter-written comments: diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index b03bb5bd..54824edd 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -50,16 +50,16 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2738-2749; `entry()` pine_adapter.cpp:5290, `order()` pine_adapter.cpp:10554) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | 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:8417 → `Flatten{}` pine_adapter.cpp:8446-8447; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2708-3915 → `host.replace` pine_adapter.cpp:2724-2804) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4723-4769, `cancel_all` pine_adapter.cpp:18151-18166; `live_by_source_key_` pine_adapter.hpp:1996; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4723-4769, `cancel_all` pine_adapter.cpp:18191-18206; `live_by_source_key_` pine_adapter.hpp:2000; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5514-5549, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7538-7555) | **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:1270, `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:2747-2753) | **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:1092-1093 | 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:13090; lowering `submit_one` pine_adapter.cpp:9114-9126, `BindCohort` pine_adapter.cpp:9615-9621, `submit_leg` pine_adapter.cpp:9639-9663; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8719-8722; `materialize_relative_exits` pine_adapter.cpp:17441-17482, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19202-19231) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:1998-2016); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10954-10978), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15717, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15951) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:684-686 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8719-8722; `materialize_relative_exits` pine_adapter.cpp:17481-17522, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19254-19283) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:2002-2020); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10954-10978), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15717, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15951) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:684-686 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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 @@ -86,8 +86,8 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:12564-12592, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:9041-9351) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4444-4459) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6771), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7978), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12821), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13417, pine_adapter.cpp:13470) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15765) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18797) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7240-7591; ledger `close_batch_bar_` pine_adapter.hpp:2039-2050; 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. | +| 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:6771), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7978), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12821), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13417, pine_adapter.cpp:13470) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15765) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18849) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7240-7591; ledger `close_batch_bar_` pine_adapter.hpp:2043-2054; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | ### 1.4 Margin, admission, liquidation, risk @@ -99,11 +99,11 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6230-6279, cap left unset `max_open_lots` pine_adapter.cpp:2361-2363) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | | MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13366-13431 and leaves the kernel gate unset pine_adapter.cpp:13365-13367) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:14021-14022, `margin_pct` pine_adapter.cpp:13429) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14121-14299, `submit_margin_call_units` pine_adapter.cpp:14260-14306, `schedule_margin_call_path` pine_adapter.cpp:18304-18381) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **Closed:** `maintenance_long` / `maintenance_short` with `NativeLiquidationCheck::{PathAdverseExtreme, PathAdverseExtremeMark, CalculationOnly}`; the kernel solves the level, rests its own `Reduce`/`Flatten` bound to the live book, re-prices it with `CancelReason::Superseded` and books a `MarginCallEvent` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | F:D2 O:D3 S:M3 | TV parts → MG15 | +| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:14121-14299, `submit_margin_call_units` pine_adapter.cpp:14260-14306, `schedule_margin_call_path` pine_adapter.cpp:18344-18421) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14052-14088) | **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:983 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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1308 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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16369-16385, `apply_fx_opening_margin_slice` pine_adapter.cpp:18382-18410) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16893-16932) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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 | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16369-16385, `apply_fx_opening_margin_slice` pine_adapter.cpp:18422-18450) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16933-16972) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13401-13404) | **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:13982-13996, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:15008-15038) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15051-15285) | **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 | @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19202-19231 | 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:19254-19283 | 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:523, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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:603. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1273 | 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) | @@ -324,7 +324,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:1730, `guard_native_mutation` engine_aux_security.cpp:78 | 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). No kernel TU includes a source header | `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13 | 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:739 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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549; the ledger `close_logical_units_` pine_adapter.hpp:2024-2035; `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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549; the ledger `close_logical_units_` pine_adapter.hpp:2028-2039; `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) | 58 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 58 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:13933f70987f6bfe37d3015ae065a8d5ee5937c37431d48e2ecd1f17a4c29958` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:862, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:590-709, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:515-567, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:494-511, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:802-833, c_abi.cpp:929 | 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 | @@ -523,9 +523,9 @@ line numbers are this tree's (683a82f); kernel semantics are |---|---|---|---|---|---| | `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2362); 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:13312-13313; 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:16893, `update_risk_state` pine_adapter.cpp:13953-13980): 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:13305), 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_drawdown` | `update_risk_state(bar.close)` once per script bar at the close mark (`update_risk_state(bar.close)` pine_adapter.cpp:16933, `update_risk_state` pine_adapter.cpp:13953-13980): 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:13305), 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:1200-1208): +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:17724-17733); 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:16363); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16475), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15038-15047, `submit_intraday_loss_close` pine_adapter.cpp:15008-15036), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17250); 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:5984, `:7685`, `:8418`, `:8546`, `:11535`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18149-18152) | `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_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16363); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16475), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15038-15047, `submit_intraday_loss_close` pine_adapter.cpp:15008-15036), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17290); 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:5984, `:7685`, `:8418`, `:8546`, `:11535`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18189-18192) | `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:13944-13946); 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:13850-13871, `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 @@ -937,7 +937,14 @@ this lane re-pins the affected v19 witnesses once. (`flush_pooc_marketable_limit_entry_fills`) now takes a pure stop entry that is the bar's lone entry marketable at the close; two such entries keep the next open's both-marketable arbitration, which no tape orders at a - close. That pass also never ran on an aggregated or magnified run: its + close. Since R5 lane PAR-ORDERS-2 the pass (`fill_pooc_close_entries`) + also takes such a stop entry placed while the book holds the other side: + TradingView fills it at that close, sized as the reversal it is, and after + the bar's own `strategy.close` of the held side when one fills there + (`tests/test_pooc_reversing_stop_tapes.cpp`, chart and magnified; corpus + `order-deferred-flip-pooc-cross-bar-01`: 57 of TradingView's 792 entries + at its bar and price before, 734 a bar late and one trade missing; all 792 + trades exact after). That pass also never ran on an aggregated or magnified run: its quiet-bar gate tested the script-bar `interval_index` where the pass tests the input slot (`projection_bar_index`), so since K-IDX every such run skipped it, limit entries and exit legs included. diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index d25a3995..7238f907 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -118,7 +118,7 @@ finding to report, not a step to take. 10. **A test row never silently disappears.** Each profile counts the rows that *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:255 and - `RELEASE_MIN_TESTS` ci_verify.py:391. Adding rows means raising the floor + `RELEASE_MIN_TESTS` ci_verify.py:393. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -264,7 +264,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:255, `RELEASE_MIN_TESTS` ci_verify.py:391). It +(`KERNEL_MIN_TESTS` ci_verify.py:255, `RELEASE_MIN_TESTS` ci_verify.py:393). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/include/pineforge/source/pine_adapter.hpp b/include/pineforge/source/pine_adapter.hpp index 78e48c2d..913ba727 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -1963,6 +1963,10 @@ class PineExecutionAdapter { void admit_deferred_open_marketable_sells(); void rearm_throttled_reopens(); void flush_pooc_marketable_limit_entry_fills(const Bar&, const NativeDecisionContext&); + // The close pass's fills at a close tick: its own pass, or (after_close) + // the reversing stops a same-bar close's fill releases. + void fill_pooc_close_entries(double raw_close, const NativeDecisionContext&, + bool after_close); void flush_pooc_marketable_exit_fills(const Bar&, const NativeDecisionContext&); void record_market_review(admission::Checkpoint, int, const std::vector&); diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index eeb7588c..a6462ad8 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -388,7 +388,9 @@ # (source-bound; INT26 pick, summed). # 697 = those 696 plus R5 lane PAR-ORDERS-2's test_second_extreme_order_tapes # (source-bound; INT26 pick, summed). -RELEASE_MIN_TESTS = 697 +# 698 = those 697 plus R5 lane PAR-ORDERS-2's test_pooc_reversing_stop_tapes +# (source-bound; INT26 pick, summed). +RELEASE_MIN_TESTS = 698 # R5 lane H-DOCGATES (AUDIT4-opus perf N-7) raises both floors by one more: # test_native_runtime_ambient_lifo, the runtime blocks' LIFO death row, is # source-free, so it registers and runs in the kernel profile (279 -> 280) and diff --git a/scripts/corpus_parity_baseline.txt b/scripts/corpus_parity_baseline.txt index fbea2634..6d45c519 100644 --- a/scripts/corpus_parity_baseline.txt +++ b/scripts/corpus_parity_baseline.txt @@ -154,7 +154,7 @@ b1c6e2f74a263c3ed0402b20cca655b329326c68b9e91405bf874d74be0e8fe0 validation/ord c9d1428ff858ea15fb8634b4634e4f438e0ff38b39e19f27e94d552ed98b821c validation/order-cross-exit-close-same-pass-01/engine_trades.csv 5804e69d5646d827f6c300b54d02c063c54e8071e999db3e4cbe9458b9c5413e validation/order-crosscurrent-opposite-market-pair-01/engine_trades.csv b71d4a6a1d3c177473f041e56504820f9c06c3ba4588fc8b8827c44f24503ed6 validation/order-deferred-flip-guaranteed-gap-stops-01/engine_trades.csv -602febda64b40ac5b089ddc13b8810ee6ac11ea4f82b86725789496bf8b3d9a2 validation/order-deferred-flip-pooc-cross-bar-01/engine_trades.csv +cdf4e5cc5c8d45d12da36bb8f0e34e0aa3164a0c9fb54850f89dfbff335ca61b validation/order-deferred-flip-pooc-cross-bar-01/engine_trades.csv 12c029df1fbe1b2b071e6ee2b9f43039d0359ae02bf2ad0ed3260bb3adadda1e validation/order-dual-four-bar-stop-no-close-01/engine_trades.csv 560e75eadf284e4374e77479fa13edf0629cfad1bfa5d76f1cfb5dbe619eb8c4 validation/order-dual-side-same-id-stop-no-cancel-01/engine_trades.csv a03be8fcb680efdaaf09d4e38f7c7dc9c2f5a74948feb9db6cc57c7c9188a5db validation/order-dual-stop-both-touch-priority-01/engine_trades.csv diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 6fdb1599..acf26c79 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -16564,10 +16564,34 @@ void PineExecutionAdapter::flush_pooc_marketable_limit_entry_fills( // a lone marketable entry: when a second entry of this bar is marketable at // the same close, which fills first is unmeasured, and the stops keep the // next open's both-marketable arbitration (lane L9b's P0-B shapes). + // A pure stop entry placed while the book held the other side fills at + // that close as well, sized as its row is: a plain reversal here, and one + // a same-bar strategy.close or close_all of the held side precedes right + // after that close fills (fill_pooc_close_entries from on_applied), as + // TradingView books both (R5 lane PAR-ORDERS-2: lab tv + // pa2-i3-rev-s-{chart,mag} and pa2-i3-flip-{s,l}-{chart,mag}, corpus probe + // 96, which filled it at the next open; pa2-i3-flip-unreached-* keep an + // unreached one resting). Only without calc_on_order_fills, the route the + // tapes cover. if (!config_.process_orders_on_close || stream_mode_ || coof_recalc_active_) return; - if (detail::run_position(require_host()).signed_units != 0.0) return; - const double raw_close = bar.close; + fill_pooc_close_entries(bar.close, context, /*after_close=*/false); +} + +void PineExecutionAdapter::fill_pooc_close_entries( + double raw_close, const NativeDecisionContext& context, bool after_close) { + const double book_before = detail::run_position(require_host()).signed_units; if (!finite_positive(raw_close)) return; + // A same-bar close of the held side fills at this close after this pass: + // the reversing stops it precedes wait for its fill. + const bool close_follows = !after_close && std::any_of( + live_handles_.begin(), live_handles_.end(), + [&](const native_order::RequestHandle& handle) { + const auto found = placement_.find(handle.incarnation); + return found != placement_.end() + && (found->second.family == PineOrderFamily::Close + || found->second.family == PineOrderFamily::CloseAll) + && found->second.projection_created_bar == projection_bar_index(context); + }); const double tick = staged_.syminfo.mintick; const double close_tick = source_bar_fill_tick(raw_close, tick); std::vector> marketable; @@ -16578,16 +16602,23 @@ void PineExecutionAdapter::flush_pooc_marketable_limit_entry_fills( if (row.family != PineOrderFamily::Entry || !row.opening) continue; if (row.birth.from_fill()) continue; if (row.projection_created_bar != projection_bar_index(context)) continue; - if (row.projection_position_side - != static_cast(PositionSide::FLAT)) continue; const bool pure_limit = !config_.calc_on_order_fills && finite_positive(row.exit_levels.limit) && !std::isfinite(row.exit_levels.stop); const bool pure_stop = finite_positive(row.exit_levels.stop) && !std::isfinite(row.exit_levels.limit); if (!pure_limit && !pure_stop) continue; - if (row.direction_gate || row.terms_priced_reverse - || row.paired_flat_market_candidate) continue; + const bool placed_flat = row.projection_position_side + == static_cast(PositionSide::FLAT); + const bool placed_against = row.projection_position_side + == static_cast(row.is_long ? PositionSide::SHORT + : PositionSide::LONG); + const bool reversing_stop = placed_against && pure_stop + && !config_.calc_on_order_fills && (after_close || !close_follows) + && (book_before == 0.0 || (book_before > 0.0) != row.is_long); + if (placed_flat ? (after_close || book_before != 0.0) : !reversing_stop) continue; + if (row.direction_gate || row.paired_flat_market_candidate) continue; + if (row.terms_priced_reverse && !reversing_stop) continue; const bool at_close = pure_limit ? (row.is_long ? raw_close <= row.exit_levels.limit : raw_close >= row.exit_levels.limit) @@ -16603,11 +16634,13 @@ void PineExecutionAdapter::flush_pooc_marketable_limit_entry_fills( }), marketable.end()); } for (auto& candidate : marketable) { - // An earlier fill of this pass leaves the book non-flat; the owner's - // later rows are then ordinary pending orders again. - if (detail::run_position(require_host()).signed_units != 0.0) break; + // An earlier fill of this pass moves the book; the owner's later rows + // are then ordinary pending orders again. + if (detail::run_position(require_host()).signed_units != book_before) break; const auto& row = candidate.second; - const bool host_sized = row.deferred_cohort + // A reversal's frozen transaction is resolved by the host, as at the + // next open (resolve_execution_terms reads the copied row). + const bool host_sized = row.deferred_cohort || row.terms_priced_reverse || row.qty_type == static_cast(QtyType::CASH) || row.qty_type == static_cast(QtyType::PERCENT_OF_EQUITY); if (!host_sized && !finite_positive(row.requested_qty)) continue; @@ -16636,6 +16669,13 @@ void PineExecutionAdapter::flush_pooc_marketable_limit_entry_fills( const auto accepted = submit_or_replace( std::move(request), std::move(immediate), true, row.source_id); if (accepted) { + // The replacement is the row's own order executed at this close: + // it keeps the side and carried units it was placed against, + // which its reversal sizing reads, when a same-bar close has + // flattened the book since. + auto& placed = placement_.at(accepted->incarnation); + placed.projection_position_side = row.projection_position_side; + placed.projection_tv_carry_qty = row.projection_tv_carry_qty; (void)require_host().execute_current( {*accepted, NativeCurrentPriceRule::NearestTick}); } @@ -18448,6 +18488,18 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& } } } + // A same-bar strategy.close or close_all that fills at a + // process_orders_on_close close is followed there by the reversing stop + // entries the close pass left for it (R5 lane PAR-ORDERS-2). + if (placement_snapshot && event.closed_units > 0.0 + && (placement_snapshot->family == PineOrderFamily::Close + || placement_snapshot->family == PineOrderFamily::CloseAll) + && placement_snapshot->projection_created_bar == projection_bar_index(context) + && config_.process_orders_on_close && !config_.calc_on_order_fills + && !stream_mode_ && !coof_recalc_active_ + && event.cursor.point.path_phase == NativePathPhase::Close) { + fill_pooc_close_entries(event.raw_price, context, /*after_close=*/true); + } } int PineExecutionAdapter::short_seed_collision_role_v1(native_order::RequestHandle handle) const noexcept { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a4505cd5..92dae3df 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1373,6 +1373,8 @@ list(APPEND TEST_SOURCES test_native_current_cohort_refusal) list(APPEND TEST_SOURCES test_flat_coof_exit_tapes) # An order a recalculation places on a bar's second extreme. list(APPEND TEST_SOURCES test_second_extreme_order_tapes) +# A process_orders_on_close stop entry placed while the book is still open. +list(APPEND TEST_SOURCES test_pooc_reversing_stop_tapes) # R4-D L4b-fix restores every removed ShortSeed, affordability, sizing and # deferred-close oracle as an individually compiled native-route twin. Keep @@ -2117,7 +2119,8 @@ foreach(_pf_hm_row IN ITEMS "test_adapter_margin_schedule_differential|PINEFORGE_HM_M7A_FIXTURE_DIR|margin_entry_bar" # R5 lane PAR-ORDERS-2's tape rows. "test_flat_coof_exit_tapes|PINEFORGE_PA2_FLAT_COOF_FIXTURE_DIR|flat_coof_exit" - "test_second_extreme_order_tapes|PINEFORGE_PA2_SECOND_EXTREME_FIXTURE_DIR|second_extreme_orders") + "test_second_extreme_order_tapes|PINEFORGE_PA2_SECOND_EXTREME_FIXTURE_DIR|second_extreme_orders" + "test_pooc_reversing_stop_tapes|PINEFORGE_PA2_REVERSING_STOP_FIXTURE_DIR|pooc_reversing_stop") string(REPLACE "|" ";" _pf_hm_fields "${_pf_hm_row}") list(GET _pf_hm_fields 0 _pf_hm_target) list(GET _pf_hm_fields 1 _pf_hm_macro) diff --git a/tests/fixtures/pooc_reversing_stop/README.md b/tests/fixtures/pooc_reversing_stop/README.md new file mode 100644 index 00000000..945c7d56 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/README.md @@ -0,0 +1,32 @@ +# process_orders_on_close reversing stop tapes (R5 lane PAR-ORDERS-2, item 3) + +TradingView's own trades for corpus probe 96's flip on bar counts, under +`process_orders_on_close`: every six bars a stop entry of the other side is placed +while the book is still open, with a same-bar `strategy.close` of the held id (or +without one: a plain reversal), the stop 5c on the close's marketable side (or 5c +beyond it, the control). TradingView fills the reached entry at the signal bar's +close, its quantity grown by the side it was placed against. +`tests/test_pooc_reversing_stop_tapes.cpp` replays each tape through the Pine +adapter on the 15m chart (`-chart`) or the 1m bars aggregated to 15m under the +magnifier (`-mag`). + +Each directory is one `lab tv` export (pineforge-workflow, channel `ws-report-v1`, +`rangeProof: covered`, NYSE:F 15, window 2025-07-01 .. 2025-07-08), byte-identical: +`strategy.pine`, `tv_trades.csv` (times UTC+8), `meta.json`, `metrics.json`. The +scripts count chart bars from 2025-07-02 09:30 ET, the first bar of +`tests/fixtures/session_islastbar/bars.inc`, whose 15m and 1m bars the rows replay. + +| probe | variant | trades | tvTradesSha256 | note | +|---|---|---|---|---| +| `pa2-i3-flip-s-chart` | flip with a same-bar strategy.close, first flip short, chart | 22 | `fece5dc70a3bcd9590bb28f768f5bb150c886836e049ec2a2ddd9d742250a4b0` | `tv-tape-pa2-i3-flip-s-chart-fece5dc7` | +| `pa2-i3-flip-s-mag` | the same, bar magnifier on | 22 | `e3ef67816e8b94589b18ec740441d479a10e069527f123914cafc69ba970cf8a` | `tv-tape-pa2-i3-flip-s-mag-e3ef6781` | +| `pa2-i3-flip-l-chart` | flip with a same-bar strategy.close, first flip long, chart | 22 | `213292ff0d373a4dffcc4936c2da142ee6df7508179d5fe21542ea2881f22690` | `tv-tape-pa2-i3-flip-l-chart-213292ff` | +| `pa2-i3-flip-l-mag` | the same, bar magnifier on | 22 | `33665e31b1e690ae885c2c51e5e19805659fac961fcf9f932d85623b83155c40` | `tv-tape-pa2-i3-flip-l-mag-33665e31` | +| `pa2-i3-rev-s-chart` | plain reversal, no same-bar close, chart | 22 | `e4d9c73535ca8fb6fb98bf23951822c3fcbe9a5742575182a1aa1894715669e5` | `tv-tape-pa2-i3-rev-s-chart-e4d9c735` | +| `pa2-i3-rev-s-mag` | the same, bar magnifier on | 22 | `fc405dd76617426a32815a8eade4ea07df3603ad2c5dab7572c1f9e0af36945c` | `tv-tape-pa2-i3-rev-s-mag-fc405dd7` | +| `pa2-i3-flip-unreached-s-chart` | flip, the stops 5c beyond the close (control), chart | 11 | `b2e8779bcf6bc769756b0b00cedcb182230c814985b6131b7f390b7ca70eb39e` | `tv-tape-pa2-i3-flip-unreached-s-chart-b2e8779b` | +| `pa2-i3-flip-unreached-s-mag` | the same, bar magnifier on | 11 | `b19172f54665a75a1d9e13c603759de43cb0b6cfa4624afaeb33c54cfa792cd3` | `tv-tape-pa2-i3-flip-unreached-s-mag-b19172f5` | + +The last row of each flip and reversal tape is TradingView's trade still open at +the end of the data (its exit signal empty); the row compares it with the book the +replay ends on. diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/meta.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/meta.json new file mode 100644 index 00000000..17747d3f --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I3 flip-l-chart", + "slug": "pa2-i3-flip-l-chart", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:55:50.531Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "331f80fe52feeaa0b875abf5f9cb0d4f671add2eb6f3015d380084448626d56e", + "tv_trades_sha256": "213292ff0d373a4dffcc4936c2da142ee6df7508179d5fe21542ea2881f22690", + "tv_trades_rows": 44 +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/metrics.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/metrics.json new file mode 100644 index 00000000..0577e88d --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i3-flip-l-chart", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 22, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "331f80fe52feeaa0b875abf5f9cb0d4f671add2eb6f3015d380084448626d56e", + "tvTradesCsvHash": "213292ff0d373a4dffcc4936c2da142ee6df7508179d5fe21542ea2881f22690", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75517", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "a857c79d52eec3339fac97e9f5c2a6203ec7956844fb5d1f888e18ad2445c959", + "decodedReportSha256": "ec28bd0bb25a0198084e28b54cfd10e097830a49666311f97d20111285d26dd8", + "closedTrades": 22, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:55:50.531Z" +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/strategy.pine b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/strategy.pine new file mode 100644 index 00000000..4e016e11 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/strategy.pine @@ -0,0 +1,39 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 3: corpus probe 96's flip on bar counts, under +// process_orders_on_close. Every six bars a stop entry of the other side is placed while the book is still open, and a same-bar strategy.close of the held id +// flattens it at that close. The stop sits 5c on the close's marketable side (the close has reached it). +// The first flip opens a long. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I3 flip-l-chart", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +isDown = k % 6 == 5 +isUp = k % 6 == 2 +if k >= 0 and time < tEnd + if isDown + strategy.entry("SE", strategy.short, stop=(t + 5) / 100) + strategy.cancel("LE") + if isUp + strategy.entry("LE", strategy.long, stop=(t - 5) / 100) + strategy.cancel("SE") + if isDown and units > 0 + strategy.close("LE") + if isUp and units < 0 + strategy.close("SE") +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/tv_trades.csv b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/tv_trades.csv new file mode 100644 index 00000000..37a21dd9 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-chart/tv_trades.csv @@ -0,0 +1,45 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:45,Close entry(s) order LE,11.51,100,1145,5.7704,0.50,0.2296,6.8855,0.60,-2.1145,-0.18,5.7704,0.01,3 +1,Entry long,2025-07-02 22:00,LE,11.45,100,1145,5.7704,0.50,0.2296,6.8855,0.60,-2.1145,-0.18,5.7704,0.01,3 +2,Exit short,2025-07-02 23:30,Close entry(s) order SE,11.66,200,2302,-30.4634,-1.32,0.4634,1.7698,0.08,-38.2302,-1.66,-24.693,-0.02,3 +2,Entry short,2025-07-02 22:45,SE,11.51,200,2302,-30.4634,-1.32,0.4634,1.7698,0.08,-38.2302,-1.66,-24.693,-0.02,3 +3,Exit long,2025-07-03 00:15,Close entry(s) order LE,11.66,300,3498,-0.6996,-0.02,0.6996,5.6502,0.16,-9.3498,-0.27,-25.3926,-0.03,3 +3,Entry long,2025-07-02 23:30,LE,11.66,300,3498,-0.6996,-0.02,0.6996,5.6502,0.16,-9.3498,-0.27,-25.3926,-0.03,3 +4,Exit short,2025-07-03 01:00,Close entry(s) order SE,11.66,400,4664,-0.9328,-0.02,0.9328,7.5336,0.16,-8.4664,-0.18,-26.3254,-0.03,3 +4,Entry short,2025-07-03 00:15,SE,11.66,400,4664,-0.9328,-0.02,0.9328,7.5336,0.16,-8.4664,-0.18,-26.3254,-0.03,3 +5,Exit long,2025-07-03 01:45,Close entry(s) order LE,11.66,500,5830,-1.166,-0.02,1.166,14.417,0.25,-10.583,-0.18,-27.4914,-0.03,3 +5,Entry long,2025-07-03 01:00,LE,11.66,500,5830,-1.166,-0.02,1.166,14.417,0.25,-10.583,-0.18,-27.4914,-0.03,3 +6,Exit short,2025-07-03 02:30,Close entry(s) order SE,11.73,600,6996,-43.4034,-0.62,1.4034,11.3004,0.16,-42.6996,-0.61,-70.8948,-0.07,3 +6,Entry short,2025-07-03 01:45,SE,11.66,600,6996,-43.4034,-0.62,1.4034,11.3004,0.16,-42.6996,-0.61,-70.8948,-0.07,3 +7,Exit long,2025-07-03 03:15,Close entry(s) order LE,11.75,700,8211,12.3564,0.15,1.6436,34.1789,0.42,-7.8211,-0.10,-58.5384,-0.06,3 +7,Entry long,2025-07-03 02:30,LE,11.73,700,8211,12.3564,0.15,1.6436,34.1789,0.42,-7.8211,-0.10,-58.5384,-0.06,3 +8,Exit short,2025-07-03 21:30,Close entry(s) order SE,11.94,800,9400,-153.8952,-1.64,1.8952,23.06,0.25,-168.94,-1.80,-212.4336,-0.21,3 +8,Entry short,2025-07-03 03:15,SE,11.75,800,9400,-153.8952,-1.64,1.8952,23.06,0.25,-168.94,-1.80,-212.4336,-0.21,3 +9,Exit long,2025-07-03 22:15,Close entry(s) order LE,11.84,900,10746,-92.1402,-0.86,2.1402,7.9254,0.07,-100.0746,-0.93,-304.5738,-0.30,3 +9,Entry long,2025-07-03 21:30,LE,11.94,900,10746,-92.1402,-0.86,2.1402,7.9254,0.07,-100.0746,-0.93,-304.5738,-0.30,3 +10,Exit short,2025-07-03 23:00,Close entry(s) order SE,11.82,1000,11840,17.634,0.15,2.366,38.816,0.33,-31.184,-0.26,-286.9398,-0.29,3 +10,Entry short,2025-07-03 22:15,SE,11.84,1000,11840,17.634,0.15,2.366,38.816,0.33,-31.184,-0.26,-286.9398,-0.29,3 +11,Exit long,2025-07-03 23:45,Close entry(s) order LE,11.84,1100,13002,19.3974,0.15,2.6026,64.6998,0.50,-12.3002,-0.09,-267.5424,-0.27,3 +11,Entry long,2025-07-03 23:00,LE,11.82,1100,13002,19.3974,0.15,2.6026,64.6998,0.50,-12.3002,-0.09,-267.5424,-0.27,3 +12,Exit short,2025-07-04 00:30,Close entry(s) order SE,11.81,1200,14208,33.162,0.23,2.838,58.5792,0.41,-13.4208,-0.09,-234.3804,-0.23,3 +12,Entry short,2025-07-03 23:45,SE,11.84,1200,14208,33.162,0.23,2.838,58.5792,0.41,-13.4208,-0.09,-234.3804,-0.23,3 +13,Exit long,2025-07-07 21:45,Close entry(s) order LE,11.72,1300,15353,-120.0589,-0.78,3.0589,37.4647,0.24,-261.5353,-1.70,-354.4393,-0.35,3 +13,Entry long,2025-07-04 00:30,LE,11.81,1300,15353,-120.0589,-0.78,3.0589,37.4647,0.24,-261.5353,-1.70,-354.4393,-0.35,3 +14,Exit short,2025-07-07 22:30,Close entry(s) order SE,11.7,1400,16408,24.7212,0.15,3.2788,68.3592,0.42,-99.6408,-0.61,-329.7181,-0.33,3 +14,Entry short,2025-07-07 21:45,SE,11.72,1400,16408,24.7212,0.15,3.2788,68.3592,0.42,-99.6408,-0.61,-329.7181,-0.33,3 +15,Exit long,2025-07-07 23:15,Close entry(s) order LE,11.62,1500,17550,-123.498,-0.70,3.498,13.245,0.08,-136.755,-0.78,-453.2161,-0.45,3 +15,Entry long,2025-07-07 22:30,LE,11.7,1500,17550,-123.498,-0.70,3.498,13.245,0.08,-136.755,-0.78,-453.2161,-0.45,3 +16,Exit short,2025-07-08 00:00,Close entry(s) order SE,11.57,1600,18592,76.2896,0.41,3.7104,94.1408,0.51,-33.8592,-0.18,-376.9265,-0.38,3 +16,Entry short,2025-07-07 23:15,SE,11.62,1600,18592,76.2896,0.41,3.7104,94.1408,0.51,-33.8592,-0.18,-376.9265,-0.38,3 +17,Exit long,2025-07-08 00:45,Close entry(s) order LE,11.54,1700,19669,-54.9287,-0.28,3.9287,15.0331,0.08,-137.9669,-0.70,-431.8552,-0.43,3 +17,Entry long,2025-07-08 00:00,LE,11.57,1700,19669,-54.9287,-0.28,3.9287,15.0331,0.08,-137.9669,-0.70,-431.8552,-0.43,3 +18,Exit short,2025-07-08 01:30,Close entry(s) order SE,11.58,1800,20772,-76.1616,-0.37,4.1616,0,0.00,-128.0772,-0.62,-508.0168,-0.51,3 +18,Entry short,2025-07-08 00:45,SE,11.54,1800,20772,-76.1616,-0.37,4.1616,0,0.00,-128.0772,-0.62,-508.0168,-0.51,3 +19,Exit long,2025-07-08 02:15,Close entry(s) order LE,11.52,1900,22002,-118.389,-0.54,4.389,0,0.00,-154.2002,-0.70,-626.4058,-0.63,3 +19,Entry long,2025-07-08 01:30,LE,11.58,1900,22002,-118.389,-0.54,4.389,0,0.00,-154.2002,-0.70,-626.4058,-0.63,3 +20,Exit short,2025-07-08 03:00,Close entry(s) order SE,11.59,2000,23040,-144.622,-0.63,4.622,17.696,0.08,-162.304,-0.70,-771.0278,-0.77,3 +20,Entry short,2025-07-08 02:15,SE,11.52,2000,23040,-144.622,-0.63,4.622,17.696,0.08,-162.304,-0.70,-771.0278,-0.77,3 +21,Exit long,2025-07-08 03:45,Close entry(s) order LE,11.6,2100,24339,16.1301,0.07,4.8699,39.5661,0.16,-86.4339,-0.36,-754.8977,-0.75,3 +21,Entry long,2025-07-08 03:00,LE,11.59,2100,24339,16.1301,0.07,4.8699,39.5661,0.16,-86.4339,-0.36,-754.8977,-0.75,3 +22,Exit short,2025-07-08 03:45,,11.6,2200,25520,-5.104,-0.02,2.552,0,0.00,-2.552,-0.01,-762.5537,-0.76,0 +22,Entry short,2025-07-08 03:45,SE,11.6,2200,25520,-5.104,-0.02,2.552,0,0.00,-2.552,-0.01,-762.5537,-0.76,0 \ No newline at end of file diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/meta.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/meta.json new file mode 100644 index 00000000..db301257 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I3 flip-l-mag", + "slug": "pa2-i3-flip-l-mag", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:55:55.895Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "f5e533c66072fb7eb55bbd07431273bf210a3fa034e85a03e9c57fc5aedab6b7", + "tv_trades_sha256": "33665e31b1e690ae885c2c51e5e19805659fac961fcf9f932d85623b83155c40", + "tv_trades_rows": 44 +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/metrics.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/metrics.json new file mode 100644 index 00000000..704fd1f5 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i3-flip-l-mag", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 22, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "f5e533c66072fb7eb55bbd07431273bf210a3fa034e85a03e9c57fc5aedab6b7", + "tvTradesCsvHash": "33665e31b1e690ae885c2c51e5e19805659fac961fcf9f932d85623b83155c40", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75523", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "0bd939a73838e842d93154aa9815b2bfe517690ca69e7b012a12c6ce31b9fa4e", + "decodedReportSha256": "92e317d061162f5328b8c90d7d8f0a8469f6645490d616410201fc4de0879282", + "closedTrades": 22, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:55:55.894Z" +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/strategy.pine b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/strategy.pine new file mode 100644 index 00000000..39594617 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/strategy.pine @@ -0,0 +1,39 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 3: corpus probe 96's flip on bar counts, under +// process_orders_on_close. Every six bars a stop entry of the other side is placed while the book is still open, and a same-bar strategy.close of the held id +// flattens it at that close. The stop sits 5c on the close's marketable side (the close has reached it). +// The first flip opens a long. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I3 flip-l-mag", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=true) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +isDown = k % 6 == 5 +isUp = k % 6 == 2 +if k >= 0 and time < tEnd + if isDown + strategy.entry("SE", strategy.short, stop=(t + 5) / 100) + strategy.cancel("LE") + if isUp + strategy.entry("LE", strategy.long, stop=(t - 5) / 100) + strategy.cancel("SE") + if isDown and units > 0 + strategy.close("LE") + if isUp and units < 0 + strategy.close("SE") +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/tv_trades.csv b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/tv_trades.csv new file mode 100644 index 00000000..2385874e --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-l-mag/tv_trades.csv @@ -0,0 +1,45 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 22:45,Close entry(s) order LE,11.51,100,1145,5.7704,0.50,0.2296,6.8855,0.60,-2.1145,-0.18,5.7704,0.01,3 +1,Entry long,2025-07-02 22:00,LE,11.45,100,1145,5.7704,0.50,0.2296,6.8855,0.60,-2.1145,-0.18,5.7704,0.01,3 +2,Exit short,2025-07-02 23:30,Close entry(s) order SE,11.66,200,2302,-30.4634,-1.32,0.4634,1.7698,0.08,-38.2302,-1.66,-24.693,-0.02,3 +2,Entry short,2025-07-02 22:45,SE,11.51,200,2302,-30.4634,-1.32,0.4634,1.7698,0.08,-38.2302,-1.66,-24.693,-0.02,3 +3,Exit long,2025-07-03 00:15,Close entry(s) order LE,11.66,300,3498,-0.6996,-0.02,0.6996,5.6502,0.16,-9.3498,-0.27,-25.3926,-0.03,3 +3,Entry long,2025-07-02 23:30,LE,11.66,300,3498,-0.6996,-0.02,0.6996,5.6502,0.16,-9.3498,-0.27,-25.3926,-0.03,3 +4,Exit short,2025-07-03 01:00,Close entry(s) order SE,11.66,400,4664,-0.9328,-0.02,0.9328,7.5336,0.16,-8.4664,-0.18,-26.3254,-0.03,3 +4,Entry short,2025-07-03 00:15,SE,11.66,400,4664,-0.9328,-0.02,0.9328,7.5336,0.16,-8.4664,-0.18,-26.3254,-0.03,3 +5,Exit long,2025-07-03 01:45,Close entry(s) order LE,11.66,500,5830,-1.166,-0.02,1.166,14.417,0.25,-10.583,-0.18,-27.4914,-0.03,3 +5,Entry long,2025-07-03 01:00,LE,11.66,500,5830,-1.166,-0.02,1.166,14.417,0.25,-10.583,-0.18,-27.4914,-0.03,3 +6,Exit short,2025-07-03 02:30,Close entry(s) order SE,11.73,600,6996,-43.4034,-0.62,1.4034,11.3004,0.16,-42.6996,-0.61,-70.8948,-0.07,3 +6,Entry short,2025-07-03 01:45,SE,11.66,600,6996,-43.4034,-0.62,1.4034,11.3004,0.16,-42.6996,-0.61,-70.8948,-0.07,3 +7,Exit long,2025-07-03 03:15,Close entry(s) order LE,11.75,700,8211,12.3564,0.15,1.6436,34.1789,0.42,-7.8211,-0.10,-58.5384,-0.06,3 +7,Entry long,2025-07-03 02:30,LE,11.73,700,8211,12.3564,0.15,1.6436,34.1789,0.42,-7.8211,-0.10,-58.5384,-0.06,3 +8,Exit short,2025-07-03 21:30,Close entry(s) order SE,11.94,800,9400,-153.8952,-1.64,1.8952,23.06,0.25,-152.94,-1.63,-212.4336,-0.21,3 +8,Entry short,2025-07-03 03:15,SE,11.75,800,9400,-153.8952,-1.64,1.8952,23.06,0.25,-152.94,-1.63,-212.4336,-0.21,3 +9,Exit long,2025-07-03 22:15,Close entry(s) order LE,11.84,900,10746,-92.1402,-0.86,2.1402,16.9254,0.16,-100.0746,-0.93,-304.5738,-0.30,3 +9,Entry long,2025-07-03 21:30,LE,11.94,900,10746,-92.1402,-0.86,2.1402,16.9254,0.16,-100.0746,-0.93,-304.5738,-0.30,3 +10,Exit short,2025-07-03 23:00,Close entry(s) order SE,11.82,1000,11840,17.634,0.15,2.366,38.816,0.33,-31.184,-0.26,-286.9398,-0.29,3 +10,Entry short,2025-07-03 22:15,SE,11.84,1000,11840,17.634,0.15,2.366,38.816,0.33,-31.184,-0.26,-286.9398,-0.29,3 +11,Exit long,2025-07-03 23:45,Close entry(s) order LE,11.84,1100,13002,19.3974,0.15,2.6026,64.6998,0.50,-12.3002,-0.09,-267.5424,-0.27,3 +11,Entry long,2025-07-03 23:00,LE,11.82,1100,13002,19.3974,0.15,2.6026,64.6998,0.50,-12.3002,-0.09,-267.5424,-0.27,3 +12,Exit short,2025-07-04 00:30,Close entry(s) order SE,11.81,1200,14208,33.162,0.23,2.838,58.5792,0.41,-13.4208,-0.09,-234.3804,-0.23,3 +12,Entry short,2025-07-03 23:45,SE,11.84,1200,14208,33.162,0.23,2.838,58.5792,0.41,-13.4208,-0.09,-234.3804,-0.23,3 +13,Exit long,2025-07-07 21:45,Close entry(s) order LE,11.72,1300,15353,-120.0589,-0.78,3.0589,37.4647,0.24,-261.5353,-1.70,-354.4393,-0.35,3 +13,Entry long,2025-07-04 00:30,LE,11.81,1300,15353,-120.0589,-0.78,3.0589,37.4647,0.24,-261.5353,-1.70,-354.4393,-0.35,3 +14,Exit short,2025-07-07 22:30,Close entry(s) order SE,11.7,1400,16408,24.7212,0.15,3.2788,68.3592,0.42,-99.6408,-0.61,-329.7181,-0.33,3 +14,Entry short,2025-07-07 21:45,SE,11.72,1400,16408,24.7212,0.15,3.2788,68.3592,0.42,-99.6408,-0.61,-329.7181,-0.33,3 +15,Exit long,2025-07-07 23:15,Close entry(s) order LE,11.62,1500,17550,-123.498,-0.70,3.498,13.245,0.08,-136.755,-0.78,-453.2161,-0.45,3 +15,Entry long,2025-07-07 22:30,LE,11.7,1500,17550,-123.498,-0.70,3.498,13.245,0.08,-136.755,-0.78,-453.2161,-0.45,3 +16,Exit short,2025-07-08 00:00,Close entry(s) order SE,11.57,1600,18592,76.2896,0.41,3.7104,94.1408,0.51,-33.8592,-0.18,-376.9265,-0.38,3 +16,Entry short,2025-07-07 23:15,SE,11.62,1600,18592,76.2896,0.41,3.7104,94.1408,0.51,-33.8592,-0.18,-376.9265,-0.38,3 +17,Exit long,2025-07-08 00:45,Close entry(s) order LE,11.54,1700,19669,-54.9287,-0.28,3.9287,15.0331,0.08,-137.9669,-0.70,-431.8552,-0.43,3 +17,Entry long,2025-07-08 00:00,LE,11.57,1700,19669,-54.9287,-0.28,3.9287,15.0331,0.08,-137.9669,-0.70,-431.8552,-0.43,3 +18,Exit short,2025-07-08 01:30,Close entry(s) order SE,11.58,1800,20772,-76.1616,-0.37,4.1616,0,0.00,-128.0772,-0.62,-508.0168,-0.51,3 +18,Entry short,2025-07-08 00:45,SE,11.54,1800,20772,-76.1616,-0.37,4.1616,0,0.00,-128.0772,-0.62,-508.0168,-0.51,3 +19,Exit long,2025-07-08 02:15,Close entry(s) order LE,11.52,1900,22002,-118.389,-0.54,4.389,0,0.00,-154.2002,-0.70,-626.4058,-0.63,3 +19,Entry long,2025-07-08 01:30,LE,11.58,1900,22002,-118.389,-0.54,4.389,0,0.00,-154.2002,-0.70,-626.4058,-0.63,3 +20,Exit short,2025-07-08 03:00,Close entry(s) order SE,11.59,2000,23040,-144.622,-0.63,4.622,17.696,0.08,-162.304,-0.70,-771.0278,-0.77,3 +20,Entry short,2025-07-08 02:15,SE,11.52,2000,23040,-144.622,-0.63,4.622,17.696,0.08,-162.304,-0.70,-771.0278,-0.77,3 +21,Exit long,2025-07-08 03:45,Close entry(s) order LE,11.6,2100,24339,16.1301,0.07,4.8699,39.5661,0.16,-86.4339,-0.36,-754.8977,-0.75,3 +21,Entry long,2025-07-08 03:00,LE,11.59,2100,24339,16.1301,0.07,4.8699,39.5661,0.16,-86.4339,-0.36,-754.8977,-0.75,3 +22,Exit short,2025-07-08 03:45,,11.6,2200,25520,-5.104,-0.02,2.552,0,0.00,-2.552,-0.01,-762.5537,-0.76,0 +22,Entry short,2025-07-08 03:45,SE,11.6,2200,25520,-5.104,-0.02,2.552,0,0.00,-2.552,-0.01,-762.5537,-0.76,0 \ No newline at end of file diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/meta.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/meta.json new file mode 100644 index 00000000..c3342fd6 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I3 flip-s-chart", + "slug": "pa2-i3-flip-s-chart", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:56:04.067Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "98e47eab4519a31054dcc68879971cdc2920871b2fa4b0d8459d046d5106a6f4", + "tv_trades_sha256": "fece5dc70a3bcd9590bb28f768f5bb150c886836e049ec2a2ddd9d742250a4b0", + "tv_trades_rows": 44 +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/metrics.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/metrics.json new file mode 100644 index 00000000..d51c9ea8 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i3-flip-s-chart", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 22, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "98e47eab4519a31054dcc68879971cdc2920871b2fa4b0d8459d046d5106a6f4", + "tvTradesCsvHash": "fece5dc70a3bcd9590bb28f768f5bb150c886836e049ec2a2ddd9d742250a4b0", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75528", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "6aef8fa7a31f34b823ae1ba1b9f5851dcb43ea65ba22e38789130ab45b4c25b0", + "decodedReportSha256": "3d2029ca804449a3500a4936232054204598a2a43a6a0d1680dc3d1e52b2d9a6", + "closedTrades": 22, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:56:04.067Z" +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/strategy.pine b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/strategy.pine new file mode 100644 index 00000000..890f9756 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/strategy.pine @@ -0,0 +1,39 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 3: corpus probe 96's flip on bar counts, under +// process_orders_on_close. Every six bars a stop entry of the other side is placed while the book is still open, and a same-bar strategy.close of the held id +// flattens it at that close. The stop sits 5c on the close's marketable side (the close has reached it). +// The first flip opens a short. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I3 flip-s-chart", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +isDown = k % 6 == 2 +isUp = k % 6 == 5 +if k >= 0 and time < tEnd + if isDown + strategy.entry("SE", strategy.short, stop=(t + 5) / 100) + strategy.cancel("LE") + if isUp + strategy.entry("LE", strategy.long, stop=(t - 5) / 100) + strategy.cancel("SE") + if isDown and units > 0 + strategy.close("LE") + if isUp and units < 0 + strategy.close("SE") +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/tv_trades.csv b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/tv_trades.csv new file mode 100644 index 00000000..e4144e67 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-chart/tv_trades.csv @@ -0,0 +1,45 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-02 22:45,Close entry(s) order SE,11.51,100,1145,-6.2296,-0.54,0.2296,1.8855,0.16,-7.1145,-0.62,-6.2296,-0.01,3 +1,Entry short,2025-07-02 22:00,SE,11.45,100,1145,-6.2296,-0.54,0.2296,1.8855,0.16,-7.1145,-0.62,-6.2296,-0.01,3 +2,Exit long,2025-07-02 23:30,Close entry(s) order LE,11.66,200,2302,29.5366,1.28,0.4634,37.7698,1.64,-2.2302,-0.10,23.307,0.02,3 +2,Entry long,2025-07-02 22:45,LE,11.51,200,2302,29.5366,1.28,0.4634,37.7698,1.64,-2.2302,-0.10,23.307,0.02,3 +3,Exit short,2025-07-03 00:15,Close entry(s) order SE,11.66,300,3498,-0.6996,-0.02,0.6996,8.6502,0.25,-6.3498,-0.18,22.6074,0.02,3 +3,Entry short,2025-07-02 23:30,SE,11.66,300,3498,-0.6996,-0.02,0.6996,8.6502,0.25,-6.3498,-0.18,22.6074,0.02,3 +4,Exit long,2025-07-03 01:00,Close entry(s) order LE,11.66,400,4664,-0.9328,-0.02,0.9328,7.5336,0.16,-8.4664,-0.18,21.6746,0.02,3 +4,Entry long,2025-07-03 00:15,LE,11.66,400,4664,-0.9328,-0.02,0.9328,7.5336,0.16,-8.4664,-0.18,21.6746,0.02,3 +5,Exit short,2025-07-03 01:45,Close entry(s) order SE,11.66,500,5830,-1.166,-0.02,1.166,9.417,0.16,-15.583,-0.27,20.5086,0.02,3 +5,Entry short,2025-07-03 01:00,SE,11.66,500,5830,-1.166,-0.02,1.166,9.417,0.16,-15.583,-0.27,20.5086,0.02,3 +6,Exit long,2025-07-03 02:30,Close entry(s) order LE,11.73,600,6996,40.5966,0.58,1.4034,41.3004,0.59,-12.6996,-0.18,61.1052,0.06,3 +6,Entry long,2025-07-03 01:45,LE,11.66,600,6996,40.5966,0.58,1.4034,41.3004,0.59,-12.6996,-0.18,61.1052,0.06,3 +7,Exit short,2025-07-03 03:15,Close entry(s) order SE,11.75,700,8211,-15.6436,-0.19,1.6436,6.1789,0.08,-35.8211,-0.44,45.4616,0.05,3 +7,Entry short,2025-07-03 02:30,SE,11.73,700,8211,-15.6436,-0.19,1.6436,6.1789,0.08,-35.8211,-0.44,45.4616,0.05,3 +8,Exit long,2025-07-03 21:30,Close entry(s) order LE,11.94,800,9400,150.1048,1.60,1.8952,167.06,1.78,-24.94,-0.27,195.5664,0.20,3 +8,Entry long,2025-07-03 03:15,LE,11.75,800,9400,150.1048,1.60,1.8952,167.06,1.78,-24.94,-0.27,195.5664,0.20,3 +9,Exit short,2025-07-03 22:15,Close entry(s) order SE,11.84,900,10746,87.8598,0.82,2.1402,97.9254,0.91,-10.0746,-0.09,283.4262,0.28,3 +9,Entry short,2025-07-03 21:30,SE,11.94,900,10746,87.8598,0.82,2.1402,97.9254,0.91,-10.0746,-0.09,283.4262,0.28,3 +10,Exit long,2025-07-03 23:00,Close entry(s) order LE,11.82,1000,11840,-22.366,-0.19,2.366,28.816,0.24,-41.184,-0.35,261.0602,0.26,3 +10,Entry long,2025-07-03 22:15,LE,11.84,1000,11840,-22.366,-0.19,2.366,28.816,0.24,-41.184,-0.35,261.0602,0.26,3 +11,Exit short,2025-07-03 23:45,Close entry(s) order SE,11.84,1100,13002,-24.6026,-0.19,2.6026,9.6998,0.07,-67.3002,-0.52,236.4576,0.24,3 +11,Entry short,2025-07-03 23:00,SE,11.82,1100,13002,-24.6026,-0.19,2.6026,9.6998,0.07,-67.3002,-0.52,236.4576,0.24,3 +12,Exit long,2025-07-04 00:30,Close entry(s) order LE,11.81,1200,14208,-38.838,-0.27,2.838,10.5792,0.07,-61.4208,-0.43,197.6196,0.20,3 +12,Entry long,2025-07-03 23:45,LE,11.84,1200,14208,-38.838,-0.27,2.838,10.5792,0.07,-61.4208,-0.43,197.6196,0.20,3 +13,Exit short,2025-07-07 21:45,Close entry(s) order SE,11.72,1300,15353,113.9411,0.74,3.0589,258.4647,1.68,-40.5353,-0.26,311.5607,0.31,3 +13,Entry short,2025-07-04 00:30,SE,11.81,1300,15353,113.9411,0.74,3.0589,258.4647,1.68,-40.5353,-0.26,311.5607,0.31,3 +14,Exit long,2025-07-07 22:30,Close entry(s) order LE,11.7,1400,16408,-31.2788,-0.19,3.2788,96.3592,0.59,-71.6408,-0.44,280.2819,0.28,3 +14,Entry long,2025-07-07 21:45,LE,11.72,1400,16408,-31.2788,-0.19,3.2788,96.3592,0.59,-71.6408,-0.44,280.2819,0.28,3 +15,Exit short,2025-07-07 23:15,Close entry(s) order SE,11.62,1500,17550,116.502,0.66,3.498,133.245,0.76,-16.755,-0.10,396.7839,0.40,3 +15,Entry short,2025-07-07 22:30,SE,11.7,1500,17550,116.502,0.66,3.498,133.245,0.76,-16.755,-0.10,396.7839,0.40,3 +16,Exit long,2025-07-08 00:00,Close entry(s) order LE,11.57,1600,18592,-83.7104,-0.45,3.7104,30.1408,0.16,-97.8592,-0.53,313.0735,0.31,3 +16,Entry long,2025-07-07 23:15,LE,11.62,1600,18592,-83.7104,-0.45,3.7104,30.1408,0.16,-97.8592,-0.53,313.0735,0.31,3 +17,Exit short,2025-07-08 00:45,Close entry(s) order SE,11.54,1700,19669,47.0713,0.24,3.9287,134.0331,0.68,-18.9669,-0.10,360.1448,0.36,3 +17,Entry short,2025-07-08 00:00,SE,11.57,1700,19669,47.0713,0.24,3.9287,134.0331,0.68,-18.9669,-0.10,360.1448,0.36,3 +18,Exit long,2025-07-08 01:30,Close entry(s) order LE,11.58,1800,20772,67.8384,0.33,4.1616,123.9228,0.60,-2.0772,-0.01,427.9832,0.43,3 +18,Entry long,2025-07-08 00:45,LE,11.54,1800,20772,67.8384,0.33,4.1616,123.9228,0.60,-2.0772,-0.01,427.9832,0.43,3 +19,Exit short,2025-07-08 02:15,Close entry(s) order SE,11.52,1900,22002,109.611,0.50,4.389,149.7998,0.68,-2.2002,-0.01,537.5942,0.54,3 +19,Entry short,2025-07-08 01:30,SE,11.58,1900,22002,109.611,0.50,4.389,149.7998,0.68,-2.2002,-0.01,537.5942,0.54,3 +20,Exit long,2025-07-08 03:00,Close entry(s) order LE,11.59,2000,23040,135.378,0.59,4.622,157.696,0.68,-22.304,-0.10,672.9722,0.67,3 +20,Entry long,2025-07-08 02:15,LE,11.52,2000,23040,135.378,0.59,4.622,157.696,0.68,-22.304,-0.10,672.9722,0.67,3 +21,Exit short,2025-07-08 03:45,Close entry(s) order SE,11.6,2100,24339,-25.8699,-0.11,4.8699,81.5661,0.34,-44.4339,-0.18,647.1023,0.65,3 +21,Entry short,2025-07-08 03:00,SE,11.59,2100,24339,-25.8699,-0.11,4.8699,81.5661,0.34,-44.4339,-0.18,647.1023,0.65,3 +22,Exit long,2025-07-08 03:45,,11.6,2200,25520,-5.104,-0.02,2.552,0,0.00,-2.552,-0.01,639.4463,0.64,0 +22,Entry long,2025-07-08 03:45,LE,11.6,2200,25520,-5.104,-0.02,2.552,0,0.00,-2.552,-0.01,639.4463,0.64,0 \ No newline at end of file diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/meta.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/meta.json new file mode 100644 index 00000000..e2c9622f --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I3 flip-s-mag", + "slug": "pa2-i3-flip-s-mag", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:56:15.501Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "64db35f0f77557124a4bf55c8c2557c0e26b15f27b2017b316dcb805399832a4", + "tv_trades_sha256": "e3ef67816e8b94589b18ec740441d479a10e069527f123914cafc69ba970cf8a", + "tv_trades_rows": 44 +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/metrics.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/metrics.json new file mode 100644 index 00000000..3335511a --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i3-flip-s-mag", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 22, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "64db35f0f77557124a4bf55c8c2557c0e26b15f27b2017b316dcb805399832a4", + "tvTradesCsvHash": "e3ef67816e8b94589b18ec740441d479a10e069527f123914cafc69ba970cf8a", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75535", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f2f6bfc2dc79d70366ae68ac3e401f62e135388a63ce4858e05e80bdb93fdc7a", + "decodedReportSha256": "e54dc65c377c71c8a5ffb8766b7a2f6da8e8f3d3d848199caed4502add67ece4", + "closedTrades": 22, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:56:15.500Z" +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/strategy.pine b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/strategy.pine new file mode 100644 index 00000000..db972cde --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/strategy.pine @@ -0,0 +1,39 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 3: corpus probe 96's flip on bar counts, under +// process_orders_on_close. Every six bars a stop entry of the other side is placed while the book is still open, and a same-bar strategy.close of the held id +// flattens it at that close. The stop sits 5c on the close's marketable side (the close has reached it). +// The first flip opens a short. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I3 flip-s-mag", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=true) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +isDown = k % 6 == 2 +isUp = k % 6 == 5 +if k >= 0 and time < tEnd + if isDown + strategy.entry("SE", strategy.short, stop=(t + 5) / 100) + strategy.cancel("LE") + if isUp + strategy.entry("LE", strategy.long, stop=(t - 5) / 100) + strategy.cancel("SE") + if isDown and units > 0 + strategy.close("LE") + if isUp and units < 0 + strategy.close("SE") +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/tv_trades.csv b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/tv_trades.csv new file mode 100644 index 00000000..56dd1ba6 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-s-mag/tv_trades.csv @@ -0,0 +1,45 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-02 22:45,Close entry(s) order SE,11.51,100,1145,-6.2296,-0.54,0.2296,1.8855,0.16,-7.1145,-0.62,-6.2296,-0.01,3 +1,Entry short,2025-07-02 22:00,SE,11.45,100,1145,-6.2296,-0.54,0.2296,1.8855,0.16,-7.1145,-0.62,-6.2296,-0.01,3 +2,Exit long,2025-07-02 23:30,Close entry(s) order LE,11.66,200,2302,29.5366,1.28,0.4634,37.7698,1.64,-2.2302,-0.10,23.307,0.02,3 +2,Entry long,2025-07-02 22:45,LE,11.51,200,2302,29.5366,1.28,0.4634,37.7698,1.64,-2.2302,-0.10,23.307,0.02,3 +3,Exit short,2025-07-03 00:15,Close entry(s) order SE,11.66,300,3498,-0.6996,-0.02,0.6996,8.6502,0.25,-6.3498,-0.18,22.6074,0.02,3 +3,Entry short,2025-07-02 23:30,SE,11.66,300,3498,-0.6996,-0.02,0.6996,8.6502,0.25,-6.3498,-0.18,22.6074,0.02,3 +4,Exit long,2025-07-03 01:00,Close entry(s) order LE,11.66,400,4664,-0.9328,-0.02,0.9328,7.5336,0.16,-8.4664,-0.18,21.6746,0.02,3 +4,Entry long,2025-07-03 00:15,LE,11.66,400,4664,-0.9328,-0.02,0.9328,7.5336,0.16,-8.4664,-0.18,21.6746,0.02,3 +5,Exit short,2025-07-03 01:45,Close entry(s) order SE,11.66,500,5830,-1.166,-0.02,1.166,9.417,0.16,-15.583,-0.27,20.5086,0.02,3 +5,Entry short,2025-07-03 01:00,SE,11.66,500,5830,-1.166,-0.02,1.166,9.417,0.16,-15.583,-0.27,20.5086,0.02,3 +6,Exit long,2025-07-03 02:30,Close entry(s) order LE,11.73,600,6996,40.5966,0.58,1.4034,41.3004,0.59,-12.6996,-0.18,61.1052,0.06,3 +6,Entry long,2025-07-03 01:45,LE,11.66,600,6996,40.5966,0.58,1.4034,41.3004,0.59,-12.6996,-0.18,61.1052,0.06,3 +7,Exit short,2025-07-03 03:15,Close entry(s) order SE,11.75,700,8211,-15.6436,-0.19,1.6436,6.1789,0.08,-35.8211,-0.44,45.4616,0.05,3 +7,Entry short,2025-07-03 02:30,SE,11.73,700,8211,-15.6436,-0.19,1.6436,6.1789,0.08,-35.8211,-0.44,45.4616,0.05,3 +8,Exit long,2025-07-03 21:30,Close entry(s) order LE,11.94,800,9400,150.1048,1.60,1.8952,151.06,1.61,-24.94,-0.27,195.5664,0.20,3 +8,Entry long,2025-07-03 03:15,LE,11.75,800,9400,150.1048,1.60,1.8952,151.06,1.61,-24.94,-0.27,195.5664,0.20,3 +9,Exit short,2025-07-03 22:15,Close entry(s) order SE,11.84,900,10746,87.8598,0.82,2.1402,97.9254,0.91,-19.0746,-0.18,283.4262,0.28,3 +9,Entry short,2025-07-03 21:30,SE,11.94,900,10746,87.8598,0.82,2.1402,97.9254,0.91,-19.0746,-0.18,283.4262,0.28,3 +10,Exit long,2025-07-03 23:00,Close entry(s) order LE,11.82,1000,11840,-22.366,-0.19,2.366,28.816,0.24,-41.184,-0.35,261.0602,0.26,3 +10,Entry long,2025-07-03 22:15,LE,11.84,1000,11840,-22.366,-0.19,2.366,28.816,0.24,-41.184,-0.35,261.0602,0.26,3 +11,Exit short,2025-07-03 23:45,Close entry(s) order SE,11.84,1100,13002,-24.6026,-0.19,2.6026,9.6998,0.07,-67.3002,-0.52,236.4576,0.24,3 +11,Entry short,2025-07-03 23:00,SE,11.82,1100,13002,-24.6026,-0.19,2.6026,9.6998,0.07,-67.3002,-0.52,236.4576,0.24,3 +12,Exit long,2025-07-04 00:30,Close entry(s) order LE,11.81,1200,14208,-38.838,-0.27,2.838,10.5792,0.07,-61.4208,-0.43,197.6196,0.20,3 +12,Entry long,2025-07-03 23:45,LE,11.84,1200,14208,-38.838,-0.27,2.838,10.5792,0.07,-61.4208,-0.43,197.6196,0.20,3 +13,Exit short,2025-07-07 21:45,Close entry(s) order SE,11.72,1300,15353,113.9411,0.74,3.0589,258.4647,1.68,-40.5353,-0.26,311.5607,0.31,3 +13,Entry short,2025-07-04 00:30,SE,11.81,1300,15353,113.9411,0.74,3.0589,258.4647,1.68,-40.5353,-0.26,311.5607,0.31,3 +14,Exit long,2025-07-07 22:30,Close entry(s) order LE,11.7,1400,16408,-31.2788,-0.19,3.2788,96.3592,0.59,-71.6408,-0.44,280.2819,0.28,3 +14,Entry long,2025-07-07 21:45,LE,11.72,1400,16408,-31.2788,-0.19,3.2788,96.3592,0.59,-71.6408,-0.44,280.2819,0.28,3 +15,Exit short,2025-07-07 23:15,Close entry(s) order SE,11.62,1500,17550,116.502,0.66,3.498,133.245,0.76,-16.755,-0.10,396.7839,0.40,3 +15,Entry short,2025-07-07 22:30,SE,11.7,1500,17550,116.502,0.66,3.498,133.245,0.76,-16.755,-0.10,396.7839,0.40,3 +16,Exit long,2025-07-08 00:00,Close entry(s) order LE,11.57,1600,18592,-83.7104,-0.45,3.7104,30.1408,0.16,-97.8592,-0.53,313.0735,0.31,3 +16,Entry long,2025-07-07 23:15,LE,11.62,1600,18592,-83.7104,-0.45,3.7104,30.1408,0.16,-97.8592,-0.53,313.0735,0.31,3 +17,Exit short,2025-07-08 00:45,Close entry(s) order SE,11.54,1700,19669,47.0713,0.24,3.9287,134.0331,0.68,-18.9669,-0.10,360.1448,0.36,3 +17,Entry short,2025-07-08 00:00,SE,11.57,1700,19669,47.0713,0.24,3.9287,134.0331,0.68,-18.9669,-0.10,360.1448,0.36,3 +18,Exit long,2025-07-08 01:30,Close entry(s) order LE,11.58,1800,20772,67.8384,0.33,4.1616,123.9228,0.60,-2.0772,-0.01,427.9832,0.43,3 +18,Entry long,2025-07-08 00:45,LE,11.54,1800,20772,67.8384,0.33,4.1616,123.9228,0.60,-2.0772,-0.01,427.9832,0.43,3 +19,Exit short,2025-07-08 02:15,Close entry(s) order SE,11.52,1900,22002,109.611,0.50,4.389,149.7998,0.68,-2.2002,-0.01,537.5942,0.54,3 +19,Entry short,2025-07-08 01:30,SE,11.58,1900,22002,109.611,0.50,4.389,149.7998,0.68,-2.2002,-0.01,537.5942,0.54,3 +20,Exit long,2025-07-08 03:00,Close entry(s) order LE,11.59,2000,23040,135.378,0.59,4.622,157.696,0.68,-22.304,-0.10,672.9722,0.67,3 +20,Entry long,2025-07-08 02:15,LE,11.52,2000,23040,135.378,0.59,4.622,157.696,0.68,-22.304,-0.10,672.9722,0.67,3 +21,Exit short,2025-07-08 03:45,Close entry(s) order SE,11.6,2100,24339,-25.8699,-0.11,4.8699,81.5661,0.34,-44.4339,-0.18,647.1023,0.65,3 +21,Entry short,2025-07-08 03:00,SE,11.59,2100,24339,-25.8699,-0.11,4.8699,81.5661,0.34,-44.4339,-0.18,647.1023,0.65,3 +22,Exit long,2025-07-08 03:45,,11.6,2200,25520,-5.104,-0.02,2.552,0,0.00,-2.552,-0.01,639.4463,0.64,0 +22,Entry long,2025-07-08 03:45,LE,11.6,2200,25520,-5.104,-0.02,2.552,0,0.00,-2.552,-0.01,639.4463,0.64,0 \ No newline at end of file diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/meta.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/meta.json new file mode 100644 index 00000000..b89967f9 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I3 flip-unreached-s-chart", + "slug": "pa2-i3-flip-unreached-s-chart", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:56:19.632Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "ba510dd838f271e762e1efb1fc05552dca960bf6f78b157e25b5a0fa1677429c", + "tv_trades_sha256": "b2e8779bcf6bc769756b0b00cedcb182230c814985b6131b7f390b7ca70eb39e", + "tv_trades_rows": 22 +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/metrics.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/metrics.json new file mode 100644 index 00000000..01d15232 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i3-flip-unreached-s-chart", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 11, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "ba510dd838f271e762e1efb1fc05552dca960bf6f78b157e25b5a0fa1677429c", + "tvTradesCsvHash": "b2e8779bcf6bc769756b0b00cedcb182230c814985b6131b7f390b7ca70eb39e", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75540", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "7d5693d2dacff042dc504fb9637c8ce54b73dcfc256d26b650e726420a3336d5", + "decodedReportSha256": "2eb43844aed15eec97d9b2cc7783a30f3719a698ad92d81153bddc206799ece3", + "closedTrades": 11, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:56:19.632Z" +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/strategy.pine b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/strategy.pine new file mode 100644 index 00000000..61bd1a1e --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/strategy.pine @@ -0,0 +1,39 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 3: corpus probe 96's flip on bar counts, under +// process_orders_on_close. Every six bars a stop entry of the other side is placed while the book is still open, and a same-bar strategy.close of the held id +// flattens it at that close. The stop sits 5c beyond the close (the close has not reached it). +// The first flip opens a short. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I3 flip-unreached-s-chart", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +isDown = k % 6 == 2 +isUp = k % 6 == 5 +if k >= 0 and time < tEnd + if isDown + strategy.entry("SE", strategy.short, stop=(t - 5) / 100) + strategy.cancel("LE") + if isUp + strategy.entry("LE", strategy.long, stop=(t + 5) / 100) + strategy.cancel("SE") + if isDown and units > 0 + strategy.close("LE") + if isUp and units < 0 + strategy.close("SE") +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/tv_trades.csv b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/tv_trades.csv new file mode 100644 index 00000000..740459ea --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-chart/tv_trades.csv @@ -0,0 +1,23 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:30,Close entry(s) order LE,11.66,100,1155,10.7679,0.93,0.2321,14.8845,1.29,0,0.00,10.7679,0.01,2 +1,Entry long,2025-07-02 23:00,LE,11.55,100,1155,10.7679,0.93,0.2321,14.8845,1.29,0,0.00,10.7679,0.01,2 +2,Exit long,2025-07-03 02:30,Close entry(s) order LE,11.73,100,1170,2.7657,0.24,0.2343,2.883,0.25,0,0.00,13.5336,0.01,1 +2,Entry long,2025-07-03 02:15,LE,11.7,100,1170,2.7657,0.24,0.2343,2.883,0.25,0,0.00,13.5336,0.01,1 +3,Exit long,2025-07-03 21:30,Close entry(s) order LE,11.94,100,1179,14.7627,1.25,0.2373,16.8821,1.43,0,0.00,28.2963,0.03,0 +3,Entry long,2025-07-03 21:30,LE,11.79,100,1179,14.7627,1.25,0.2373,16.8821,1.43,0,0.00,28.2963,0.03,0 +4,Exit short,2025-07-03 22:15,Close entry(s) order SE,11.84,200,2378,9.5254,0.40,0.4746,11.7622,0.49,-2.2378,-0.09,37.8217,0.04,2 +4,Entry short,2025-07-03 21:45,SE,11.89,200,2378,9.5254,0.40,0.4746,11.7622,0.49,-2.2378,-0.09,37.8217,0.04,2 +5,Exit short,2025-07-07 21:45,Close entry(s) order SE,11.72,100,1162,-10.2334,-0.88,0.2334,0.8838,0.08,-11.1162,-0.96,27.5883,0.03,1 +5,Entry short,2025-07-07 21:30,SE,11.62,100,1162,-10.2334,-0.88,0.2334,0.8838,0.08,-11.1162,-0.96,27.5883,0.03,1 +6,Exit long,2025-07-07 22:30,Close entry(s) order LE,11.7,200,2354,-14.4694,-0.61,0.4694,3.7646,0.16,-20.2354,-0.86,13.1189,0.01,2 +6,Entry long,2025-07-07 22:00,LE,11.77,200,2354,-14.4694,-0.61,0.4694,3.7646,0.16,-20.2354,-0.86,13.1189,0.01,2 +7,Exit short,2025-07-07 23:15,Close entry(s) order SE,11.62,300,3492,5.3022,0.15,0.6978,8.6508,0.25,0,0.00,18.4211,0.02,1 +7,Entry short,2025-07-07 23:00,SE,11.64,300,3492,5.3022,0.15,0.6978,8.6508,0.25,0,0.00,18.4211,0.02,1 +8,Exit short,2025-07-08 00:45,Close entry(s) order SE,11.54,100,1151,-3.2305,-0.28,0.2305,1.8849,0.16,-6.1151,-0.53,15.1906,0.02,2 +8,Entry short,2025-07-08 00:15,SE,11.51,100,1151,-3.2305,-0.28,0.2305,1.8849,0.16,-6.1151,-0.53,15.1906,0.02,2 +9,Exit long,2025-07-08 01:30,Close entry(s) order LE,11.58,200,2316,-0.4632,-0.02,0.4632,5.7684,0.25,-2.2316,-0.10,14.7274,0.01,1 +9,Entry long,2025-07-08 01:15,LE,11.58,200,2316,-0.4632,-0.02,0.4632,5.7684,0.25,-2.2316,-0.10,14.7274,0.01,1 +10,Exit short,2025-07-08 02:15,Close entry(s) order SE,11.52,300,3456,-0.6912,-0.02,0.6912,5.6544,0.16,-0.3456,-0.01,14.0362,0.01,0 +10,Entry short,2025-07-08 02:15,SE,11.52,300,3456,-0.6912,-0.02,0.6912,5.6544,0.16,-0.3456,-0.01,14.0362,0.01,0 +11,Exit long,2025-07-08 03:00,Close entry(s) order LE,11.59,400,4624,11.074,0.24,0.926,15.5376,0.34,-4.4624,-0.10,25.1102,0.03,2 +11,Entry long,2025-07-08 02:30,LE,11.56,400,4624,11.074,0.24,0.926,15.5376,0.34,-4.4624,-0.10,25.1102,0.03,2 \ No newline at end of file diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/meta.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/meta.json new file mode 100644 index 00000000..85957005 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I3 flip-unreached-s-mag", + "slug": "pa2-i3-flip-unreached-s-mag", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:56:25.584Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "620cfd7d47d2c8511d07f9991bf82036202c1b3e14ebddc41522d936166adac7", + "tv_trades_sha256": "b19172f54665a75a1d9e13c603759de43cb0b6cfa4624afaeb33c54cfa792cd3", + "tv_trades_rows": 22 +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/metrics.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/metrics.json new file mode 100644 index 00000000..4d8436b5 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i3-flip-unreached-s-mag", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 11, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "620cfd7d47d2c8511d07f9991bf82036202c1b3e14ebddc41522d936166adac7", + "tvTradesCsvHash": "b19172f54665a75a1d9e13c603759de43cb0b6cfa4624afaeb33c54cfa792cd3", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75543", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "ab2c765c29b74b2d76928d6965a0334b4c5d97388190e1760ad5032870149a9e", + "decodedReportSha256": "b6645095d9f046321feacf86add19bbb8bf86bb38a087eac24d74886d7cf929b", + "closedTrades": 11, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:56:25.583Z" +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/strategy.pine b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/strategy.pine new file mode 100644 index 00000000..aeea2ee7 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/strategy.pine @@ -0,0 +1,39 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 3: corpus probe 96's flip on bar counts, under +// process_orders_on_close. Every six bars a stop entry of the other side is placed while the book is still open, and a same-bar strategy.close of the held id +// flattens it at that close. The stop sits 5c beyond the close (the close has not reached it). +// The first flip opens a short. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I3 flip-unreached-s-mag", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=true) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +isDown = k % 6 == 2 +isUp = k % 6 == 5 +if k >= 0 and time < tEnd + if isDown + strategy.entry("SE", strategy.short, stop=(t - 5) / 100) + strategy.cancel("LE") + if isUp + strategy.entry("LE", strategy.long, stop=(t + 5) / 100) + strategy.cancel("SE") + if isDown and units > 0 + strategy.close("LE") + if isUp and units < 0 + strategy.close("SE") +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/tv_trades.csv b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/tv_trades.csv new file mode 100644 index 00000000..29fa5f8d --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-flip-unreached-s-mag/tv_trades.csv @@ -0,0 +1,23 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 23:30,Close entry(s) order LE,11.66,100,1155,10.7679,0.93,0.2321,14.8845,1.29,-1.1155,-0.10,10.7679,0.01,2 +1,Entry long,2025-07-02 23:00,LE,11.55,100,1155,10.7679,0.93,0.2321,14.8845,1.29,-1.1155,-0.10,10.7679,0.01,2 +2,Exit long,2025-07-03 02:30,Close entry(s) order LE,11.73,100,1170,2.7657,0.24,0.2343,2.883,0.25,-0.117,-0.01,13.5336,0.01,1 +2,Entry long,2025-07-03 02:15,LE,11.7,100,1170,2.7657,0.24,0.2343,2.883,0.25,-0.117,-0.01,13.5336,0.01,1 +3,Exit long,2025-07-03 21:30,Close entry(s) order LE,11.94,100,1179,14.7627,1.25,0.2373,14.8821,1.26,-0.1179,-0.01,28.2963,0.03,0 +3,Entry long,2025-07-03 21:30,LE,11.79,100,1179,14.7627,1.25,0.2373,14.8821,1.26,-0.1179,-0.01,28.2963,0.03,0 +4,Exit short,2025-07-03 22:15,Close entry(s) order SE,11.84,200,2378,9.5254,0.40,0.4746,11.7622,0.49,-2.2378,-0.09,37.8217,0.04,2 +4,Entry short,2025-07-03 21:45,SE,11.89,200,2378,9.5254,0.40,0.4746,11.7622,0.49,-2.2378,-0.09,37.8217,0.04,2 +5,Exit short,2025-07-07 21:45,Close entry(s) order SE,11.72,100,1162,-10.2334,-0.88,0.2334,0.8838,0.08,-11.1162,-0.96,27.5883,0.03,1 +5,Entry short,2025-07-07 21:30,SE,11.62,100,1162,-10.2334,-0.88,0.2334,0.8838,0.08,-11.1162,-0.96,27.5883,0.03,1 +6,Exit long,2025-07-07 22:30,Close entry(s) order LE,11.7,200,2354,-14.4694,-0.61,0.4694,3.7646,0.16,-20.2354,-0.86,13.1189,0.01,2 +6,Entry long,2025-07-07 22:00,LE,11.77,200,2354,-14.4694,-0.61,0.4694,3.7646,0.16,-20.2354,-0.86,13.1189,0.01,2 +7,Exit short,2025-07-07 23:15,Close entry(s) order SE,11.62,300,3492,5.3022,0.15,0.6978,8.6508,0.25,-3.3492,-0.10,18.4211,0.02,1 +7,Entry short,2025-07-07 23:00,SE,11.64,300,3492,5.3022,0.15,0.6978,8.6508,0.25,-3.3492,-0.10,18.4211,0.02,1 +8,Exit short,2025-07-08 00:45,Close entry(s) order SE,11.54,100,1151,-3.2305,-0.28,0.2305,1.8849,0.16,-6.1151,-0.53,15.1906,0.02,2 +8,Entry short,2025-07-08 00:15,SE,11.51,100,1151,-3.2305,-0.28,0.2305,1.8849,0.16,-6.1151,-0.53,15.1906,0.02,2 +9,Exit long,2025-07-08 01:30,Close entry(s) order LE,11.58,200,2316,-0.4632,-0.02,0.4632,5.7684,0.25,-2.2316,-0.10,14.7274,0.01,1 +9,Entry long,2025-07-08 01:15,LE,11.58,200,2316,-0.4632,-0.02,0.4632,5.7684,0.25,-2.2316,-0.10,14.7274,0.01,1 +10,Exit short,2025-07-08 02:15,Close entry(s) order SE,11.52,300,3456,-0.6912,-0.02,0.6912,5.6544,0.16,-6.3456,-0.18,14.0362,0.01,0 +10,Entry short,2025-07-08 02:15,SE,11.52,300,3456,-0.6912,-0.02,0.6912,5.6544,0.16,-6.3456,-0.18,14.0362,0.01,0 +11,Exit long,2025-07-08 03:00,Close entry(s) order LE,11.59,400,4624,11.074,0.24,0.926,15.5376,0.34,-4.4624,-0.10,25.1102,0.03,2 +11,Entry long,2025-07-08 02:30,LE,11.56,400,4624,11.074,0.24,0.926,15.5376,0.34,-4.4624,-0.10,25.1102,0.03,2 \ No newline at end of file diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/meta.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/meta.json new file mode 100644 index 00000000..ef124fe3 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I3 rev-s-chart", + "slug": "pa2-i3-rev-s-chart", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:56:30.405Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "c36485be842883780e340f451a874089247883fde3c319676a68cb42a598d39d", + "tv_trades_sha256": "e4d9c73535ca8fb6fb98bf23951822c3fcbe9a5742575182a1aa1894715669e5", + "tv_trades_rows": 44 +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/metrics.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/metrics.json new file mode 100644 index 00000000..8b4b8173 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i3-rev-s-chart", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 22, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "c36485be842883780e340f451a874089247883fde3c319676a68cb42a598d39d", + "tvTradesCsvHash": "e4d9c73535ca8fb6fb98bf23951822c3fcbe9a5742575182a1aa1894715669e5", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75549", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "dd537ea067b878f43d55a0887e0a2c799a3b4176586e712659bd0918a08e255a", + "decodedReportSha256": "647d1def7d02e5be2777186b641f6606d0e9a21ce53bac2e8cea56d78b855d22", + "closedTrades": 22, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:56:30.405Z" +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/strategy.pine b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/strategy.pine new file mode 100644 index 00000000..525c3307 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/strategy.pine @@ -0,0 +1,35 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 3: corpus probe 96's flip on bar counts, under +// process_orders_on_close. Every six bars a stop entry of the other side is placed while the book is still open, with NO same-bar strategy.close (a plain reversal) +// The stop sits 5c on the close's marketable side (the close has reached it). +// The first flip opens a short. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I3 rev-s-chart", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +isDown = k % 6 == 2 +isUp = k % 6 == 5 +if k >= 0 and time < tEnd + if isDown + strategy.entry("SE", strategy.short, stop=(t + 5) / 100) + strategy.cancel("LE") + if isUp + strategy.entry("LE", strategy.long, stop=(t - 5) / 100) + strategy.cancel("SE") +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/tv_trades.csv b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/tv_trades.csv new file mode 100644 index 00000000..79d5d872 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-chart/tv_trades.csv @@ -0,0 +1,45 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-02 22:45,LE,11.51,100,1145,-6.2296,-0.54,0.2296,1.8855,0.16,-7.1145,-0.62,-6.2296,-0.01,3 +1,Entry short,2025-07-02 22:00,SE,11.45,100,1145,-6.2296,-0.54,0.2296,1.8855,0.16,-7.1145,-0.62,-6.2296,-0.01,3 +2,Exit long,2025-07-02 23:30,SE,11.66,100,1151,14.7683,1.28,0.2317,18.8849,1.64,-1.1151,-0.10,8.5387,0.01,3 +2,Entry long,2025-07-02 22:45,LE,11.51,100,1151,14.7683,1.28,0.2317,18.8849,1.64,-1.1151,-0.10,8.5387,0.01,3 +3,Exit short,2025-07-03 00:15,LE,11.66,100,1166,-0.2332,-0.02,0.2332,2.8834,0.25,-2.1166,-0.18,8.3055,0.01,3 +3,Entry short,2025-07-02 23:30,SE,11.66,100,1166,-0.2332,-0.02,0.2332,2.8834,0.25,-2.1166,-0.18,8.3055,0.01,3 +4,Exit long,2025-07-03 01:00,SE,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-2.1166,-0.18,8.0723,0.01,3 +4,Entry long,2025-07-03 00:15,LE,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-2.1166,-0.18,8.0723,0.01,3 +5,Exit short,2025-07-03 01:45,LE,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-3.1166,-0.27,7.8391,0.01,3 +5,Entry short,2025-07-03 01:00,SE,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-3.1166,-0.27,7.8391,0.01,3 +6,Exit long,2025-07-03 02:30,SE,11.73,100,1166,6.7661,0.58,0.2339,6.8834,0.59,-2.1166,-0.18,14.6052,0.01,3 +6,Entry long,2025-07-03 01:45,LE,11.66,100,1166,6.7661,0.58,0.2339,6.8834,0.59,-2.1166,-0.18,14.6052,0.01,3 +7,Exit short,2025-07-03 03:15,LE,11.75,100,1173,-2.2348,-0.19,0.2348,0.8827,0.08,-5.1173,-0.44,12.3704,0.01,3 +7,Entry short,2025-07-03 02:30,SE,11.73,100,1173,-2.2348,-0.19,0.2348,0.8827,0.08,-5.1173,-0.44,12.3704,0.01,3 +8,Exit long,2025-07-03 21:30,SE,11.94,100,1175,18.7631,1.60,0.2369,20.8825,1.78,-3.1175,-0.27,31.1335,0.03,3 +8,Entry long,2025-07-03 03:15,LE,11.75,100,1175,18.7631,1.60,0.2369,20.8825,1.78,-3.1175,-0.27,31.1335,0.03,3 +9,Exit short,2025-07-03 22:15,LE,11.84,100,1194,9.7622,0.82,0.2378,10.8806,0.91,-1.1194,-0.09,40.8957,0.04,3 +9,Entry short,2025-07-03 21:30,SE,11.94,100,1194,9.7622,0.82,0.2378,10.8806,0.91,-1.1194,-0.09,40.8957,0.04,3 +10,Exit long,2025-07-03 23:00,SE,11.82,100,1184,-2.2366,-0.19,0.2366,2.8816,0.24,-4.1184,-0.35,38.6591,0.04,3 +10,Entry long,2025-07-03 22:15,LE,11.84,100,1184,-2.2366,-0.19,0.2366,2.8816,0.24,-4.1184,-0.35,38.6591,0.04,3 +11,Exit short,2025-07-03 23:45,LE,11.84,100,1182,-2.2366,-0.19,0.2366,0.8818,0.07,-6.1182,-0.52,36.4225,0.04,3 +11,Entry short,2025-07-03 23:00,SE,11.82,100,1182,-2.2366,-0.19,0.2366,0.8818,0.07,-6.1182,-0.52,36.4225,0.04,3 +12,Exit long,2025-07-04 00:30,SE,11.81,100,1184,-3.2365,-0.27,0.2365,0.8816,0.07,-5.1184,-0.43,33.186,0.03,3 +12,Entry long,2025-07-03 23:45,LE,11.84,100,1184,-3.2365,-0.27,0.2365,0.8816,0.07,-5.1184,-0.43,33.186,0.03,3 +13,Exit short,2025-07-07 21:45,LE,11.72,100,1181,8.7647,0.74,0.2353,19.8819,1.68,-3.1181,-0.26,41.9507,0.04,3 +13,Entry short,2025-07-04 00:30,SE,11.81,100,1181,8.7647,0.74,0.2353,19.8819,1.68,-3.1181,-0.26,41.9507,0.04,3 +14,Exit long,2025-07-07 22:30,SE,11.7,100,1172,-2.2342,-0.19,0.2342,6.8828,0.59,-5.1172,-0.44,39.7165,0.04,3 +14,Entry long,2025-07-07 21:45,LE,11.72,100,1172,-2.2342,-0.19,0.2342,6.8828,0.59,-5.1172,-0.44,39.7165,0.04,3 +15,Exit short,2025-07-07 23:15,LE,11.62,100,1170,7.7668,0.66,0.2332,8.883,0.76,-1.117,-0.10,47.4833,0.05,3 +15,Entry short,2025-07-07 22:30,SE,11.7,100,1170,7.7668,0.66,0.2332,8.883,0.76,-1.117,-0.10,47.4833,0.05,3 +16,Exit long,2025-07-08 00:00,SE,11.57,100,1162,-5.2319,-0.45,0.2319,1.8838,0.16,-6.1162,-0.53,42.2514,0.04,3 +16,Entry long,2025-07-07 23:15,LE,11.62,100,1162,-5.2319,-0.45,0.2319,1.8838,0.16,-6.1162,-0.53,42.2514,0.04,3 +17,Exit short,2025-07-08 00:45,LE,11.54,100,1157,2.7689,0.24,0.2311,7.8843,0.68,-1.1157,-0.10,45.0203,0.05,3 +17,Entry short,2025-07-08 00:00,SE,11.57,100,1157,2.7689,0.24,0.2311,7.8843,0.68,-1.1157,-0.10,45.0203,0.05,3 +18,Exit long,2025-07-08 01:30,SE,11.58,100,1154,3.7688,0.33,0.2312,6.8846,0.60,-0.1154,-0.01,48.7891,0.05,3 +18,Entry long,2025-07-08 00:45,LE,11.54,100,1154,3.7688,0.33,0.2312,6.8846,0.60,-0.1154,-0.01,48.7891,0.05,3 +19,Exit short,2025-07-08 02:15,LE,11.52,100,1158,5.769,0.50,0.231,7.8842,0.68,-0.1158,-0.01,54.5581,0.05,3 +19,Entry short,2025-07-08 01:30,SE,11.58,100,1158,5.769,0.50,0.231,7.8842,0.68,-0.1158,-0.01,54.5581,0.05,3 +20,Exit long,2025-07-08 03:00,SE,11.59,100,1152,6.7689,0.59,0.2311,7.8848,0.68,-1.1152,-0.10,61.327,0.06,3 +20,Entry long,2025-07-08 02:15,LE,11.52,100,1152,6.7689,0.59,0.2311,7.8848,0.68,-1.1152,-0.10,61.327,0.06,3 +21,Exit short,2025-07-08 03:45,LE,11.6,100,1159,-1.2319,-0.11,0.2319,3.8841,0.34,-2.1159,-0.18,60.0951,0.06,3 +21,Entry short,2025-07-08 03:00,SE,11.59,100,1159,-1.2319,-0.11,0.2319,3.8841,0.34,-2.1159,-0.18,60.0951,0.06,3 +22,Exit long,2025-07-08 03:45,,11.6,100,1160,-0.232,-0.02,0.116,0,0.00,-0.116,-0.01,59.7471,0.06,0 +22,Entry long,2025-07-08 03:45,LE,11.6,100,1160,-0.232,-0.02,0.116,0,0.00,-0.116,-0.01,59.7471,0.06,0 \ No newline at end of file diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/meta.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/meta.json new file mode 100644 index 00000000..2f5afdfc --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/meta.json @@ -0,0 +1,16 @@ +{ + "title": "PA2 I3 rev-s-mag", + "slug": "pa2-i3-rev-s-mag", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:56:34.315Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "a166ff17a9f1584e7603739c81a40201e91541363b7752ba3e199955c094b1db", + "tv_trades_sha256": "fc405dd76617426a32815a8eade4ea07df3603ad2c5dab7572c1f9e0af36945c", + "tv_trades_rows": 44 +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/metrics.json b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/metrics.json new file mode 100644 index 00000000..0723872a --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pa2-i3-rev-s-mag", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 22, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "a166ff17a9f1584e7603739c81a40201e91541363b7752ba3e199955c094b1db", + "tvTradesCsvHash": "fc405dd76617426a32815a8eade4ea07df3603ad2c5dab7572c1f9e0af36945c", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75554", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "12cecd942b23d9288eef4801fd1a59b6c75c9a93865d7cdec523bf4997f08783", + "decodedReportSha256": "5531dbe83d8b501afe235b8ec68bf01c3c24109fbe0b078e706bf8dec594d968", + "closedTrades": 22, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:56:34.314Z" +} diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/strategy.pine b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/strategy.pine new file mode 100644 index 00000000..659505b2 --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/strategy.pine @@ -0,0 +1,35 @@ +//@version=6 +// PAR-ORDERS-2 (R5 lane) item 3: corpus probe 96's flip on bar counts, under +// process_orders_on_close. Every six bars a stop entry of the other side is placed while the book is still open, with NO same-bar strategy.close (a plain reversal) +// The stop sits 5c on the close's marketable side (the close has reached it). +// The first flip opens a short. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("PA2 I3 rev-s-mag", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=1, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=true) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +units = strategy.position_size +t = math.floor(close * 100 + 0.000001) +isDown = k % 6 == 2 +isUp = k % 6 == 5 +if k >= 0 and time < tEnd + if isDown + strategy.entry("SE", strategy.short, stop=(t + 5) / 100) + strategy.cancel("LE") + if isUp + strategy.entry("LE", strategy.long, stop=(t - 5) / 100) + strategy.cancel("SE") +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/tv_trades.csv b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/tv_trades.csv new file mode 100644 index 00000000..1a7cb92b --- /dev/null +++ b/tests/fixtures/pooc_reversing_stop/pa2-i3-rev-s-mag/tv_trades.csv @@ -0,0 +1,45 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-02 22:45,LE,11.51,100,1145,-6.2296,-0.54,0.2296,1.8855,0.16,-7.1145,-0.62,-6.2296,-0.01,3 +1,Entry short,2025-07-02 22:00,SE,11.45,100,1145,-6.2296,-0.54,0.2296,1.8855,0.16,-7.1145,-0.62,-6.2296,-0.01,3 +2,Exit long,2025-07-02 23:30,SE,11.66,100,1151,14.7683,1.28,0.2317,18.8849,1.64,-1.1151,-0.10,8.5387,0.01,3 +2,Entry long,2025-07-02 22:45,LE,11.51,100,1151,14.7683,1.28,0.2317,18.8849,1.64,-1.1151,-0.10,8.5387,0.01,3 +3,Exit short,2025-07-03 00:15,LE,11.66,100,1166,-0.2332,-0.02,0.2332,2.8834,0.25,-2.1166,-0.18,8.3055,0.01,3 +3,Entry short,2025-07-02 23:30,SE,11.66,100,1166,-0.2332,-0.02,0.2332,2.8834,0.25,-2.1166,-0.18,8.3055,0.01,3 +4,Exit long,2025-07-03 01:00,SE,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-2.1166,-0.18,8.0723,0.01,3 +4,Entry long,2025-07-03 00:15,LE,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-2.1166,-0.18,8.0723,0.01,3 +5,Exit short,2025-07-03 01:45,LE,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-3.1166,-0.27,7.8391,0.01,3 +5,Entry short,2025-07-03 01:00,SE,11.66,100,1166,-0.2332,-0.02,0.2332,1.8834,0.16,-3.1166,-0.27,7.8391,0.01,3 +6,Exit long,2025-07-03 02:30,SE,11.73,100,1166,6.7661,0.58,0.2339,6.8834,0.59,-2.1166,-0.18,14.6052,0.01,3 +6,Entry long,2025-07-03 01:45,LE,11.66,100,1166,6.7661,0.58,0.2339,6.8834,0.59,-2.1166,-0.18,14.6052,0.01,3 +7,Exit short,2025-07-03 03:15,LE,11.75,100,1173,-2.2348,-0.19,0.2348,0.8827,0.08,-5.1173,-0.44,12.3704,0.01,3 +7,Entry short,2025-07-03 02:30,SE,11.73,100,1173,-2.2348,-0.19,0.2348,0.8827,0.08,-5.1173,-0.44,12.3704,0.01,3 +8,Exit long,2025-07-03 21:30,SE,11.94,100,1175,18.7631,1.60,0.2369,18.8825,1.61,-3.1175,-0.27,31.1335,0.03,3 +8,Entry long,2025-07-03 03:15,LE,11.75,100,1175,18.7631,1.60,0.2369,18.8825,1.61,-3.1175,-0.27,31.1335,0.03,3 +9,Exit short,2025-07-03 22:15,LE,11.84,100,1194,9.7622,0.82,0.2378,10.8806,0.91,-2.1194,-0.18,40.8957,0.04,3 +9,Entry short,2025-07-03 21:30,SE,11.94,100,1194,9.7622,0.82,0.2378,10.8806,0.91,-2.1194,-0.18,40.8957,0.04,3 +10,Exit long,2025-07-03 23:00,SE,11.82,100,1184,-2.2366,-0.19,0.2366,2.8816,0.24,-4.1184,-0.35,38.6591,0.04,3 +10,Entry long,2025-07-03 22:15,LE,11.84,100,1184,-2.2366,-0.19,0.2366,2.8816,0.24,-4.1184,-0.35,38.6591,0.04,3 +11,Exit short,2025-07-03 23:45,LE,11.84,100,1182,-2.2366,-0.19,0.2366,0.8818,0.07,-6.1182,-0.52,36.4225,0.04,3 +11,Entry short,2025-07-03 23:00,SE,11.82,100,1182,-2.2366,-0.19,0.2366,0.8818,0.07,-6.1182,-0.52,36.4225,0.04,3 +12,Exit long,2025-07-04 00:30,SE,11.81,100,1184,-3.2365,-0.27,0.2365,0.8816,0.07,-5.1184,-0.43,33.186,0.03,3 +12,Entry long,2025-07-03 23:45,LE,11.84,100,1184,-3.2365,-0.27,0.2365,0.8816,0.07,-5.1184,-0.43,33.186,0.03,3 +13,Exit short,2025-07-07 21:45,LE,11.72,100,1181,8.7647,0.74,0.2353,19.8819,1.68,-3.1181,-0.26,41.9507,0.04,3 +13,Entry short,2025-07-04 00:30,SE,11.81,100,1181,8.7647,0.74,0.2353,19.8819,1.68,-3.1181,-0.26,41.9507,0.04,3 +14,Exit long,2025-07-07 22:30,SE,11.7,100,1172,-2.2342,-0.19,0.2342,6.8828,0.59,-5.1172,-0.44,39.7165,0.04,3 +14,Entry long,2025-07-07 21:45,LE,11.72,100,1172,-2.2342,-0.19,0.2342,6.8828,0.59,-5.1172,-0.44,39.7165,0.04,3 +15,Exit short,2025-07-07 23:15,LE,11.62,100,1170,7.7668,0.66,0.2332,8.883,0.76,-1.117,-0.10,47.4833,0.05,3 +15,Entry short,2025-07-07 22:30,SE,11.7,100,1170,7.7668,0.66,0.2332,8.883,0.76,-1.117,-0.10,47.4833,0.05,3 +16,Exit long,2025-07-08 00:00,SE,11.57,100,1162,-5.2319,-0.45,0.2319,1.8838,0.16,-6.1162,-0.53,42.2514,0.04,3 +16,Entry long,2025-07-07 23:15,LE,11.62,100,1162,-5.2319,-0.45,0.2319,1.8838,0.16,-6.1162,-0.53,42.2514,0.04,3 +17,Exit short,2025-07-08 00:45,LE,11.54,100,1157,2.7689,0.24,0.2311,7.8843,0.68,-1.1157,-0.10,45.0203,0.05,3 +17,Entry short,2025-07-08 00:00,SE,11.57,100,1157,2.7689,0.24,0.2311,7.8843,0.68,-1.1157,-0.10,45.0203,0.05,3 +18,Exit long,2025-07-08 01:30,SE,11.58,100,1154,3.7688,0.33,0.2312,6.8846,0.60,-0.1154,-0.01,48.7891,0.05,3 +18,Entry long,2025-07-08 00:45,LE,11.54,100,1154,3.7688,0.33,0.2312,6.8846,0.60,-0.1154,-0.01,48.7891,0.05,3 +19,Exit short,2025-07-08 02:15,LE,11.52,100,1158,5.769,0.50,0.231,7.8842,0.68,-0.1158,-0.01,54.5581,0.05,3 +19,Entry short,2025-07-08 01:30,SE,11.58,100,1158,5.769,0.50,0.231,7.8842,0.68,-0.1158,-0.01,54.5581,0.05,3 +20,Exit long,2025-07-08 03:00,SE,11.59,100,1152,6.7689,0.59,0.2311,7.8848,0.68,-1.1152,-0.10,61.327,0.06,3 +20,Entry long,2025-07-08 02:15,LE,11.52,100,1152,6.7689,0.59,0.2311,7.8848,0.68,-1.1152,-0.10,61.327,0.06,3 +21,Exit short,2025-07-08 03:45,LE,11.6,100,1159,-1.2319,-0.11,0.2319,3.8841,0.34,-2.1159,-0.18,60.0951,0.06,3 +21,Entry short,2025-07-08 03:00,SE,11.59,100,1159,-1.2319,-0.11,0.2319,3.8841,0.34,-2.1159,-0.18,60.0951,0.06,3 +22,Exit long,2025-07-08 03:45,,11.6,100,1160,-0.232,-0.02,0.116,0,0.00,-0.116,-0.01,59.7471,0.06,0 +22,Entry long,2025-07-08 03:45,LE,11.6,100,1160,-0.232,-0.02,0.116,0,0.00,-0.116,-0.01,59.7471,0.06,0 \ No newline at end of file diff --git a/tests/test_pooc_reversing_stop_tapes.cpp b/tests/test_pooc_reversing_stop_tapes.cpp new file mode 100644 index 00000000..2d6f8de8 --- /dev/null +++ b/tests/test_pooc_reversing_stop_tapes.cpp @@ -0,0 +1,313 @@ +/* + * test_pooc_reversing_stop_tapes.cpp -- R5 lane PAR-ORDERS-2 (item 3). + * + * Under process_orders_on_close, a stop entry placed while the book holds the + * other side, whose stop the placing close has already reached, fills at that + * close: after a same-bar strategy.close of the held side (corpus probe 96's + * flip), and as a plain reversal when no close precedes it. R5 lane PAR-ORDERS + * (H-MEASURE Finding 6b) filled only such an entry placed on a FLAT book there; + * this one waited for the next open, a bar late on every row. + * + * The lane's own synthetic script, probe 96's flip on bar counts, is taped on + * NYSE:F 15m over the three days of tests/fixtures/session_islastbar (`lab tv`, + * ws-report-v1, rangeProof covered; tests/fixtures/pooc_reversing_stop, whose + * README names each tape), and replayed here on the 15m chart and on the 1m + * bars aggregated to 15m under the bar magnifier: + * pa2-i3-flip-s-* every six bars a reached stop entry of the other side and + * a same-bar strategy.close of the held id; the first flip + * opens a short. The entry's quantity grows by the side it + * was placed against (100, 200, 300, ...), as in probe 96. + * pa2-i3-flip-l-* the same, the first flip opening a long. + * pa2-i3-rev-s-* the reversal alone: no same-bar close. + * pa2-i3-flip-unreached-s-* the stops 5c beyond the close: resting, filled + * later on the bar they are reached -- the control the rule + * must leave alone (it matched before and after). + * Every closed row is compared field by field -- side, instants, prices, + * quantity, signals, bar indices -- and TradingView's open trade at the end of + * the data (its exit signal empty) against the engine's open position. + * + * Fail-before, this TU against the lane's previous commit (128 of 178 checks + * fail): every flip and reversal entry but the first books at the next open + * (bar j+1, the open's price) where TradingView books the signal bar's close + * -- 20 of 21 closed rows on each flip tape and every row of each reversal + * tape (20 against TradingView's 21) -- and the book the data ends on is not + * TradingView's (flat against 2200 on the flip tapes, short 100 against long + * 100 on the reversal tapes). The unreached controls match either way. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; + +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +#ifndef PINEFORGE_PA2_REVERSING_STOP_FIXTURE_DIR +#error "PINEFORGE_PA2_REVERSING_STOP_FIXTURE_DIR must name tests/fixtures/pooc_reversing_stop" +#endif + +namespace { + +struct FeedBar { + std::int64_t ts; + double open, high, low, close, volume; +}; + +#include "fixtures/session_islastbar/bars.inc" + +constexpr std::int64_t kMinute = 60'000; +const double kNaN = std::numeric_limits::quiet_NaN(); + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +// "YYYY-MM-DD HH:MM" in the tape's UTC+8 -> UTC milliseconds. +std::int64_t tape_ms(const std::string& text) { + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + if (std::sscanf(text.c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) return -1; + const std::int64_t days = days_from_civil(y, static_cast(mo), + static_cast(d)); + return ((days * 24 + h - 8) * 60 + mi) * kMinute; +} + +struct TapeTrade { + bool is_long = true; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = 0.0, exit_price = 0.0, qty = 0.0; + std::string entry_signal, exit_signal; +}; + +std::vector read_tape(const std::string& slug) { + std::ifstream in(std::string(PINEFORGE_PA2_REVERSING_STOP_FIXTURE_DIR) + "/" + slug + + "/tv_trades.csv"); + std::vector trades; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 17) continue; + const std::size_t n = static_cast(std::stoi(cell[0])); + if (trades.size() < n) trades.resize(n); + TapeTrade& t = trades[n - 1]; + const bool entry = cell[1].rfind("Entry", 0) == 0; + t.is_long = cell[1].find("long") != std::string::npos; + (entry ? t.entry_ms : t.exit_ms) = tape_ms(cell[2]); + (entry ? t.entry_signal : t.exit_signal) = cell[3]; + (entry ? t.entry_price : t.exit_price) = std::stod(cell[4]); + t.qty = std::stod(cell[5]); + } + return trades; +} + +// floor(x * 100 + 1e-6): the script's cent grid (NYSE:F mintick 0.01). +double cents(double x) { return std::floor(x * 100.0 + 1e-6); } + +struct Variant { + const char* slug; + bool reached; // the stop sits on the close's marketable side + bool same_bar_close; + bool long_first; + bool magnifier; +}; + +// The Pine probe's body (tests/fixtures/pooc_reversing_stop//strategy.pine). +class FlipHost final : public source::PineStrategyHost { +public: + explicit FlipHost(const Variant& v) : v_(v) { + set_syminfo_session("0930-1600"); + set_syminfo_timezone("America/New_York"); + source::PineStrategyConfig c; + c.initial_capital = 100000; + c.default_qty_type = static_cast(QtyType::FIXED); + c.default_qty_value = 100; + c.pyramiding = 1; + c.process_orders_on_close = true; + c.calc_on_order_fills = false; + c.slippage = 0; + c.commission_type = static_cast(CommissionType::PERCENT); + c.commission_value = 0.01; + c.margin_long = 100; + c.margin_short = 100; + configure_pine_strategy(c); + syminfo_mintick_ = 0.01; + } + void on_source_bar(const Bar& b) override { + const int k = bar_index_; // the replay's first bar is the Pine counter's origin + const double units = signed_position_size(); + const int down = v_.long_first ? 5 : 2; + const int up = v_.long_first ? 2 : 5; + const bool is_down = k % 6 == down; + const bool is_up = k % 6 == up; + const double t = cents(b.close); + if (is_down) { + strategy_entry("SE", false, kNaN, (v_.reached ? t + 5.0 : t - 5.0) / 100.0); + strategy_cancel("LE"); + } + if (is_up) { + strategy_entry("LE", true, kNaN, (v_.reached ? t - 5.0 : t + 5.0) / 100.0); + strategy_cancel("SE"); + } + if (v_.same_bar_close && is_down && units > 0.0) strategy_close("LE"); + if (v_.same_bar_close && is_up && units < 0.0) strategy_close("SE"); + } + + struct Row { + bool is_long = true; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = 0.0, exit_price = 0.0, qty = 0.0; + int entry_bar = 0, exit_bar = 0; + std::string entry_id, exit_id; + }; + std::vector rows() const { + std::vector out; + for (int i = 0; i < trade_count(); ++i) { + Row r; + r.is_long = trades_[static_cast(i)].is_long; + r.entry_ms = closed_trade_entry_time(i); + r.exit_ms = closed_trade_exit_time(i); + r.entry_price = closed_trade_entry_price(i); + r.exit_price = closed_trade_exit_price(i); + r.qty = closed_trade_size(i); + r.entry_bar = closed_trade_entry_bar_index(i); + r.exit_bar = closed_trade_exit_bar_index(i); + r.entry_id = closed_trade_entry_id(i); + r.exit_id = closed_trade_exit_id(i); + out.push_back(r); + } + return out; + } + double open_units() const { return physical_position().signed_units; } + +private: + Variant v_; +}; + +template +std::vector feed(const FeedBar (&rows)[N]) { + std::vector bars; + for (const FeedBar& row : rows) { + Bar b{}; + b.timestamp = row.ts; + b.open = row.open; b.high = row.high; b.low = row.low; b.close = row.close; + b.volume = row.volume; + bars.push_back(b); + } + return bars; +} + +// The chart bar (fixture order) whose open is `ms`; -1 when none is. +int chart_bar_of(std::int64_t ms) { + for (std::size_t i = 0; i < sizeof(kFord15) / sizeof(kFord15[0]); ++i) + if (kFord15[i].ts == ms) return static_cast(i); + return -1; +} + +// TradingView's exit signal for strategy.close("X") is "Close entry(s) order X". +std::string exit_signal(const FlipHost::Row& r) { + if (r.exit_id.rfind("__close__", 0) == 0 && r.exit_id.size() > 9) + return "Close entry(s) order " + r.exit_id.substr(9); + return r.exit_id; +} + +bool near(double a, double b) { return std::fabs(a - b) <= 1e-9; } + +void replay(const Variant& v) { + std::printf("-- %s [%s]\n", v.slug, v.magnifier ? "aggregated 1 -> 15, magnifier" + : "chart 15 -> 15"); + static const std::vector fifteen = feed(kFord15); + static const std::vector one = feed(kFord1m); + FlipHost host(v); + if (v.magnifier) { + host.run(one.data(), static_cast(one.size()), "1", "15", true); + } else { + host.run(fifteen.data(), static_cast(fifteen.size()), "15", "15", false); + } + CHECK(host.last_error().empty()); + std::vector closed; + const TapeTrade* open_at_end = nullptr; + const auto tape = read_tape(v.slug); + for (const TapeTrade& t : tape) { + if (t.exit_signal.empty()) open_at_end = &t; + else closed.push_back(t); + } + CHECK(!closed.empty()); + const auto rows = host.rows(); + CHECK(rows.size() == closed.size()); + int differing = 0; + for (std::size_t i = 0; i < rows.size() && i < closed.size(); ++i) { + const TapeTrade& tv = closed[i]; + const FlipHost::Row& r = rows[i]; + const bool ok = r.is_long == tv.is_long && r.entry_ms == tv.entry_ms + && r.exit_ms == tv.exit_ms && near(r.entry_price, tv.entry_price) + && near(r.exit_price, tv.exit_price) && r.qty == tv.qty + && r.entry_id == tv.entry_signal && exit_signal(r) == tv.exit_signal + && r.entry_bar == chart_bar_of(tv.entry_ms) && r.exit_bar == chart_bar_of(tv.exit_ms); + CHECK(ok); + if (!ok) { + ++differing; + std::printf(" row %zu: TradingView %s %s bar %d @%.2f -> bar %d @%.2f %s q%g\n" + " engine %s %s bar %d @%.2f -> bar %d @%.2f %s q%g\n", + i + 1, tv.entry_signal.c_str(), tv.is_long ? "L" : "S", + chart_bar_of(tv.entry_ms), tv.entry_price, chart_bar_of(tv.exit_ms), + tv.exit_price, tv.exit_signal.c_str(), tv.qty, r.entry_id.c_str(), + r.is_long ? "L" : "S", r.entry_bar, r.entry_price, r.exit_bar, + r.exit_price, exit_signal(r).c_str(), r.qty); + } + } + // TradingView's open trade at the end of the data is the engine's open + // position: same side and quantity. + const double tv_open = open_at_end + ? (open_at_end->is_long ? open_at_end->qty : -open_at_end->qty) : 0.0; + CHECK(host.open_units() == tv_open); + std::printf(" %zu closed rows, %d differ; open at the end %g (TradingView %g)\n", + rows.size(), differing, host.open_units(), tv_open); +} + +} // namespace + +int main() { + const Variant variants[] = { + {"pa2-i3-flip-s-chart", true, true, false, false}, + {"pa2-i3-flip-s-mag", true, true, false, true}, + {"pa2-i3-flip-l-chart", true, true, true, false}, + {"pa2-i3-flip-l-mag", true, true, true, true}, + {"pa2-i3-rev-s-chart", true, false, false, false}, + {"pa2-i3-rev-s-mag", true, false, false, true}, + {"pa2-i3-flip-unreached-s-chart", false, true, false, false}, + {"pa2-i3-flip-unreached-s-mag", false, true, false, true}, + }; + for (const Variant& v : variants) replay(v); + std::printf("\n%s process_orders_on_close reversing stop tapes: %d checks, %d failures\n", + tests_failed == 0 ? "PASS" : "FAIL", tests_passed + tests_failed, tests_failed); + return tests_failed == 0 ? 0 : 1; +} From 48832b62012dc0cc0f056c7c0039ce42bd1dc7bc Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 12:11:40 +0800 Subject: [PATCH 37/83] PAR-ORDERS-2: the trades and transcripts its fixes move, pinned (INT26: the behaviour half of 9f7a025c) R5 lane PAR-ORDERS-2's own re-pin, 9f7a025c ("PAR-ORDERS-2 v19 hash re-pin"), moves hash values and behaviour together. INT26 folds its hash values into the single INT26 v19 hash re-pin (the late-picks list), whose tool refuses any non-hash change; so the behaviour it pins lands here, with the fixes that move it. Harvested on this tree with each TU's own switch (PINEFORGE_QUIET_BAR_HARVEST, PINEFORGE_D2D_PUBLICATION_HARVEST, PINEFORGE_V19E_HARVEST, PINEFORGE_P1_HARVEST) and compared field by field with the pins before the five picks, the lane's 9f7a025c and its base 6945fc19: every non-hash field that moved equals the lane's value, and none moved that the lane did not move. - test_adapter_quiet_bar: the trade digest of CalcOnFills/0/*, Random10/0/* and Random22/0/*, and Random10/0/*'s boundary count 70 -> 69 and trade count 23 -> 22 (item 2's rule; CalcOnFills only renumbers Trade::entry_incarnation). The list sits in the pinned file. - test_adapter_live_state_equivalence: 16 of 108 transcript digests (the transcript holds no hash value): reversals seed 3246599 completes (135 trades and "native current evaluated allowance mismatch" -> 156 trades, item 1), item 2 moves 9 and item 3's close pass 7, as the lane's message attributes them; the list and the header's 3246599 sentence are the lane's. - test_publication_witness and test_adapter_continuation_view: hash values only (the new coof_fill_forced_ fold); nothing lands here. Until the INT26 v19 hash re-pin, test_adapter_quiet_bar, test_publication_witness and test_adapter_continuation_view fail on those hash values only; every other witness row passes on this tree. (behaviour half of commit 9f7a025cb790d3cbd1ad3f9ddd94a9a0f4a2f21f) Co-Authored-By: Claude Opus 5.5 (1M context) --- ...st_adapter_live_state_equivalence_data.hpp | 59 +++++++++++++------ tests/test_adapter_quiet_bar_pinned.inc | 23 ++++++-- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/tests/test_adapter_live_state_equivalence_data.hpp b/tests/test_adapter_live_state_equivalence_data.hpp index 7831cf5e..e3d983e1 100644 --- a/tests/test_adapter_live_state_equivalence_data.hpp +++ b/tests/test_adapter_live_state_equivalence_data.hpp @@ -3,9 +3,11 @@ // retained, pineforge-source-adapter/v3) by that TU compiled with // -DPINEFORGE_V19E_HARVEST. Generated data: rebuild it the same way, never by // hand. Included inside that TU's anonymous namespace. One configuration -// (reversals seed 3246599) ends in the kernel's own "native current evaluated -// allowance mismatch" failure on the base too; the failure is part of what -// its transcript pins. +// (reversals seed 3246599) ended in the kernel's own "native current evaluated +// allowance mismatch" failure on the base too, and the failure was part of +// what its transcript pinned, until R5 lane PAR-ORDERS-2: the kernel now +// refuses the current execution that failed (a request bound to a host +// roster), and the run completes. // // R5 lane V19-D re-pinned every value once, on its tree: the adapter re-issues // with a carried binding (ReplaceOptions::keep_binding), so the kernel records @@ -21,46 +23,65 @@ // R5 lane PAR-ORDERS re-harvested it the same way on its tree (29 of 108 digests // move; see the lane's hash commit for why each moves). +// R5 lane PAR-ORDERS-2 moves 16 of 108 digests (INT26: the behaviour half of +// the lane's hash commit 9f7a025c, which says why each moves; harvested the +// same way on the integrated tree, every value equal to the lane's): +// reversals seed 104729: 8320704943709246891 -> 17062111550189209216 (131 trades, 397 commands, 426 rows placed -> 131 trades, 397 commands, 431 rows placed) +// reversals seed 314187: 9660742953292018077 -> 4920319354091947109 (208 trades, 684 commands, 653 rows placed -> 208 trades, 684 commands, 651 rows placed) +// reversals seed 628374: 1718744356722272312 -> 12651613190033426865 (138 trades, 410 commands, 393 rows placed -> 138 trades, 410 commands, 399 rows placed) +// reversals seed 1152019: 6104491581818825501 -> 4811666956682998059 (145 trades, 386 commands, 411 rows placed -> 151 trades, 397 commands, 409 rows placed) +// reversals seed 1675664: 9695271777348687666 -> 15023244533711012002 (125 trades, 398 commands, 400 rows placed -> 124 trades, 398 commands, 405 rows placed) +// reversals seed 1780393: 3952138674949420146 -> 3425020382181293193 (197 trades, 704 commands, 645 rows placed -> 196 trades, 702 commands, 629 rows placed) +// reversals seed 2199309: 12243088919143375019 -> 12203651668804466516 (126 trades, 398 commands, 370 rows placed -> 127 trades, 398 commands, 375 rows placed) +// reversals seed 2722954: 1694449538189143220 -> 4868757197565921069 (150 trades, 390 commands, 419 rows placed -> 150 trades, 390 commands, 428 rows placed) +// reversals seed 3246599: 15380097802441451398 -> 14261337452491704800 (135 trades, 415 commands, 422 rows placed, error: native current evaluated allowance mismatch -> 156 trades, 469 commands, 484 rows placed) +// reversals seed 3770244: 17742628010123667157 -> 16581004868667265691 (53 trades, 339 commands, 239 rows placed -> 53 trades, 339 commands, 245 rows placed) +// brackets seed 314188: 10835407349126156117 -> 11664346520909115979 (147 trades, 866 commands, 1036 rows placed -> 145 trades, 850 commands, 981 rows placed) +// brackets seed 1780394: 11565075807030336076 -> 1550055039614354387 (95 trades, 697 commands, 696 rows placed -> 95 trades, 697 commands, 705 rows placed) +// brackets seed 3246600: 8662530819297017027 -> 2136070886124345893 (132 trades, 700 commands, 868 rows placed -> 115 trades, 657 commands, 847 rows placed) +// chains seed 314189: 10134000431367652603 -> 5640170146031748940 (107 trades, 526 commands, 444 rows placed -> 107 trades, 526 commands, 454 rows placed) +// chains seed 1780395: 632278533107602324 -> 11993708214650334332 (82 trades, 480 commands, 426 rows placed -> 82 trades, 480 commands, 436 rows placed) +// chains seed 3246601: 17544117140908072910 -> 17828078233634427072 (98 trades, 485 commands, 468 rows placed -> 98 trades, 485 commands, 466 rows placed) constexpr std::uint64_t kTranscriptDigests[] = { - 8320704943709246891ull, // reversals seed 104729, 131 trades, 397 commands, 426 rows placed + 17062111550189209216ull, // reversals seed 104729, 131 trades, 397 commands, 431 rows placed 3937583110602879389ull, // reversals seed 209458, 126 trades, 382 commands, 409 rows placed - 9660742953292018077ull, // reversals seed 314187, 208 trades, 684 commands, 653 rows placed + 4920319354091947109ull, // reversals seed 314187, 208 trades, 684 commands, 651 rows placed 14691229822702022375ull, // reversals seed 418916, 105 trades, 378 commands, 361 rows placed 201369272286234124ull, // reversals seed 523645, 104 trades, 417 commands, 357 rows placed - 1718744356722272312ull, // reversals seed 628374, 138 trades, 410 commands, 393 rows placed + 12651613190033426865ull, // reversals seed 628374, 138 trades, 410 commands, 399 rows placed 10868119957001212051ull, // reversals seed 733103, 139 trades, 386 commands, 404 rows placed 15494092140752158088ull, // reversals seed 837832, 91 trades, 401 commands, 332 rows placed 5702736951683229543ull, // reversals seed 942561, 98 trades, 408 commands, 356 rows placed 10200415260680638236ull, // reversals seed 1047290, 134 trades, 402 commands, 393 rows placed - 6104491581818825501ull, // reversals seed 1152019, 145 trades, 386 commands, 411 rows placed + 4811666956682998059ull, // reversals seed 1152019, 151 trades, 397 commands, 409 rows placed 9826242374101611623ull, // reversals seed 1256748, 118 trades, 402 commands, 362 rows placed 4883135135199369800ull, // reversals seed 1361477, 105 trades, 413 commands, 349 rows placed 6980972530369447172ull, // reversals seed 1466206, 151 trades, 372 commands, 393 rows placed 419860461863827083ull, // reversals seed 1570935, 77 trades, 357 commands, 297 rows placed - 9695271777348687666ull, // reversals seed 1675664, 125 trades, 398 commands, 400 rows placed - 3952138674949420146ull, // reversals seed 1780393, 197 trades, 704 commands, 645 rows placed + 15023244533711012002ull, // reversals seed 1675664, 124 trades, 398 commands, 405 rows placed + 3425020382181293193ull, // reversals seed 1780393, 196 trades, 702 commands, 629 rows placed 4513324373166417315ull, // reversals seed 1885122, 113 trades, 393 commands, 388 rows placed 18393465585117555058ull, // reversals seed 1989851, 180 trades, 430 commands, 448 rows placed 6120235323624511436ull, // reversals seed 2094580, 92 trades, 384 commands, 340 rows placed - 12243088919143375019ull, // reversals seed 2199309, 126 trades, 398 commands, 370 rows placed + 12203651668804466516ull, // reversals seed 2199309, 127 trades, 398 commands, 375 rows placed 7979846979225294549ull, // reversals seed 2304038, 149 trades, 412 commands, 418 rows placed 16092912242308174488ull, // reversals seed 2408767, 139 trades, 415 commands, 404 rows placed 1517737495207721277ull, // reversals seed 2513496, 110 trades, 390 commands, 394 rows placed 1610718425964291349ull, // reversals seed 2618225, 105 trades, 411 commands, 371 rows placed - 1694449538189143220ull, // reversals seed 2722954, 150 trades, 390 commands, 419 rows placed + 4868757197565921069ull, // reversals seed 2722954, 150 trades, 390 commands, 428 rows placed 8640229404818843386ull, // reversals seed 2827683, 72 trades, 339 commands, 277 rows placed 8135938306046315211ull, // reversals seed 2932412, 96 trades, 360 commands, 318 rows placed 9738428349677663372ull, // reversals seed 3037141, 114 trades, 405 commands, 377 rows placed 2670083654842814989ull, // reversals seed 3141870, 66 trades, 332 commands, 248 rows placed - 15380097802441451398ull, // reversals seed 3246599, 135 trades, 415 commands, 422 rows placed, error: native current evaluated allowance mismatch + 14261337452491704800ull, // reversals seed 3246599, 156 trades, 469 commands, 484 rows placed 14583498249706098574ull, // reversals seed 3351328, 108 trades, 414 commands, 345 rows placed 13691690510990974075ull, // reversals seed 3456057, 88 trades, 373 commands, 334 rows placed 14092552094636276530ull, // reversals seed 3560786, 147 trades, 413 commands, 444 rows placed 4203447471686931733ull, // reversals seed 3665515, 133 trades, 394 commands, 397 rows placed - 17742628010123667157ull, // reversals seed 3770244, 53 trades, 339 commands, 239 rows placed + 16581004868667265691ull, // reversals seed 3770244, 53 trades, 339 commands, 245 rows placed 5125743962394059998ull, // brackets seed 104730, 82 trades, 521 commands, 568 rows placed 3732927941814356572ull, // brackets seed 209459, 77 trades, 487 commands, 551 rows placed - 10835407349126156117ull, // brackets seed 314188, 147 trades, 866 commands, 1036 rows placed + 11664346520909115979ull, // brackets seed 314188, 145 trades, 850 commands, 981 rows placed 3737662390130165609ull, // brackets seed 418917, 94 trades, 557 commands, 632 rows placed 12564349532258145101ull, // brackets seed 523646, 71 trades, 517 commands, 451 rows placed 16592088247586231707ull, // brackets seed 628375, 100 trades, 536 commands, 662 rows placed @@ -74,7 +95,7 @@ constexpr std::uint64_t kTranscriptDigests[] = { 6438015927310529334ull, // brackets seed 1466207, 72 trades, 568 commands, 577 rows placed 17986613720295818227ull, // brackets seed 1570936, 69 trades, 475 commands, 535 rows placed 15751662755974333447ull, // brackets seed 1675665, 87 trades, 520 commands, 602 rows placed - 11565075807030336076ull, // brackets seed 1780394, 95 trades, 697 commands, 696 rows placed + 1550055039614354387ull, // brackets seed 1780394, 95 trades, 697 commands, 705 rows placed 2015779222221407240ull, // brackets seed 1885123, 90 trades, 542 commands, 587 rows placed 1872777722679096721ull, // brackets seed 1989852, 88 trades, 514 commands, 572 rows placed 11372035822747614056ull, // brackets seed 2094581, 85 trades, 496 commands, 575 rows placed @@ -88,7 +109,7 @@ constexpr std::uint64_t kTranscriptDigests[] = { 4716911134077760843ull, // brackets seed 2932413, 75 trades, 469 commands, 515 rows placed 6708847418396898846ull, // brackets seed 3037142, 72 trades, 476 commands, 521 rows placed 7906131650209684341ull, // brackets seed 3141871, 101 trades, 537 commands, 653 rows placed - 8662530819297017027ull, // brackets seed 3246600, 132 trades, 700 commands, 868 rows placed + 2136070886124345893ull, // brackets seed 3246600, 115 trades, 657 commands, 847 rows placed 14331750725646803575ull, // brackets seed 3351329, 85 trades, 504 commands, 520 rows placed 7444517548780988427ull, // brackets seed 3456058, 80 trades, 530 commands, 529 rows placed 6178143291077411923ull, // brackets seed 3560787, 107 trades, 521 commands, 634 rows placed @@ -96,7 +117,7 @@ constexpr std::uint64_t kTranscriptDigests[] = { 5122510577651000745ull, // brackets seed 3770245, 78 trades, 503 commands, 560 rows placed 11244999202114714137ull, // chains seed 104731, 59 trades, 301 commands, 286 rows placed 11413128619516782387ull, // chains seed 209460, 85 trades, 390 commands, 386 rows placed - 10134000431367652603ull, // chains seed 314189, 107 trades, 526 commands, 444 rows placed + 5640170146031748940ull, // chains seed 314189, 107 trades, 526 commands, 454 rows placed 3121930092179333450ull, // chains seed 418918, 45 trades, 312 commands, 299 rows placed 165784926829741990ull, // chains seed 523647, 61 trades, 322 commands, 329 rows placed 10122741367483366791ull, // chains seed 628376, 90 trades, 358 commands, 415 rows placed @@ -110,7 +131,7 @@ constexpr std::uint64_t kTranscriptDigests[] = { 9973593188851025006ull, // chains seed 1466208, 73 trades, 382 commands, 338 rows placed 15204985627708005050ull, // chains seed 1570937, 90 trades, 366 commands, 370 rows placed 4161494051921551918ull, // chains seed 1675666, 64 trades, 321 commands, 339 rows placed - 632278533107602324ull, // chains seed 1780395, 82 trades, 480 commands, 426 rows placed + 11993708214650334332ull, // chains seed 1780395, 82 trades, 480 commands, 436 rows placed 8033768331525115464ull, // chains seed 1885124, 66 trades, 360 commands, 304 rows placed 8033691744657508662ull, // chains seed 1989853, 73 trades, 353 commands, 373 rows placed 10675979410645833515ull, // chains seed 2094582, 75 trades, 321 commands, 355 rows placed @@ -124,7 +145,7 @@ constexpr std::uint64_t kTranscriptDigests[] = { 6451466112212038981ull, // chains seed 2932414, 65 trades, 350 commands, 330 rows placed 6296605148496367564ull, // chains seed 3037143, 81 trades, 324 commands, 328 rows placed 18120582394174365029ull, // chains seed 3141872, 82 trades, 355 commands, 349 rows placed - 17544117140908072910ull, // chains seed 3246601, 98 trades, 485 commands, 468 rows placed + 17828078233634427072ull, // chains seed 3246601, 98 trades, 485 commands, 466 rows placed 9531902743311875506ull, // chains seed 3351330, 60 trades, 311 commands, 310 rows placed 1817828500136052373ull, // chains seed 3456059, 59 trades, 292 commands, 289 rows placed 5397962162622300398ull, // chains seed 3560788, 73 trades, 359 commands, 337 rows placed diff --git a/tests/test_adapter_quiet_bar_pinned.inc b/tests/test_adapter_quiet_bar_pinned.inc index 5b387fc3..45ea5734 100644 --- a/tests/test_adapter_quiet_bar_pinned.inc +++ b/tests/test_adapter_quiet_bar_pinned.inc @@ -962,6 +962,17 @@ // Random43/1/0: boundary 0xb9a953e47994593f -> 0xffd1c55ce888ef72, final_hash 0xb6e20a498ce1b25a -> 0x45393018e93b32be // Random45/1/1: boundary 0x81ba9360ff25c670 -> 0x32a1d85e85b93f0a, rows 0xe345a7e76585b1de -> 0xebcfcdf78cd848a5, final_hash 0x6c13a376f0a1a840 -> 0x56b0e295081820c0 // Random45/1/0: boundary 0x81ba9360ff25c670 -> 0x32a1d85e85b93f0a, final_hash 0x6c13a376f0a1a840 -> 0x56b0e295081820c0 +// R5 lane PAR-ORDERS-2 moves these runs' trades (INT26: the behaviour half of +// the lane's hash commit 9f7a025c, which says why each moves; harvested the +// same way on the integrated tree, every value equal to the lane's). Their +// hash values, and those of the other runs the lane's new fold moves, move +// once, in the INT26 v19 hash re-pin: +// CalcOnFills/0/1: trades 0x4603f8c446ca5c1e -> 0x0b9be2eee5282e84 +// CalcOnFills/0/0: trades 0x4603f8c446ca5c1e -> 0x0b9be2eee5282e84 +// Random10/0/1: trades 0x274b98362186a62f -> 0xe335d30ff30abed4, boundary_count 70 -> 69, trade_count 23 -> 22 +// Random10/0/0: trades 0x274b98362186a62f -> 0xe335d30ff30abed4, boundary_count 70 -> 69, trade_count 23 -> 22 +// Random22/0/1: trades 0x603c10590ec68778 -> 0x87be32daa84c6426 +// Random22/0/0: trades 0x603c10590ec68778 -> 0x87be32daa84c6426 constexpr Pinned kPinned[] = { {"Quiet", 0, 1, 0x1c2b2e37a44f6877ull, 0x81799b58f0e052b1ull, 0x47fe0d7eaf8e51e3ull, 0xe0aa4c0240939bcfull, 40, 40, 0}, {"Quiet", 0, 0, 0x1c2b2e37a44f6877ull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0xe0aa4c0240939bcfull, 40, 0, 0}, @@ -1007,8 +1018,8 @@ constexpr Pinned kPinned[] = { {"DeclinedReversal", 0, 0, 0xfcb9145b39ec7b5aull, 0x14650fb0739d0383ull, 0xc3448bef59024d51ull, 0x764450acb0596bfcull, 24, 0, 1}, {"DeclinedReversal", 1, 1, 0x4a1378eba012ef40ull, 0x16ea86de895d5b40ull, 0xc3448bef59024d51ull, 0x14734f0e113371e4ull, 24, 24, 1}, {"DeclinedReversal", 1, 0, 0x4a1378eba012ef40ull, 0x14650fb0739d0383ull, 0xc3448bef59024d51ull, 0x14734f0e113371e4ull, 24, 0, 1}, - {"CalcOnFills", 0, 1, 0x7641907936993779ull, 0x3777fe6ece242880ull, 0x4603f8c446ca5c1eull, 0x2583ce2bc6a3c1d9ull, 57, 40, 10}, - {"CalcOnFills", 0, 0, 0x7641907936993779ull, 0x14650fb0739d0383ull, 0x4603f8c446ca5c1eull, 0x2583ce2bc6a3c1d9ull, 57, 0, 10}, + {"CalcOnFills", 0, 1, 0x7641907936993779ull, 0x3777fe6ece242880ull, 0x0b9be2eee5282e84ull, 0x2583ce2bc6a3c1d9ull, 57, 40, 10}, + {"CalcOnFills", 0, 0, 0x7641907936993779ull, 0x14650fb0739d0383ull, 0x0b9be2eee5282e84ull, 0x2583ce2bc6a3c1d9ull, 57, 0, 10}, {"CalcOnFills", 1, 1, 0xd120e8fefb0dc236ull, 0xdbb28de374d30bc3ull, 0x57aa00467e66c075ull, 0xad6e26cf69199431ull, 54, 40, 8}, {"CalcOnFills", 1, 0, 0xd120e8fefb0dc236ull, 0x14650fb0739d0383ull, 0x57aa00467e66c075ull, 0xad6e26cf69199431ull, 54, 0, 8}, {"CloseOrders", 0, 1, 0x78ff3352f7085436ull, 0x896bcdcf15c8a4c6ull, 0x36ebbdb119ff2815ull, 0x8e67de695bfb2c60ull, 48, 48, 16}, @@ -1079,8 +1090,8 @@ constexpr Pinned kPinned[] = { {"Random09", 0, 0, 0x400c46d70e6e84bcull, 0x14650fb0739d0383ull, 0x8abae7302d7911beull, 0x1f6cdd2e111e016aull, 58, 0, 2}, {"Random09", 1, 1, 0x32d4b35f029bad56ull, 0x447742874dde46e0ull, 0x99aae055f2d8091dull, 0x3d99e7abdd92a773ull, 58, 58, 2}, {"Random09", 1, 0, 0x32d4b35f029bad56ull, 0x14650fb0739d0383ull, 0x99aae055f2d8091dull, 0x3d99e7abdd92a773ull, 58, 0, 2}, - {"Random10", 0, 1, 0x998ee74ea73334c6ull, 0x5a3104007433abc0ull, 0x274b98362186a62full, 0x0a7888fb9b890fb4ull, 70, 51, 23}, - {"Random10", 0, 0, 0x998ee74ea73334c6ull, 0x14650fb0739d0383ull, 0x274b98362186a62full, 0x0a7888fb9b890fb4ull, 70, 0, 23}, + {"Random10", 0, 1, 0x998ee74ea73334c6ull, 0x5a3104007433abc0ull, 0xe335d30ff30abed4ull, 0x0a7888fb9b890fb4ull, 69, 51, 22}, + {"Random10", 0, 0, 0x998ee74ea73334c6ull, 0x14650fb0739d0383ull, 0xe335d30ff30abed4ull, 0x0a7888fb9b890fb4ull, 69, 0, 22}, {"Random10", 1, 1, 0xde7dbd52ac33a7ffull, 0xaf8f0ca763b06949ull, 0x5dcb9b87576b70edull, 0xfbe70c357ac296cfull, 67, 51, 22}, {"Random10", 1, 0, 0xde7dbd52ac33a7ffull, 0x14650fb0739d0383ull, 0x5dcb9b87576b70edull, 0xfbe70c357ac296cfull, 67, 0, 22}, {"Random11", 0, 1, 0x805038d1cb0a3848ull, 0xebc3fde6db4a37f3ull, 0x2f92b1af8d2cfafcull, 0x27bc15c5c142c728ull, 60, 60, 3}, @@ -1127,8 +1138,8 @@ constexpr Pinned kPinned[] = { {"Random21", 0, 0, 0x18e96beb584671fcull, 0x14650fb0739d0383ull, 0x0571605cf9e50eb1ull, 0xca719be0f46f42acull, 54, 0, 18}, {"Random21", 1, 1, 0xd32a038cc5b6ea6eull, 0x9074957bf890f8e6ull, 0x20e23c0060589e5eull, 0xb6387745e72b0075ull, 54, 54, 18}, {"Random21", 1, 0, 0xd32a038cc5b6ea6eull, 0x14650fb0739d0383ull, 0x20e23c0060589e5eull, 0xb6387745e72b0075ull, 54, 0, 18}, - {"Random22", 0, 1, 0xef05a2f01ca0c05cull, 0xb151ebfe9a44ebebull, 0x603c10590ec68778ull, 0x1b1b4c42bccc2191ull, 68, 49, 13}, - {"Random22", 0, 0, 0xef05a2f01ca0c05cull, 0x14650fb0739d0383ull, 0x603c10590ec68778ull, 0x1b1b4c42bccc2191ull, 68, 0, 13}, + {"Random22", 0, 1, 0xef05a2f01ca0c05cull, 0xb151ebfe9a44ebebull, 0x87be32daa84c6426ull, 0x1b1b4c42bccc2191ull, 68, 49, 13}, + {"Random22", 0, 0, 0xef05a2f01ca0c05cull, 0x14650fb0739d0383ull, 0x87be32daa84c6426ull, 0x1b1b4c42bccc2191ull, 68, 0, 13}, {"Random22", 1, 1, 0x1624d3651edc9089ull, 0x2a2f5457686fdb7full, 0x29259a278cc3600bull, 0xb361721c9b7f57e7ull, 68, 49, 13}, {"Random22", 1, 0, 0x1624d3651edc9089ull, 0x14650fb0739d0383ull, 0x29259a278cc3600bull, 0xb361721c9b7f57e7ull, 68, 0, 13}, {"Random23", 0, 1, 0xc50cf90af093d6e9ull, 0x629f1e11fd65d1cdull, 0xf0e62f804df9e022ull, 0x9f14134203a5c6e9ull, 56, 54, 1}, From 03d250faf19d746439e89a2a345a18e7672e9de7 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 12:16:45 +0800 Subject: [PATCH 38/83] Adapter: a leveraged opening is margin-checked on its own entry bar (R5 lane PAR-MARGIN, M7) Root cause. on_applied scheduled the entry-bar margin check (it names the kernel's AfterApplied point for the fill's driver point, which margin_check_allowed then admits) only for a full-margin opening or one with a full bracket. A leveraged opening (margin below 100) never had its entry bar checked: the kernel's post-fill point was refused, and the first call came at the next bar's open, or never when that bar had recovered. ab9714be checked that bar over the path after the fill (pine_fills.cpp:1025-1063, :1535-1546); the gap dates from 73817c1d, where the retired F2 pin of test_entry_bar_margin_path.cpp started failing. TradingView does check it: the four lab tv tapes lane H-MEASURE committed (tests/fixtures/margin_entry_bar, BINANCE:ETHUSDT.P 15m, rangeProof covered) book the margin call on the entry bar at its low, 4 of 4. Fix (adapter policy only, no kernel line). A leveraged opening fill on the position's entry bar now schedules the path check too, so the kernel's AfterApplied point is admitted there and sizes the call with TradingView's money and slice (the adapter's own hooks). Scope: the run shapes ab9714be took the entry-bar suffix on (leveraged_entry_bar_checked: not process_orders_on_close, calc_on_order_fills, the magnifier, or a timestamped FX series, which keep their routes, unmeasured against TradingView). An add to a carried book is still refused (M7-B stays pinned: TradingView unmeasured). The default-percent stop entry taken at its bar's open that schedule_preopen_margin_slice answers for keeps that slice's verdict (preopen_slice_class): the slice sizes on the frozen units, and eight NYSE:F lab tv tapes (the M10 commit) show TradingView's book and entry-bar call are that book's, where the kernel's point would size on the fill-time book the adapter opens above 100 %. Pinned rows flipped to TradingView's expectation, in tests/test_adapter_margin_schedule_differential.cpp: - M7 on tapes: the adapter now books each tape's rows (before: 0621 bar 2 2.2748 @2297.51 + bar 5; 0922 bar 2 0.668 @4152.54 + bar 5; 1121 bar 2 2.9012 @2701.42 + bar 5; 1010 no margin call); asserted equal to the tape, the call on bar 1 at its low. - M7-A: the adapter books 12 @92 on the entry bar, as the kernel and ab9714be do (before: nothing). M10's four shapes are unchanged (the pre-open slice's own opening). test_adapter_live_state_equivalence_data.hpp: three transcript digests re-harvested with the TU's harvest switch (reversals seeds 1570935, 2827683, 3456057 -- leveraged configurations whose openings breach on the entry bar; each now books that call on the entry bar instead of a bar later). The other 105 are unchanged, erased == retained still holds, and the transcript folds no hash value. Docs made true: ADR-0001 "Margin", native-engine.md's margin paragraph, design row MG15 and the fixture README (the divergence H-MEASURE recorded is closed); anchors carried across by an exact line map (1681/1681). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 778809b5d7fc01e60c5d71dc1f5e51e1187b8763) INT26 pick note: tests/test_adapter_live_state_equivalence_data.hpp conflicted line-adjacent with PAR-ORDERS' re-pin (1518fa8f): PAR-ORDERS moved seeds 1675664 and 2722954, this commit 1570935, 2827683 and 3456057, and no seed moved in both. The merged table takes each lane's own rows; the row passes on the merged tree (108 configurations, 328 checks, 0 failures), so neither change moves the other's seeds. The adapter and test hunks applied without conflict; the ADR, design and native-engine pages merged on citation digits only. The sentences it retires recorded the M7 divergence as open: H-MEASURE's margin measurements (bafefba, picked here as bc866e5). Replayed after the late pick PAR-ORDERS-2 (48832b62 pins its 16 digests): its seeds and this commit's three are disjoint again; the merged table takes both, and the row passes on the replayed tree. --- docs/adr/0001-kernel-adapter-boundary.md | 39 ++++---- docs/design/native-feature-parity.md | 92 +++++++++---------- docs/pages/native-engine.md | 21 +++-- include/pineforge/source/pine_adapter.hpp | 3 + src/source/pine_adapter.cpp | 43 +++++++++ tests/fixtures/margin_entry_bar/README.md | 5 +- ...st_adapter_live_state_equivalence_data.hpp | 14 ++- ...t_adapter_margin_schedule_differential.cpp | 84 +++++++++-------- 8 files changed, 187 insertions(+), 114 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index eb555f24..a67d5059 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -209,8 +209,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:677`) for TV sizing and fill spelling; `validate_execution_precommit` (`pine_strategy_host.cpp:702`), whose - `validate_precommit` (`pine_adapter.cpp:13120`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:13365`) to own the opening margin decision; `owns_lot_excursions() = true` + `validate_precommit` (`pine_adapter.cpp:13098`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:13343`) to own the opening margin decision; `owns_lot_excursions() = true` (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:787`), so the Pine host keeps its own per-lot MFE/MAE, `ab9714be`'s model (RULING A48). Measured against TradingView by R5 lane H-MEASURE (`tests/test_e19_excursion_tape.cpp`, `lab tv` tapes): where @@ -232,9 +232,9 @@ kernel and own these quirks, each at its site: (`select_exit_activation` src/compat/pine/exit_activation.cpp:42, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:10, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:46) 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:9665-9670`); the half-tick + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:9643-9648`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:333) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11757`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11735`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:454-460`, 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 @@ -245,12 +245,12 @@ kernel and own these quirks, each at its site: quantities leave no remnant row on a `lab tv` tape, which the Pine host books row for row while a bare host books four 2.8e-17-unit dust rows beside them. - **Close reservations.** `strategy.close` callsite batching, two-call provenance and the entry-id - ledger (`close_logical_units_` `pine_adapter.hpp:2028-2039`); the POOC reservation-growth population predicate + ledger (`close_logical_units_` `pine_adapter.hpp:2016-2027`); 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:14155`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14191`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14133`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:14169`) 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 @@ -261,7 +261,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:16963`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16901`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -273,18 +273,23 @@ kernel and own these quirks, each at its site: restore's `+1e-6` floor and on the frozen signal-time units; a gap-open breach executes at the open, sized on the open's money, where no kernel check kind does; the opening gate declines a 1x long whose entry fee `AdmitWithHostMargin` admits, and admits a gap-up add the adapter - refuses against its signal-time equity. The scheduling also misses one class the kernel - reaches: a leveraged opening is never checked on its own entry bar. Four `lab tv` tapes - (`tests/fixtures/margin_entry_bar`) book TradingView's margin call on that bar at its low, as - the kernel's `AfterApplied` point does; the adapter books it a bar late or not at all — a - recorded divergence, open. + refuses against its signal-time equity. A leveraged opening is checked on its own entry bar: + four `lab tv` tapes (`tests/fixtures/margin_entry_bar`) book TradingView's margin call on that + bar at its low, as the kernel's `AfterApplied` point does, so `on_applied` admits that point + for a leveraged opening's entry bar (R5 lane PAR-MARGIN; until then the adapter booked the + call a bar late or not at all, the divergence H-MEASURE recorded). It does so on the run + shapes `ab9714be`'s entry-bar pass covered (`leveraged_entry_bar_checked`): a + `process_orders_on_close`, `calc_on_order_fills`, magnified or timestamped-FX run keeps its + own route, unmeasured against TradingView, an add to a carried book is still refused there, and + the default-percent stop entry the pre-open slice answers for keeps that slice's verdict + (`preopen_slice_class`). - **Calculation timing.** The cadence itself is the kernel's: a `calc_on_order_fills` strategy 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:8356`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:8334`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:680`), and the two fills Pine refuses to - recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5645`). + recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5663`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the three session flags, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` @@ -337,7 +342,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:454-460`). `strategy.close` batching and the entry-id ledger - are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2028-2039`) and + are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2016-2027`) 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:424`); what enables the generic model is the presence of `NativeRunSpec::margin`. So is the string-sentinel decoding of adapter-written comments: @@ -644,7 +649,7 @@ Every line number in this section names its symbol on this tree, which |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:342`), `source_level_on_price_grid` (`:328`), `nearest_tick` (`:266`) / `source_bar_fill_tick` (`:287`) / `directional_tick` (`:311`), 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`, `tests/test_native_price_grid.cpp` | | `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13953`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:15008`), `chart_day_key` (`pine_adapter.cpp:13850`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13312-13313`): 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_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:13290-13291`): 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:2361-2363`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2388-2396`, `margin.maintenance_long` `pine_adapter.cpp:2403`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 54824edd..95f5cdfa 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,19 +47,19 @@ 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:2738-2749; `entry()` pine_adapter.cpp:5290, `order()` pine_adapter.cpp:10554) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2738-2749; `entry()` pine_adapter.cpp:5311, `order()` pine_adapter.cpp:10532) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | 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:8417 → `Flatten{}` pine_adapter.cpp:8446-8447; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2708-3915 → `host.replace` pine_adapter.cpp:2724-2804) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4723-4769, `cancel_all` pine_adapter.cpp:18191-18206; `live_by_source_key_` pine_adapter.hpp:2000; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4723-4769, `cancel_all` pine_adapter.cpp:18129-18144; `live_by_source_key_` pine_adapter.hpp:1988; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5514-5549, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7538-7555) | **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:1270, `CancelReason::OwnerGone` native_order.hpp:949 | K | — | F:A6 O:A8 O:A9 S:O5 | FIFO *ordering* is host-computed (O) | +| 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:5535-5570, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7525-7542) | **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:1270, `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:2747-2753) | **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:1092-1093 | 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:13090; lowering `submit_one` pine_adapter.cpp:9114-9126, `BindCohort` pine_adapter.cpp:9615-9621, `submit_leg` pine_adapter.cpp:9639-9663; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8719-8722; `materialize_relative_exits` pine_adapter.cpp:17481-17522, pine_adapter.hpp:1820-1832) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19254-19283) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:2002-2020); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10954-10978), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15717, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15951) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:684-686 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:13068; lowering `submit_one` pine_adapter.cpp:9092-9104, `BindCohort` pine_adapter.cpp:9593-9599, `submit_leg` pine_adapter.cpp:9617-9641; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8697-8700; `materialize_relative_exits` pine_adapter.cpp:17419-17460, pine_adapter.hpp:1823-1835) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19201-19230) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:2000-2018); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10932-10956), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15695, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15929) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:684-686 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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,26 +68,26 @@ 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:5692-5714; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1080 | 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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11865-11877) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11778-11915) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11843-11855) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11756-11893) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4022-4068) | **partial** — the read exists `trail_state` native_host.hpp:1080; `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:287-289) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9665-9670, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11778-11915, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8689-8915, `trail_one_shot` pine_adapter.cpp:9754-9883 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9643-9648, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11756-11893, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8667-8893, `trail_one_shot` pine_adapter.cpp:9732-9861 | 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:5925-5929, pine_adapter.cpp:5925) | **yes** — `Transact` in `OrderIntent` native_order.hpp:229 | K | — | F:C1 O:C1 S:S1 | — | +| SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5924-5928, pine_adapter.cpp:5924) | **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:10989-12193) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4922-4956 | 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:2596-2651) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4957-4959 **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: rounded equity `source_money_round` pine_adapter.cpp:2548, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2557-2559) | **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:2609, `typed_entry_units` pine_adapter.cpp:2627; `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:2588; `percent_commission_live_equity` pine_adapter.cpp:4329-4369) | **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. | -| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2514-2529, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6710-6736) | **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`. | +| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2514-2529, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6697-6723) | **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:591, 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:4973-4976; 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:12564-12592, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:9041-9351) | **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. | +| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:12542-12570, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:9019-9329) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4444-4459) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6771), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7978), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12821), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13417, pine_adapter.cpp:13470) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15765) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18849) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7240-7591; ledger `close_batch_bar_` pine_adapter.hpp:2043-2054; 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. | +| 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:6758), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7965), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12799), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13395, pine_adapter.cpp:13448) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15743) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18796) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7227-7578; ledger `close_batch_bar_` pine_adapter.hpp:2041-2052; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | ### 1.4 Margin, admission, liquidation, risk @@ -95,21 +95,21 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2362) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13312-13315) is TV. | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13290-13293) is TV. | | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6230-6279, cap left unset `max_open_lots` pine_adapter.cpp:2361-2363) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13366-13431 and leaves the kernel gate unset pine_adapter.cpp:13365-13367) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:14021-14022, `margin_pct` pine_adapter.cpp:13429) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14121-14299, `submit_margin_call_units` pine_adapter.cpp:14260-14306, `schedule_margin_call_path` pine_adapter.cpp:18344-18421) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14052-14088) | **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:983 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. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13344-13409 and leaves the kernel gate unset pine_adapter.cpp:13343-13345) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13999-14000, `margin_pct` pine_adapter.cpp:13407) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14099-14277, `submit_margin_call_units` pine_adapter.cpp:14238-14284, `schedule_margin_call_path` pine_adapter.cpp:18282-18359) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14030-14066) | **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:983 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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1308 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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16369-16385, `apply_fx_opening_margin_slice` pine_adapter.cpp:18422-18450) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16933-16972) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13401-13404) | **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:13982-13996, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:15008-15038) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15051-15285) | **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:13792-13871) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:14010, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14069, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14777-14817, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14301-14428, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14301) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, the restore's `+1e-6` floor and the frozen signal-time units (M10); `margin_check_allowed` never admits the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/margin_entry_bar`) and the kernel's `AfterApplied` point book the margin call at that bar's low — a recorded divergence, open (M7). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15289-15347) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16347-16363, `apply_fx_opening_margin_slice` pine_adapter.cpp:18360-18409) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16871-16910) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13379-13382) | **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:13960-13974, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14986-15016) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15029-15263) | **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:13770-13849) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:13946, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14005, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14713-14753, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14237-14364, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14237) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, the restore's `+1e-6` floor and the frozen signal-time units (M10); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15267-15325) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -117,15 +117,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:881, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7523-7527 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16204) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7065-7069; `execute_current` legal native_host.hpp:1092 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16182) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7065-7069; `execute_current` legal native_host.hpp:1092 | 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:7008 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7059 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1061 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:863, `NativeInputContext` native_host.hpp:761-766 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5611, `flush_coof_tail` pine_adapter.cpp:16347, `end_coof_recalc` pine_adapter.cpp:5634) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6919, native_execution_consumer.cpp:6919-6949; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5632, `flush_coof_tail` pine_adapter.cpp:16325, `end_coof_recalc` pine_adapter.cpp:5653) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6919, native_execution_consumer.cpp:6919-6949; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:866 fires per accepted realtime print native_execution_consumer.cpp:7147-7151; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | | 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:7517-7634 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:234-320) 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:5780), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1659-1666 | 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:5779), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1659-1666 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds @@ -145,7 +145,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:5685-5732, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6268-6276; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | | FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12032-12041) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:472-479) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:12048-12057, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:12026-12035, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:1190 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:8705). **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). | @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19254-19283 | 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:19201-19230 | 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:523, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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:603. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1273 | 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) | @@ -298,7 +298,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2949-3017) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2956-3024) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:45, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -324,7 +324,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:1730, `guard_native_mutation` engine_aux_security.cpp:78 | 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). No kernel TU includes a source header | `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13 | 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:739 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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549; the ledger `close_logical_units_` pine_adapter.hpp:2028-2039; `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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549; the ledger `close_logical_units_` pine_adapter.hpp:2016-2027; `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) | 58 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 58 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:13933f70987f6bfe37d3015ae065a8d5ee5937c37431d48e2ecd1f17a4c29958` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:862, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:590-709, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:515-567, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:494-511, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:802-833, c_abi.cpp:929 | 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 | @@ -522,11 +522,11 @@ line numbers are this tree's (683a82f); kernel semantics are | 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:2362); 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:13312-13313; 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:16933, `update_risk_state` pine_adapter.cpp:13953-13980): 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:13305), 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_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:13290-13291; 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:16871, `update_risk_state` pine_adapter.cpp:13931-13958): 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:13283), 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:1200-1208): +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:17724-17733); 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:16363); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16475), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15038-15047, `submit_intraday_loss_close` pine_adapter.cpp:15008-15036), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17290); 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:5984, `:7685`, `:8418`, `:8546`, `:11535`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18189-18192) | `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:13944-13946); 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:13850-13871, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16341); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16453), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15016-15025, `submit_intraday_loss_close` pine_adapter.cpp:14986-15014), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17228); 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:5983, `:7672`, `:8396`, `:8524`, `:11513`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18127-18130) | `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:13922-13924); 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:13828-13849, `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, @@ -1053,10 +1053,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:8830). Full-table scans + (`has_full_entry_bracket` pine_adapter.cpp:8808). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1696, `revive_brackets_after_margin` pine_adapter.cpp:1918, - `preserved_by_close_all` pine_adapter.cpp:11566), and + `preserved_by_close_all` pine_adapter.cpp:11544), 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. @@ -1299,8 +1299,8 @@ unchanged after it. | E1 | **Spec hashing.** `hash_spec` folds every field unconditionally (native_execution_consumer.cpp:234-256) | 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:1620); 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5780) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290); 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:16204-16233) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5779) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290); 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:16182-16211) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | @@ -1345,7 +1345,7 @@ unchanged after it. | **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:16369-16385) 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:2646) 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:16347-16363) 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:2646) 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. | @@ -1443,7 +1443,7 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1651-1652) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6981-6993) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1635-1636) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6981-6993) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). 2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5616-5620) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1659-1666) is calibrated against it. 3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6834-6841). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2553); valid in bar-open, applied and tick callbacks. @@ -1531,7 +1531,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:1317) (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:373), 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:13776-13874); 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:13754-13852); 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:4654-4667 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 61ce8a79..956b2ab5 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1458,7 +1458,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:14155-14179`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:14133-14157`), 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 @@ -1490,13 +1490,18 @@ that row. On the same money the pre-open slice and the kernel's `AfterApplied` point part on the one-contract band, on the restore's `+1e-6` floor and on the frozen signal-time units. The opening gate parts both ways: the kernel declines a 1× long whose entry fee the adapter admits, and admits a -gap-up add the adapter refuses against its signal-time equity. And the -scheduling misses one class the kernel reaches: a leveraged opening (margin -below 100 %) is never checked on its own entry bar. Four `lab tv` tapes -(`tests/fixtures/margin_entry_bar`) book TradingView's margin call on that -bar at its low, as the kernel's `AfterApplied` point does; the adapter books -it a bar late or not at all. The row pins that divergence as recorded, not -ruled. +gap-up add the adapter refuses against its signal-time equity. A leveraged +opening (margin below 100 %) is checked on its own entry bar: four `lab tv` +tapes (`tests/fixtures/margin_entry_bar`) book TradingView's margin call on +that bar at its low, as the kernel's `AfterApplied` point does, and since R5 +lane PAR-MARGIN the adapter admits that point for a leveraged opening's entry +bar and books the tapes' rows (before it, the call came a bar late or not at +all). The admission covers the run shapes `ab9714be`'s entry-bar pass covered; +a `process_orders_on_close`, `calc_on_order_fills`, magnified or +timestamped-FX run keeps its own route, unmeasured against TradingView, the +`AfterApplied` point after an add to a carried book is still refused, and a +default-percent stop entry taken at its bar's open keeps the pre-open slice's +verdict. ### Risk limits diff --git a/include/pineforge/source/pine_adapter.hpp b/include/pineforge/source/pine_adapter.hpp index 913ba727..67522962 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -1737,6 +1737,9 @@ class PineExecutionAdapter { void apply_fx_opening_margin_slice(const native_order::ExecutionAppliedEvent&, const NativeDecisionContext&); void schedule_preopen_margin_slice(const Bar&, const NativeDecisionContext&); + bool leveraged_entry_bar_checked() const noexcept; + bool preopen_slice_class(const PlacementSnapshot&, + const NativeDecisionContext&) const noexcept; bool submit_margin_call_slice(double mark_price, const NativeDecisionContext&, bool opening_checkpoint = false); bool submit_margin_call_units(double mark_price, const NativeDecisionContext&, diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index acf26c79..0e6aac59 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -4873,6 +4873,28 @@ void PineExecutionAdapter::apply_fx_opening_margin_slice( (void)submit_margin_call_slice(event.resolved_price, context, true); } +// Whether on_applied admits the kernel's post-fill point on a leveraged +// opening's entry bar (the run shapes ab9714be took the entry-bar suffix on, +// pine_fills.cpp:1014-1023, less a timestamped-FX run, whose path checks +// on_bar_open does not schedule either). +bool PineExecutionAdapter::leveraged_entry_bar_checked() const noexcept { + return !config_.process_orders_on_close && !config_.calc_on_order_fills + && !bar_magnifier_ && staged_.account_fx_effective_from_ms.empty(); +} + +// The opening schedule_preopen_margin_slice answers for: a default-percent +// stop entry taken at its bar's open. That slice sizes its entry-bar call on +// the entry's frozen units, and its verdict -- a slice, or none -- stands for +// the bar; the kernel's post-fill point is not admitted beside it. +bool PineExecutionAdapter::preopen_slice_class( + const PlacementSnapshot& opening, const NativeDecisionContext& context) const noexcept { + return config_.default_qty_type == static_cast(QtyType::PERCENT_OF_EQUITY) + && opening.family == PineOrderFamily::Entry + && finite_positive(opening.sizing.frozen_units) + && finite_positive(opening.exit_levels.stop) + && context.coordinate.path_phase == NativePathPhase::Open; +} + void PineExecutionAdapter::schedule_preopen_margin_slice( const Bar& bar, const NativeDecisionContext& context) { // The legacy broker checks the adverse excursion of a default-percent @@ -18343,6 +18365,27 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& schedule_margin_call_path(policy_script_bar_, context); } } + } else if (!full_margin_opening && !placement_snapshot->has_full_entry_bracket + && !preopen_margin_already_scheduled && stable_opening_fx + && position_open_script_bar_ == context.script_bar_open_ms + && leveraged_entry_bar_checked() + && !preopen_slice_class(*placement_snapshot, context)) { + // A leveraged opening (margin below 100) has no fill-price trim, + // but TradingView still checks its ENTRY bar over the path after + // the fill: four lab tv tapes (tests/fixtures/margin_entry_bar) + // book the margin call at that bar's low, as ab9714be's + // entry-bar pass did (pine_fills.cpp:1025-1063 and :1535-1546, + // the suffix after the opening's fill). The run shapes ab9714be + // took no such suffix on keep their own routes here, unmeasured + // against TradingView (leveraged_entry_bar_checked), and so does + // the opening schedule_preopen_margin_slice answers for, sliced or + // not (preopen_slice_class). Admit the kernel's post-fill point + // for this driver point: it measures the same suffix and sizes + // the call with TradingView's money and slice + // (resolve_margin_requirement, resolve_margin_call_units). Every + // opening fill of the entry bar is admitted, so an add re-sizes a + // call armed on the smaller book. + (void)schedule_margin_call_path(policy_script_bar_, context); } schedule_intraday_loss_path(policy_script_bar_, context); } diff --git a/tests/fixtures/margin_entry_bar/README.md b/tests/fixtures/margin_entry_bar/README.md index 9021f9f4..83d353ee 100644 --- a/tests/fixtures/margin_entry_bar/README.md +++ b/tests/fixtures/margin_entry_bar/README.md @@ -5,7 +5,10 @@ does TradingView check a LEVERAGED opening (margin below 100 %) on the bar its entry fills, where the adapter's `margin_check_allowed` refuses the kernel's AfterApplied point? `tests/test_adapter_margin_schedule_differential.cpp` (section "M7 on tapes") replays every tape through the Pine adapter and through -a bare kernel host with TradingView's money and slice. +a bare kernel host with TradingView's money and slice. TradingView does, on 4 +of 4; since R5 lane PAR-MARGIN `on_applied` admits that point for a leveraged +opening's entry bar, and the section asserts the adapter books each tape's +rows (lane H-MEASURE had pinned the adapter's late or missing call instead). Each directory is one `lab tv` export (pineforge-workflow, channel `ws-report-v1`, `rangeProof: covered`), byte-identical: `strategy.pine`, diff --git a/tests/test_adapter_live_state_equivalence_data.hpp b/tests/test_adapter_live_state_equivalence_data.hpp index e3d983e1..48767aaa 100644 --- a/tests/test_adapter_live_state_equivalence_data.hpp +++ b/tests/test_adapter_live_state_equivalence_data.hpp @@ -20,6 +20,14 @@ #pragma once // K-IDX Option A re-pins the v19 transcript digests once for the script/input coordinate fold. +// +// R5 lane PAR-MARGIN re-harvested three values the same way, on its tree: +// reversals seeds 1570935, 2827683 and 3456057, the leveraged (margin 25, +// default 700 %) configurations whose openings breach on their own entry bar. +// on_applied now admits the kernel's post-fill margin point there +// (TradingView's rule: tests/fixtures/margin_entry_bar), so each books that +// margin call on the entry bar instead of a bar later; the other 105 values +// are unchanged. // R5 lane PAR-ORDERS re-harvested it the same way on its tree (29 of 108 digests // move; see the lane's hash commit for why each moves). @@ -57,7 +65,7 @@ constexpr std::uint64_t kTranscriptDigests[] = { 9826242374101611623ull, // reversals seed 1256748, 118 trades, 402 commands, 362 rows placed 4883135135199369800ull, // reversals seed 1361477, 105 trades, 413 commands, 349 rows placed 6980972530369447172ull, // reversals seed 1466206, 151 trades, 372 commands, 393 rows placed - 419860461863827083ull, // reversals seed 1570935, 77 trades, 357 commands, 297 rows placed + 11252195188350860763ull, // reversals seed 1570935, 92 trades, 362 commands, 328 rows placed 15023244533711012002ull, // reversals seed 1675664, 124 trades, 398 commands, 405 rows placed 3425020382181293193ull, // reversals seed 1780393, 196 trades, 702 commands, 629 rows placed 4513324373166417315ull, // reversals seed 1885122, 113 trades, 393 commands, 388 rows placed @@ -69,13 +77,13 @@ constexpr std::uint64_t kTranscriptDigests[] = { 1517737495207721277ull, // reversals seed 2513496, 110 trades, 390 commands, 394 rows placed 1610718425964291349ull, // reversals seed 2618225, 105 trades, 411 commands, 371 rows placed 4868757197565921069ull, // reversals seed 2722954, 150 trades, 390 commands, 428 rows placed - 8640229404818843386ull, // reversals seed 2827683, 72 trades, 339 commands, 277 rows placed + 13294227884245356190ull, // reversals seed 2827683, 48 trades, 347 commands, 215 rows placed 8135938306046315211ull, // reversals seed 2932412, 96 trades, 360 commands, 318 rows placed 9738428349677663372ull, // reversals seed 3037141, 114 trades, 405 commands, 377 rows placed 2670083654842814989ull, // reversals seed 3141870, 66 trades, 332 commands, 248 rows placed 14261337452491704800ull, // reversals seed 3246599, 156 trades, 469 commands, 484 rows placed 14583498249706098574ull, // reversals seed 3351328, 108 trades, 414 commands, 345 rows placed - 13691690510990974075ull, // reversals seed 3456057, 88 trades, 373 commands, 334 rows placed + 7833467542887485857ull, // reversals seed 3456057, 87 trades, 373 commands, 334 rows placed 14092552094636276530ull, // reversals seed 3560786, 147 trades, 413 commands, 444 rows placed 4203447471686931733ull, // reversals seed 3665515, 133 trades, 394 commands, 397 rows placed 16581004868667265691ull, // reversals seed 3770244, 53 trades, 339 commands, 245 rows placed diff --git a/tests/test_adapter_margin_schedule_differential.cpp b/tests/test_adapter_margin_schedule_differential.cpp index 5f8b1371..fb43d8d2 100644 --- a/tests/test_adapter_margin_schedule_differential.cpp +++ b/tests/test_adapter_margin_schedule_differential.cpp @@ -19,9 +19,12 @@ * Each section prints both sides' margin executions (bar, phase, time, units, * price, origin) and final book, then asserts the measured difference. * - * M7 (G2-09) scheduling. The kernel's AfterApplied point after an opening - * or an add, and its BarOpen point on a carried POOC short with fees, - * are points the adapter refuses; the kernel books there. + * M7 (G2-09) scheduling. The kernel's AfterApplied point after a + * leveraged opening is admitted on the opening's own entry bar since + * R5 lane PAR-MARGIN (TradingView's tapes below), so the adapter books + * what the kernel books there. Its AfterApplied point after an add to + * a carried book, and its BarOpen point on a carried POOC short with + * fees, are points the adapter still refuses; the kernel books there. * M8 (G2-10) checkpoints as market executions. A gap-open breach: the * adapter executes at the open, sized at the open; the kernel has no * check kind that does -- its mark check rests at the adverse extreme, @@ -40,8 +43,9 @@ * M7 on TradingView's tapes (tests/fixtures/margin_entry_bar): a leveraged * opening whose entry bar breaches. TradingView books the margin call * on the entry bar at its low (4 of 4 tapes), and so does the kernel's - * AfterApplied point with TradingView's money; the adapter checks the - * entry bar of a leveraged opening never -- a RECORDED divergence, pinned. + * AfterApplied point with TradingView's money. Until R5 lane PAR-MARGIN + * the adapter never checked that bar (a recorded divergence H-MEASURE + * pinned); it now books TradingView's rows on all four tapes. * * Source-bound (includes pineforge/source): release profile only. */ @@ -346,9 +350,10 @@ no::Request stop_buy(double units, double level, const char* label) { // the book is 680 against 736. // kernel: the AfterApplied point after the fill measures the remaining path // at 92 and books 4 x floor(56 / 18.4) = 12 there; -// adapter: margin_check_allowed refuses that point (no opening checkpoint -// for a leveraged opening without a full bracket), so the entry bar -// is never checked and bar 2 (O105) is solvent -- no margin row. +// adapter: since R5 lane PAR-MARGIN on_applied admits that point on a +// leveraged opening's entry bar, so it books the same 12 @92 (until +// then margin_check_allowed refused it: no margin row at all, as +// the run gated to BarOpen below still shows). void m7_leveraged_opening_entry_bar() { std::printf("-- M7-A leveraged market opening: the entry bar's AfterApplied point\n"); Config c; @@ -378,15 +383,17 @@ void m7_leveraged_opening_entry_bar() { open_only.run_bars(pine_margin_spec("m7-a-open", c, 0.0), bars); print_side("k-BarOpen", margin_fills(open_only), open_only.position(), open_only.balance()); - CHECK(adapter.empty()); - CHECK(pine.position() == 40.0); REQUIRE(native.size() == 1); check_fill(native[0], 1, NativePathPhase::Low, 12.0, 92.0, true); CHECK(kernel.position() == 28.0); - // The whole difference is the one point: gated to BarOpen, the kernel - // books exactly what the adapter books. + REQUIRE(adapter.size() == 1); + check_fill(adapter[0], 1, NativePathPhase::Low, 12.0, 92.0, true); + CHECK(adapter[0].label == "__margin_call__"); + CHECK(pine.position() == 28.0); + // The whole difference was the one point: gated to BarOpen, the kernel + // books what the adapter booked before PAR-MARGIN -- nothing. CHECK(margin_fills(open_only).empty()); - CHECK(open_only.position() == pine.position()); + CHECK(open_only.position() == 40.0); } // B. A carried 10 @100 long (50 %, capital 1050) adds 10 @95 on a buy limit @@ -538,6 +545,10 @@ void m8_gap_open() { // after the open fill, with the twin's slice. // ab9714be books the kernel's numbers on the stop entry in every case below // (1 @90 band, 1 @88 fudge, 1 @90 frozen); only the pre-open slice parts. +// The stop entry's own entry bar stays the pre-open slice's where it slices +// nothing (the band): R5 lane PAR-MARGIN admits the kernel's AfterApplied +// point for a leveraged opening's entry bar (M7) everywhere but on the +// opening this slice answers for (preopen_slice_class). struct M10Case { const char* label; double capital; @@ -817,13 +828,15 @@ void e20_probe_tail_margin_call() { // of 4 (the whole book on the three 16x tapes, 0.4544 of 1.074 on the 4x); // the kernel's AfterApplied point, with TradingView's money and slice (the // twin's hooks above), books the same rows; -// the adapter never checks the entry bar of a leveraged opening -// (margin_check_allowed refuses the opening's AfterApplied point): it books -// its first margin call at the NEXT bar's open, or none at all when that bar -// has recovered (the 4x tape). These are RECORDED divergences: every adapter -// row is pinned at exactly its bar, price and quantity, and asserted to -// differ from the tape, so the rows fail the day the adapter checks the -// entry bar and must be re-pinned deliberately. +// the adapter books the same rows since R5 lane PAR-MARGIN, which admits +// that point on a leveraged opening's entry bar. Lane H-MEASURE pinned what +// it booked before -- the entry bar never checked, the first margin call at +// the NEXT bar's open or none at all when that bar had recovered: +// 0621: bar 2 2.2748 @2297.51 'Margin call', bar 5 4.5672 @2301.62 +// 0922: bar 2 0.668 @4152.54 'Margin call', bar 5 3.125 @4190.01 +// 1121: bar 2 2.9012 @2701.42 'Margin call', bar 5 2.9048 @2734.82 +// 1010: bar 5 1.074 @3895 (no margin call) +// and those pins flipped to the tape's rows with the fix. struct TapeFill { int bar; double price; @@ -835,7 +848,6 @@ struct M7Tape { const FeedBar* bars; double margin; double leverage; // quantity = leverage x equity / signal close - std::vector adapter; // the adapter's pinned rows (exit bar, price, qty) }; // "YYYY-MM-DD HH:MM" in the tape's UTC+8 -> UTC milliseconds. @@ -911,14 +923,10 @@ void print_rows(const char* side, const std::vector& rows) { void m7_tradingview_tapes() { const M7Tape tapes[] = { - {"hm-m7a-eth-m5-k16-0621", kM7a_m5_k16_0621, 5.0, 16.0, - {{2, 2297.51, 2.2747999999999999, "Margin call"}, {5, 2301.62, 4.5671999999999997, ""}}}, - {"hm-m7a-eth-m5-k16-0922", kM7a_m5_k16_0922, 5.0, 16.0, - {{2, 4152.54, 0.66800000000000004, "Margin call"}, {5, 4190.01, 3.125, ""}}}, - {"hm-m7a-eth-m5-k16-1121", kM7a_m5_k16_1121, 5.0, 16.0, - {{2, 2701.42, 2.9012000000000002, "Margin call"}, {5, 2734.82, 2.9047999999999998, ""}}}, - {"hm-m7a-eth-m20-k4-1010", kM7a_m20_k4_1010, 20.0, 4.0, - {{5, 3895.0, 1.0740000000000001, ""}}}, + {"hm-m7a-eth-m5-k16-0621", kM7a_m5_k16_0621, 5.0, 16.0}, + {"hm-m7a-eth-m5-k16-0922", kM7a_m5_k16_0922, 5.0, 16.0}, + {"hm-m7a-eth-m5-k16-1121", kM7a_m5_k16_1121, 5.0, 16.0}, + {"hm-m7a-eth-m20-k4-1010", kM7a_m20_k4_1010, 20.0, 4.0}, }; for (const M7Tape& tape : tapes) { std::printf("-- M7 tape %s (margin %.0f%%, %.0fx)\n", tape.slug, tape.margin, tape.leverage); @@ -966,16 +974,14 @@ void m7_tradingview_tapes() { CHECK(std::abs(tv[0].price - bars[1].low) <= 5e-3); // The kernel's own AfterApplied point books TradingView's rows. CHECK(same_rows(native, tv)); - // The adapter: pinned, and a recorded divergence from the tape. - CHECK(!same_rows(adapter, tv)); - REQUIRE(adapter.size() == tape.adapter.size()); - for (std::size_t i = 0; i < adapter.size(); ++i) { - CHECK(adapter[i].bar == tape.adapter[i].bar); - CHECK(same_value(adapter[i].price, tape.adapter[i].price)); - CHECK(std::abs(adapter[i].qty - tape.adapter[i].qty) <= 1e-9); - CHECK(adapter[i].signal == tape.adapter[i].signal); - CHECK(adapter[i].bar != 1); // never on the entry bar - } + // So does the adapter, its margin call on the entry bar at its low + // with TradingView's own signal. + CHECK(same_rows(adapter, tv)); + REQUIRE(!adapter.empty()); + CHECK(adapter[0].bar == 1); + CHECK(adapter[0].signal == "Margin call"); + CHECK(same_value(adapter[0].price, bars[1].low)); + CHECK(same_value(adapter[0].qty, native[0].qty)); } } From 7b94176dff9457105fc000a7288bf90783257ef9 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 06:44:29 +0800 Subject: [PATCH 39/83] Adapter: a default-percent stop entry above 100 % keeps its frozen units, TradingView's book (R5 lane PAR-MARGIN, M10) H-MEASURE's M10 row found the pre-open margin slice sized on the frozen signal-time units (19 lots at a 100 signal) where the book the fill opens holds 18 (a 101 stop gapped through at 102), so it booked 4 @90 on a book the run did not hold. Root cause, measured against TradingView. The adapter froze a default-percent pure stop entry at placement -- at its snapped level, or at the signal close when the stop was already marketable -- only up to 100 % (default_stop_scope, ab9714be pine_strategy_commands.cpp:307-310). Above 100 it sized the entry again at the fill (sizing.at_fill) while frozen_units kept the signal price's quotient, and the pre-open slice read frozen_units. Eight lab tv tapes on NYSE:F 15m (tests/fixtures/margin_entry_bar/pm-m10-*, rangeProof covered, margin 25, notes tv-tape-pm-m10-*) show TradingView freezes above 100 % too: - 400 %, stop marketable when placed: books 3427, 4219, 3144, 3294 = the signal close's quotient on its tick; - 390 %, stop not yet marketable, gapped through: 3896, 3475, 3987, 3757 = the snapped level's quotient; never the fill's, with every margin call sized on that book. Fix (adapter only). The freeze and the pure-stop terms/probe paths apply at any percentage (default_stop_freeze; resolve_terms and the pending-order fill-qty probe drop their <= 100 cap); ab9714be's own <= 100 placement admission keeps its scope, and above 100 the affordability arm admits as before. frozen_units, the book and the pre-open slice's basis are now one quotient, TradingView's. Before -> after on the eight tapes (test_adapter_margin_schedule_differential "M10 on tapes"): books 0/8 -> 8/8; rows (bar, quantity, signal) 8/8 after; prices 6/8 exact after -- 1007's and 0501's margin calls at an exact half-cent low (12.105, 10.025) book a tick below TradingView (12.10 / 10.02 against 12.11 / 10.03): those decimals are stored a hair above the half cent and TradingView rounds the stored value to its nearest tick. Recorded and pinned as such, not fixed here. Before, the gap-up tapes' slices were sized on the wrong book (404, 188, 108, 236 against 376, 156, 92, 204) and 0410 and 0309 took margin calls TradingView never took. Pinned row flipped: M10 frozen -- the stop's book is now the 101 level's 18 lots on both sides and the pre-open slice's own x4 sizes nothing on them (before: 4 @90 on 19). What still parts it from the kernel's 1 @90 is the one-contract band, the band row's -- H-THIN's X15 swap, as are the fudge row's +1e-6. r5/hthin does not exist yet; this is the basis half alone. Fail-before: the new section against the previous library fails (361 checks, 6 failures, books 3481 against 3427). The corpus holds no percent_of_equity default above 100, so its trades cannot move. Docs made true: ADR-0001 "Margin", native-engine.md, design MG15, pineforge.h's DEFAULT_STOP_PLACEMENT partition, the kernel-notes page's verbatim legacy rule (a dated note beneath it), the fixture README; anchors carried by an exact line map. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 95bbca71453ef818eb9a285192a2efdfe4b94611) INT26 pick note: the adapter, header-comment, test and fixture hunks applied without conflict; the ADR and design pages merged on citation digits only. The sentence it retires recorded the M10 frozen-units row as open: H-MEASURE's margin measurements (bafefba, picked here as bc866e5). --- docs/adr/0001-kernel-adapter-boundary.md | 26 ++-- docs/design/native-feature-parity.md | 82 +++++----- docs/pages/native-engine.md | 16 +- docs/pages/pine-to-native.md | 16 +- docs/pine-adapter-kernel-notes.md | 6 + include/pineforge/pineforge.h | 9 +- src/source/pine_adapter.cpp | 28 ++-- tests/fixtures/margin_entry_bar/README.md | 29 ++++ tests/fixtures/margin_entry_bar/m10_bars.inc | 69 +++++++++ .../pm-m10-f-gapup-0331/meta.json | 16 ++ .../pm-m10-f-gapup-0331/metrics.json | 40 +++++ .../pm-m10-f-gapup-0331/strategy.pine | 14 ++ .../pm-m10-f-gapup-0331/tv_trades.csv | 5 + .../pm-m10-f-gapup-0424/meta.json | 16 ++ .../pm-m10-f-gapup-0424/metrics.json | 40 +++++ .../pm-m10-f-gapup-0424/strategy.pine | 14 ++ .../pm-m10-f-gapup-0424/tv_trades.csv | 5 + .../pm-m10-f-gapup-0501/meta.json | 16 ++ .../pm-m10-f-gapup-0501/metrics.json | 40 +++++ .../pm-m10-f-gapup-0501/strategy.pine | 14 ++ .../pm-m10-f-gapup-0501/tv_trades.csv | 5 + .../pm-m10-f-gapup-0527/meta.json | 16 ++ .../pm-m10-f-gapup-0527/metrics.json | 40 +++++ .../pm-m10-f-gapup-0527/strategy.pine | 14 ++ .../pm-m10-f-gapup-0527/tv_trades.csv | 5 + .../pm-m10-f-k4-0309/meta.json | 16 ++ .../pm-m10-f-k4-0309/metrics.json | 40 +++++ .../pm-m10-f-k4-0309/strategy.pine | 14 ++ .../pm-m10-f-k4-0309/tv_trades.csv | 7 + .../pm-m10-f-k4-0402/meta.json | 16 ++ .../pm-m10-f-k4-0402/metrics.json | 40 +++++ .../pm-m10-f-k4-0402/strategy.pine | 14 ++ .../pm-m10-f-k4-0402/tv_trades.csv | 5 + .../pm-m10-f-k4-0410/meta.json | 16 ++ .../pm-m10-f-k4-0410/metrics.json | 40 +++++ .../pm-m10-f-k4-0410/strategy.pine | 14 ++ .../pm-m10-f-k4-0410/tv_trades.csv | 3 + .../pm-m10-f-k4-1007/meta.json | 16 ++ .../pm-m10-f-k4-1007/metrics.json | 40 +++++ .../pm-m10-f-k4-1007/strategy.pine | 14 ++ .../pm-m10-f-k4-1007/tv_trades.csv | 5 + .../margin_entry_bar/pm-m10-f-ohlc/meta.json | 16 ++ .../pm-m10-f-ohlc/metrics.json | 40 +++++ .../pm-m10-f-ohlc/strategy.pine | 15 ++ .../pm-m10-f-ohlc/tv_trades.csv | 49 ++++++ .../margin_entry_bar/pm-m10-f-ohlc2/meta.json | 16 ++ .../pm-m10-f-ohlc2/metrics.json | 40 +++++ .../pm-m10-f-ohlc2/strategy.pine | 15 ++ .../pm-m10-f-ohlc2/tv_trades.csv | 49 ++++++ ...t_adapter_margin_schedule_differential.cpp | 143 ++++++++++++++++-- 50 files changed, 1175 insertions(+), 89 deletions(-) create mode 100644 tests/fixtures/margin_entry_bar/m10_bars.inc create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/tv_trades.csv diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index a67d5059..91110031 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -209,8 +209,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:677`) for TV sizing and fill spelling; `validate_execution_precommit` (`pine_strategy_host.cpp:702`), whose - `validate_precommit` (`pine_adapter.cpp:13098`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:13343`) to own the opening margin decision; `owns_lot_excursions() = true` + `validate_precommit` (`pine_adapter.cpp:13065`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:13310`) to own the opening margin decision; `owns_lot_excursions() = true` (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:787`), so the Pine host keeps its own per-lot MFE/MAE, `ab9714be`'s model (RULING A48). Measured against TradingView by R5 lane H-MEASURE (`tests/test_e19_excursion_tape.cpp`, `lab tv` tapes): where @@ -232,9 +232,9 @@ kernel and own these quirks, each at its site: (`select_exit_activation` src/compat/pine/exit_activation.cpp:42, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:10, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:46) 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:9643-9648`); the half-tick + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:9623-9628`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:333) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11735`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11703`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:454-460`, 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 @@ -249,8 +249,8 @@ kernel and own these quirks, each at its site: (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9). - **Margin.** The kernel owns the margin *mechanism* — the level solve, the check points, the kernel request, its re-pricing, the receipt — and the adapter answers its three policy hooks - with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14133`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14169`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14100`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:14136`) 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 @@ -261,7 +261,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:16901`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16835`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -269,8 +269,12 @@ kernel and own these quirks, each at its site: after the open fill measures the same bar. Measured by R5 lane H-MEASURE (`tests/test_adapter_margin_schedule_differential.cpp`: the same bars and orders through the adapter and through a bare host whose hooks answer TradingView's money and slice): the pre-open - slice parts from the kernel's point, and from `ab9714be`, on the one-contract band, on the - restore's `+1e-6` floor and on the frozen signal-time units; a gap-open breach executes at the + slice parts from the kernel's point, and from `ab9714be`, on the one-contract band and on the + restore's `+1e-6` floor (on the frozen units too, until R5 lane PAR-MARGIN froze a + default-percent stop entry above 100 % at its snapped level, or at the signal close when + marketable, as eight `lab tv` tapes show TradingView does: its book and its entry-bar call are + that quotient's, never the fill's; `ab9714be` sized such an entry at the fill); a gap-open + breach executes at the open, sized on the open's money, where no kernel check kind does; the opening gate declines a 1x long whose entry fee `AdmitWithHostMargin` admits, and admits a gap-up add the adapter refuses against its signal-time equity. A leveraged opening is checked on its own entry bar: @@ -287,7 +291,7 @@ 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:8334`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:8314`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:680`), and the two fills Pine refuses to recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5663`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and @@ -649,7 +653,7 @@ Every line number in this section names its symbol on this tree, which |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:342`), `source_level_on_price_grid` (`:328`), `nearest_tick` (`:266`) / `source_bar_fill_tick` (`:287`) / `directional_tick` (`:311`), 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`, `tests/test_native_price_grid.cpp` | | `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13953`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:15008`), `chart_day_key` (`pine_adapter.cpp:13850`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13290-13291`): 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_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:13257-13258`): 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:2361-2363`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2388-2396`, `margin.maintenance_long` `pine_adapter.cpp:2403`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 95f5cdfa..12d73bad 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,19 +47,19 @@ 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:2738-2749; `entry()` pine_adapter.cpp:5311, `order()` pine_adapter.cpp:10532) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2738-2749; `entry()` pine_adapter.cpp:5311, `order()` pine_adapter.cpp:10512) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | 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:8417 → `Flatten{}` pine_adapter.cpp:8446-8447; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2708-3915 → `host.replace` pine_adapter.cpp:2724-2804) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4723-4769, `cancel_all` pine_adapter.cpp:18129-18144; `live_by_source_key_` pine_adapter.hpp:1988; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4723-4769, `cancel_all` pine_adapter.cpp:18063-18078; `live_by_source_key_` pine_adapter.hpp:1988; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5535-5570, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7525-7542) | **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:1270, `CancelReason::OwnerGone` native_order.hpp:949 | K | — | F:A6 O:A8 O:A9 S:O5 | FIFO *ordering* is host-computed (O) | +| 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:5535-5570, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7505-7522) | **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:1270, `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:2747-2753) | **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:1092-1093 | 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:13068; lowering `submit_one` pine_adapter.cpp:9092-9104, `BindCohort` pine_adapter.cpp:9593-9599, `submit_leg` pine_adapter.cpp:9617-9641; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8697-8700; `materialize_relative_exits` pine_adapter.cpp:17419-17460, pine_adapter.hpp:1823-1835) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19201-19230) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:2000-2018); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10932-10956), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15695, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15929) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:684-686 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:13035; lowering `submit_one` pine_adapter.cpp:9072-9084, `BindCohort` pine_adapter.cpp:9573-9579, `submit_leg` pine_adapter.cpp:9597-9621; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8677-8680; `materialize_relative_exits` pine_adapter.cpp:17353-17394, pine_adapter.hpp:1823-1835) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19134-19163) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:2000-2018); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10912-10936), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15662, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15896) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:684-686 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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,9 +68,9 @@ 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:5692-5714; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1080 | 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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11843-11855) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11756-11893) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11811-11823) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11724-11861) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4022-4068) | **partial** — the read exists `trail_state` native_host.hpp:1080; `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:287-289) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9643-9648, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11756-11893, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8667-8893, `trail_one_shot` pine_adapter.cpp:9732-9861 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9623-9628, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11724-11861, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8647-8873, `trail_one_shot` pine_adapter.cpp:9712-9841 | n/a | A | — | F:B3 S:T2 | R5-1 | ### 1.3 Sizing bases and reservations @@ -81,13 +81,13 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | SZ3 | Percent-of-equity sizing (`default_sizing_units` pine_adapter.cpp:2596-2651) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4957-4959 **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: rounded equity `source_money_round` pine_adapter.cpp:2548, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2557-2559) | **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:2609, `typed_entry_units` pine_adapter.cpp:2627; `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:2588; `percent_commission_live_equity` pine_adapter.cpp:4329-4369) | **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. | -| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2514-2529, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6697-6723) | **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`. | +| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2514-2529, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6672-6698) | **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:591, 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:4973-4976; 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:12542-12570, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:9019-9329) | **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. | +| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:12510-12538, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:8999-9309) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4444-4459) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6758), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7965), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12799), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13395, pine_adapter.cpp:13448) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15743) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18796) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7227-7578; ledger `close_batch_bar_` pine_adapter.hpp:2041-2052; 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. | +| 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:6733), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7945), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12766), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13362, pine_adapter.cpp:13415) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15710) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18730) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7207-7558; ledger `close_batch_bar_` pine_adapter.hpp:2041-2052; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | ### 1.4 Margin, admission, liquidation, risk @@ -95,21 +95,21 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2362) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13290-13293) is TV. | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13257-13260) is TV. | | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6230-6279, cap left unset `max_open_lots` pine_adapter.cpp:2361-2363) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13344-13409 and leaves the kernel gate unset pine_adapter.cpp:13343-13345) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13999-14000, `margin_pct` pine_adapter.cpp:13407) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14099-14277, `submit_margin_call_units` pine_adapter.cpp:14238-14284, `schedule_margin_call_path` pine_adapter.cpp:18282-18359) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14030-14066) | **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:983 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. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13311-13376 and leaves the kernel gate unset pine_adapter.cpp:13310-13312) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13966-13967, `margin_pct` pine_adapter.cpp:13374) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14066-14244, `submit_margin_call_units` pine_adapter.cpp:14205-14251, `schedule_margin_call_path` pine_adapter.cpp:18216-18314) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13997-14033) | **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:983 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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1308 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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16347-16363, `apply_fx_opening_margin_slice` pine_adapter.cpp:18360-18409) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16871-16910) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13379-13382) | **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:13960-13974, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14986-15016) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15029-15263) | **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:13770-13849) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:13946, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14005, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14713-14753, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14237-14364, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14237) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, the restore's `+1e-6` floor and the frozen signal-time units (M10); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15267-15325) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16314-16330, `apply_fx_opening_margin_slice` pine_adapter.cpp:18315-18343) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16805-16844) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13346-13349) | **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:13927-13941, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14953-14983) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14996-15230) | **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:13737-13816) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:13955, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14014, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14722-14762, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14246-14373, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14246) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15234-15292) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -117,10 +117,10 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:881, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7523-7527 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16182) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7065-7069; `execute_current` legal native_host.hpp:1092 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16149) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7065-7069; `execute_current` legal native_host.hpp:1092 | 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:7008 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7059 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1061 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:863, `NativeInputContext` native_host.hpp:761-766 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5632, `flush_coof_tail` pine_adapter.cpp:16325, `end_coof_recalc` pine_adapter.cpp:5653) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6919, native_execution_consumer.cpp:6919-6949; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5632, `flush_coof_tail` pine_adapter.cpp:16292, `end_coof_recalc` pine_adapter.cpp:5653) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6919, native_execution_consumer.cpp:6919-6949; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:866 fires per accepted realtime print native_execution_consumer.cpp:7147-7151; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | | 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:7517-7634 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | @@ -145,7 +145,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:5685-5732, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6268-6276; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | | FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12032-12041) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:472-479) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:12026-12035, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11994-12003, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:1190 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:8705). **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). | @@ -210,10 +210,10 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19201-19230 | 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:19134-19163 | 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:523, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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:603. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1273 | 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) | +| 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:1274 | 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) | | m | `request.security` machinery | `register_security_eval` engine_security.cpp:27, `set_native_security_feed` engine_aux_security.cpp:76-127, `SecurityEvalState` engine.hpp:1156-1360 carry Pine semantics (lookahead, gaps, Heikin-Ashi, KI-55 range start) and are driven only by the source layer | HASH-VISIBLE / measured path | F planned to move both TUs and the state into the source host after native HTF. **R5-6 narrows this:** L6 reuses the feed store and routing, so those stay kernel; only the Pine-only evaluator semantics remain candidates. Do last. **Status (lane L12b): done** — the evaluator semantics are `source::PineSecurityEvalState` + `src/source/pine_security_eval.cpp`; the kernel keeps the registry, aggregator, feed store and one generic step | F S | | n | S-only proposals, not adopted | A generic `FillModel` interface with a TV model installed by the adapter (for the TradingView money / tick / path comment blocks in `engine.hpp` and the dead helpers of item e); "emit generic FIFO facts and let an adapter policy produce the KI-62 scratch" | — | Superseded by R5-3 (price-grid lane) and R5-1 (TV fill specifics stay behind `resolve_execution_terms`); the dead-helper part is item e; there is no scratch code left to re-lower (item d) | S | | o | `pineforge::admission` (market-admission journal) — **N14 addition** | was `include/pineforge/market_admission.hpp` (275 lines, installed by a kernel-only build) + `src/market_admission.cpp` (kernel TU) until N14 moved both under `source/` (`include/pineforge/source/market_admission.hpp`, `src/source/market_admission.cpp`): the observation journal of TradingView admission reviews; every `Configuration` field is a `strategy()` declaration parameter. No kernel TU consumed it (engine.hpp included it and used nothing; the hash helper in `src/broker_state_hash_internal.hpp` had one caller, `src/source/pine_state_hash.cpp`) | NEUTRAL (pure move; the fold is unchanged, so no hash domain moves) | **Status (N14): done** — moved whole to `include/pineforge/source/market_admission.hpp` + `src/source/market_admission.cpp` (`PINEFORGE_SOURCE_LAYER_SOURCES`); `hash_admission_field` moved verbatim into the source hash TU; `check_market_admission_schema.py` re-pointed; `MarketAdmissionDraft` / `MarketAdmissionJournal` spellings and the `market_admission_v2` inline namespace unchanged. ADR-0001 no longer calls it a generic stem | audit (Opus N14) | @@ -522,11 +522,11 @@ line numbers are this tree's (683a82f); kernel semantics are | 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:2362); 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:13290-13291; 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:16871, `update_risk_state` pine_adapter.cpp:13931-13958): 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:13283), 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_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:13257-13258; 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:16805, `update_risk_state` pine_adapter.cpp:13898-13925): 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:13250), 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:1200-1208): +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:17724-17733); 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:16341); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16453), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15016-15025, `submit_intraday_loss_close` pine_adapter.cpp:14986-15014), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17228); 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:5983, `:7672`, `:8396`, `:8524`, `:11513`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18127-18130) | `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:13922-13924); 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:13828-13849, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16308); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16420), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14983-14992, `submit_intraday_loss_close` pine_adapter.cpp:14953-14981), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17162); 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:5983, `:7652`, `:8376`, `:8504`, `:11481`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18061-18064) | `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:13889-13891); 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:13795-13816, `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, @@ -1053,10 +1053,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:8808). Full-table scans + (`has_full_entry_bracket` pine_adapter.cpp:8788). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1696, `revive_brackets_after_margin` pine_adapter.cpp:1918, - `preserved_by_close_all` pine_adapter.cpp:11544), and + `preserved_by_close_all` pine_adapter.cpp:11512), 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. @@ -1300,7 +1300,7 @@ unchanged after it. | 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5779) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290); 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:16182-16211) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:16149-16178) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | @@ -1345,7 +1345,7 @@ unchanged after it. | **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:16347-16363) 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:2646) 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:16314-16330) 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:2646) 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. | @@ -1531,7 +1531,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:1317) (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:373), 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:13754-13852); 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:13721-13819); 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:4654-4667 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 956b2ab5..17f4693c 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1458,7 +1458,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:14133-14157`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:14100-14124`), 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 @@ -1487,8 +1487,14 @@ market execution at the open, sized on the open's money; the kernel's mark check rests its slice at the remaining path's adverse extreme and `CalculationOnly` executes at the calculation, so no kernel check kind books that row. On the same money the pre-open slice and the kernel's -`AfterApplied` point part on the one-contract band, on the restore's `+1e-6` -floor and on the frozen signal-time units. The opening gate parts both ways: +`AfterApplied` point part on the one-contract band and on the restore's +`+1e-6` floor. They parted on the frozen units too until R5 lane PAR-MARGIN: +a default-percent stop entry above 100 % was frozen at its signal but sized +again at its fill, as `ab9714be` did. Eight `lab tv` tapes +(`tests/fixtures/margin_entry_bar/pm-m10-*`) show TradingView freezes it at +the snapped stop level, or at the signal close when the stop was already +marketable, whatever the percentage, and books its margin calls on that +book, so the adapter now does too. The opening gate parts both ways: the kernel declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity. A leveraged opening (margin below 100 %) is checked on its own entry bar: four `lab tv` @@ -2173,13 +2179,13 @@ under the ticket the model or the run named. A host running its own forced close states the cause on the row it produced. `closed_trade_close_cause(i)` (`engine.hpp:1833`) is the C++ read and -`strategy_closed_trade_close_cause` (`pineforge.h:1273`) the C one, with the +`strategy_closed_trade_close_cause` (`pineforge.h:1274`) the C one, with the same numbering: `-1` for a bad index or a NULL handle, `0` UNKNOWN, `1` SCRIPT, `2` BRACKET, `3` MARGIN_CALL, `4` INTRADAY_LOSS_CAP, `5` INTRADAY_FILL_CAP, `6` RANGE_END. A row closed at the end of the run (`open_at_end`) always answers `6`, ahead of every other cause. The ticket a row was booked under is `strategy_closed_trade_entry_id` / -`_exit_id` / `_exit_comment` (`pineforge.h:1196-1226`), which index exactly the +`_exit_id` / `_exit_comment` (`pineforge.h:1197-1227`), which index exactly the rows of `fill_report`'s trade array and take any handle this engine produces — including a `pf_strategy_t` from `strategy_native_host_create_v1`, which is how a C host reads back the ticket its own margin model declared. diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index af2b92cb..aa7a6ad5 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -613,22 +613,22 @@ span the same rows followed by the range-end rows `report_open_position_at_end` native_run_spec.hpp:614 adds. In C the report's `pf_report_t::trades` pineforge.h:409 carries the numeric fields, and the strings and the cause come from `strategy_closed_trade_entry_id` -pineforge.h:1191, `strategy_closed_trade_exit_id` pineforge.h:1204, -`strategy_closed_trade_exit_comment` pineforge.h:1206 and -`strategy_closed_trade_close_cause` pineforge.h:1273, which index exactly the +pineforge.h:1192, `strategy_closed_trade_exit_id` pineforge.h:1205, +`strategy_closed_trade_exit_comment` pineforge.h:1207 and +`strategy_closed_trade_close_cause` pineforge.h:1274, which index exactly the rows of that array. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | | `strategy.closedtrades` | `closed_trade_count` engine.hpp:1807 | `total_trades` pineforge.h:406 | `native_open_lots_strategy.cpp` | `closed_trade_count` counts booked closed rows. The C report's `total_trades` also includes range-end rows when `report_open_position_at_end` is enabled; filter `pf_trade_t::open_at_end == 0` to match Pine `strategy.closedtrades`. | | `strategy.closedtrades.first_index` | none — the kernel keeps every row | — | `native_open_lots_strategy.cpp` | TradingView drops old rows past a 9000-trade cap and advances `first_index` when it does. The kernel caps nothing, so the first index is always 0 and codegen emits the literal. | -| `strategy.closedtrades.entry_id()` | `entry_id` engine.hpp:183 | `strategy_closed_trade_entry_id` pineforge.h:1191 | `native_open_lots_strategy.cpp` | The same string the lot carried as `entry_label` native_host.hpp:333. | -| `strategy.closedtrades.entry_comment()` | `entry_comment` engine.hpp:184 | — | `native_open_lots_strategy.cpp` | No C accessor: `strategy_closed_trade_entry_id` pineforge.h:1191 answers the id, not the comment, and `pf_trade_t` carries no strings. A C host that needs it reads the open lot's `entry_comment` (`pf_native_open_lot_v1` native_c_api.h:1732) before the close. | +| `strategy.closedtrades.entry_id()` | `entry_id` engine.hpp:183 | `strategy_closed_trade_entry_id` pineforge.h:1192 | `native_open_lots_strategy.cpp` | The same string the lot carried as `entry_label` native_host.hpp:333. | +| `strategy.closedtrades.entry_comment()` | `entry_comment` engine.hpp:184 | — | `native_open_lots_strategy.cpp` | No C accessor: `strategy_closed_trade_entry_id` pineforge.h:1192 answers the id, not the comment, and `pf_trade_t` carries no strings. A C host that needs it reads the open lot's `entry_comment` (`pf_native_open_lot_v1` native_c_api.h:1732) before the close. | | `strategy.closedtrades.entry_bar_index()` | `entry_bar_index` engine.hpp:181 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | Copied from the lot. | | `strategy.closedtrades.entry_time()` | `entry_time` engine.hpp:173 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | Unix ms. | | `strategy.closedtrades.entry_price()` | `entry_price` engine.hpp:175 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.exit_id()` | `exit_id` engine.hpp:186 | `strategy_closed_trade_exit_id` pineforge.h:1204 | `native_margin_strategy.cpp` | For a kernel liquidation this is the model's own `liquidation_label` native_run_spec.hpp:251, which is how a reporting layer classifies the row. | -| `strategy.closedtrades.exit_comment()` | `exit_comment` engine.hpp:185 | `strategy_closed_trade_exit_comment` pineforge.h:1206 | `native_margin_strategy.cpp` | | +| `strategy.closedtrades.exit_id()` | `exit_id` engine.hpp:186 | `strategy_closed_trade_exit_id` pineforge.h:1205 | `native_margin_strategy.cpp` | For a kernel liquidation this is the model's own `liquidation_label` native_run_spec.hpp:251, which is how a reporting layer classifies the row. | +| `strategy.closedtrades.exit_comment()` | `exit_comment` engine.hpp:185 | `strategy_closed_trade_exit_comment` pineforge.h:1207 | `native_margin_strategy.cpp` | | | `strategy.closedtrades.exit_bar_index()` | `exit_bar_index` engine.hpp:182 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | | | `strategy.closedtrades.exit_time()` | `exit_time` engine.hpp:174 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | | | `strategy.closedtrades.exit_price()` | `exit_price` engine.hpp:176 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | | @@ -643,7 +643,7 @@ rows of that array. Pine has no word for *why* a row closed. The native row does: `closed_trade_close_cause` engine.hpp:1833 (C: -`strategy_closed_trade_close_cause` pineforge.h:1273) distinguishes a script +`strategy_closed_trade_close_cause` pineforge.h:1274) distinguishes a script close, a liquidation, a risk flatten and the range end — the kernel states the last three itself. The bracket-leg cause (`2`) is the kernel's too for a close its owner's fill armed — a `WaitForApplied` owner relation with an intent that diff --git a/docs/pine-adapter-kernel-notes.md b/docs/pine-adapter-kernel-notes.md index bbbd138d..eff81e22 100644 --- a/docs/pine-adapter-kernel-notes.md +++ b/docs/pine-adapter-kernel-notes.md @@ -308,6 +308,12 @@ The pair context exists only inside the ordinary atomic two-stop scan. No callback or stable-frame ABI read occurs between its two fills. ``` +Since R5 lane PAR-MARGIN the adapter keeps the placement quantity above +100 % as well: eight `lab tv` tapes of a 390-400 % stop on NYSE:F +(`tests/fixtures/margin_entry_bar/pm-m10-*`) open the placement quotient, never +the fill's (`tests/test_adapter_margin_schedule_differential.cpp`, "M10 on +tapes"). + ```text design-declined-reversal-close-leg: called at the KI-54 reversal-decline site with the just-declined MARKET reversal entry. Flags every pending diff --git a/include/pineforge/pineforge.h b/include/pineforge/pineforge.h index 55098ae5..58fa05a9 100644 --- a/include/pineforge/pineforge.h +++ b/include/pineforge/pineforge.h @@ -1071,10 +1071,11 @@ typedef enum pf_fill_qty_partition_e { * `pyramid_entries[0].qty - pyramid_entries[1].qty` after closing both * lots (the short-seed collision path). Both kernels are * modelled; each reports `close_only` 1 when it opens nothing. - * - `2` DEFAULT_STOP_PLACEMENT -- the DEFAULT percent_of_equity <= 100 - * pure STOP entry's placement size (`default_stop_placement_qty`, - * round-7 family K), when the placement-size rule says the - * fill consumes it: created flat, filling from flat, positive fill. + * - `2` DEFAULT_STOP_PLACEMENT -- the DEFAULT percent_of_equity pure + * STOP entry's placement size, at any percentage + * (`default_stop_placement_qty`, round-7 family K), when the + * placement-size rule says the fill consumes it: created flat, filling + * from flat, positive fill. * - `3` AT_FILL -- default sizing at the slipped fill. * @p close_only receives 1 when the kernel's close-only predicate fires * -- the fill closes against the live opposite position and that diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 0e6aac59..162f3317 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -6374,12 +6374,17 @@ void PineExecutionAdapter::entry(const SourceId& id, bool is_long, double limit_ const bool typed_sized = !default_sized && (cash_sized || percent_sized); const bool fixed_priced_reverse = reverses && !default_sized && priced && !cash_sized; const bool cash_priced_reverse = reverses && !default_sized && priced && cash_sized; - const bool default_stop_scope = default_sized && pure_stop_entry + // A default-percent stop entry freezes its units at placement whatever + // the percentage (see the freeze below); ab9714be's own placement + // admission for it (pine_strategy_commands.cpp:307-310) is the one the + // percentage caps at 100, and above 100 the affordability arm admits it. + const bool default_stop_freeze = default_sized && pure_stop_entry && finite_positive(stop_price) - && config_.default_qty_type == static_cast(QtyType::PERCENT_OF_EQUITY) + && config_.default_qty_type == static_cast(QtyType::PERCENT_OF_EQUITY); + const bool default_stop_scope = default_stop_freeze && config_.default_qty_value <= 100.0; double default_stop_sizing_price = kNaN; - if (default_stop_scope && finite_positive(staged_.syminfo.mintick)) { + if (default_stop_freeze && finite_positive(staged_.syminfo.mintick)) { stop_price = directional_tick(stop_price, staged_.syminfo.mintick, is_long); const double signal = source_point ? nearest_tick(source_point->price, staged_.syminfo.mintick) : kNaN; @@ -6722,11 +6727,16 @@ void PineExecutionAdapter::entry(const SourceId& id, bool is_long, double limit_ if (fixed_priced_reverse) { snapshot.frozen_reversal_transaction = std::abs(current) + normalized_qty; } - if (default_stop_scope && finite_positive(default_stop_sizing_price)) { + if (default_stop_freeze && finite_positive(default_stop_sizing_price)) { // A default-sized stop entry freezes its quantity against the // directionally snapped level, except an already-marketable stop // which is a next-open market order and therefore freezes at the - // source close. Neither path re-sizes at its later fill quote. + // source close. Neither path re-sizes at its later fill quote -- + // above 100 % too, where ab9714be sized at the fill: eight lab tv + // tapes of a 390-400 % stop on NYSE:F (R5 lane PAR-MARGIN, + // tests/fixtures/margin_entry_bar/pm-m10-*) open the snapped level's + // quotient when the stop was not yet marketable and the signal + // close's when it was, never the fill's. snapshot.sizing.price = default_stop_sizing_price; } if (default_sized && finite_positive(snapshot.sizing.price)) { @@ -6736,7 +6746,7 @@ void PineExecutionAdapter::entry(const SourceId& id, bool is_long, double limit_ } // ab9714be pine_fills.cpp:7139: non-pure-stop priced entries size at fill time using calc_qty(fill_price) snapshot.sizing.at_fill = (config_.calc_on_order_fills && coof_recalc_active_) - || (priced && !default_stop_scope); + || (priced && !default_stop_freeze); } // R5 R2: a declaration-level default quantity whose sizing price IS the // signal rule and whose quantity is frozen at the command is exactly what @@ -12666,9 +12676,8 @@ native_order::ExecutionTerms PineExecutionAdapter::resolve_terms( && (!(source.family == PineOrderFamily::Entry && finite_positive(source.exit_levels.stop) && !finite_positive(source.exit_levels.limit)) - || (config_.default_qty_type - == static_cast(QtyType::PERCENT_OF_EQUITY) - && config_.default_qty_value <= 100.0))) { + || config_.default_qty_type + == static_cast(QtyType::PERCENT_OF_EQUITY))) { result.units = source.sizing.frozen_units; } else if (config_.default_qty_type == static_cast(QtyType::FIXED)) { result.units = config_.default_qty_value; @@ -18935,7 +18944,6 @@ int PendingIntentView::probe_fill_qty(int index, double fill_price, double* qty, && std::isnan(snapshot.requested_qty) && owner_->config_.default_qty_type == static_cast(QtyType::PERCENT_OF_EQUITY) - && owner_->config_.default_qty_value <= 100.0 && finite_positive(snapshot.sizing.frozen_units); const bool default_stop = default_stop_shape && finite_positive(fill_price) diff --git a/tests/fixtures/margin_entry_bar/README.md b/tests/fixtures/margin_entry_bar/README.md index 83d353ee..9b9611e8 100644 --- a/tests/fixtures/margin_entry_bar/README.md +++ b/tests/fixtures/margin_entry_bar/README.md @@ -27,3 +27,32 @@ All four export windows are 2025-04-01 .. 2026-05-01; each script trades once. `bars.inc` holds each event's signal bar through the entry bar + 4, from the corpus feed `corpus/data/derived/ohlcv_ETH-USDT-USDT_15m.csv` materialized by `scripts/derive_corpus_feeds.py` at corpus gitlink eede4a2. + +## M10: the book a default-percent stop entry above 100 % opens (R5 lane PAR-MARGIN) + +Eight more `lab tv` exports on NYSE:F 15m, same channel and layout, margin 25, +one buy stop placed on the session's last bar (15:45 America/New_York) and +filled at the next session's open, closed 3 bars after its entry bar: +`pm-m10-f-k4-*` at 400 % with the stop at 0.9 x close (marketable when +placed), `pm-m10-f-gapup-*` at 390 % with the stop at round(1.002 x close, 2) +(not marketable when placed, gapped through by the open). TradingView opens the +quotient of the signal close on its tick for the first and of the snapped +stop level for the second, never the fill's, and books its margin calls on +that book. `m10_bars.inc` holds each event's bars as TradingView reports them, +read by the two `pm-m10-f-ohlc*` exports (each bar printed as an order +comment); they equal the lab feed `80f404ae85ef`, half cents included. +`tests/test_adapter_margin_schedule_differential.cpp` (section "M10 on tapes") +replays all eight. Campaign notes: `tv-tape--`. + +| probe | window | TradingView | tvTradesCsvHash | +|---|---|---|---| +| `pm-m10-f-k4-0402` | 2026-03-25 .. 2026-04-07 | book 3427; margin calls 176 @11.38 | `3bd3141a7089f30c` | +| `pm-m10-f-k4-0410` | 2025-04-02 .. 2025-04-15 | book 4219; margin calls none | `8911072fc9bfb7ad` | +| `pm-m10-f-k4-1007` | 2025-09-29 .. 2025-10-10 | book 3144; margin calls 980 @12.11 | `bd8d6bc27e858bfb` | +| `pm-m10-f-k4-0309` | 2026-03-02 .. 2026-03-12 | book 3294; margin calls 4 @11.83, 56 @11.81 | `ab895fec3d476066` | +| `pm-m10-f-gapup-0501` | 2025-04-24 .. 2025-05-06 | book 3896; margin calls 376 @10.03 | `c672eb182acd2a16` | +| `pm-m10-f-gapup-0331` | 2026-03-24 .. 2026-04-03 | book 3475; margin calls 156 @11.28 | `8f54192f161b9013` | +| `pm-m10-f-gapup-0424` | 2025-04-17 .. 2025-04-29 | book 3987; margin calls 92 @9.81 | `79a6aa531e7bfbb3` | +| `pm-m10-f-gapup-0527` | 2025-05-19 .. 2025-05-30 | book 3757; margin calls 204 @10.35 | `f6222f524d46cd37` | +| `pm-m10-f-ohlc` | 2025-04-01 .. 2026-04-10 | 24 bars' open, high, low and close printed as entry comments | `8b006a00aff01690` | +| `pm-m10-f-ohlc2` | 2025-04-01 .. 2026-04-10 | 24 bars' open, high, low and close printed as entry comments | `52160db7208aea53` | diff --git a/tests/fixtures/margin_entry_bar/m10_bars.inc b/tests/fixtures/margin_entry_bar/m10_bars.inc new file mode 100644 index 00000000..1db49918 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/m10_bars.inc @@ -0,0 +1,69 @@ +// NYSE:F 15m bars per M10 event (R5 lane PAR-MARGIN): the signal bar (the session's last, +// 15:45 America/New_York) through the entry bar + 4, as TradingView itself reports them -- +// read through the tapes pm-m10-f-ohlc and pm-m10-f-ohlc2 (every bar's open, high, low and +// close printed as an order comment), which equal the lab feed 80f404ae85ef, half cents +// included. {timestamp_ms, open, high, low, close} +constexpr FeedBar kM10_k4_0402[6] = { // signal bar 2026-04-01 19:45 UTC + {1775072700000LL, 11.655, 11.69, 11.645, 11.67}, + {1775136600000LL, 11.485, 11.5, 11.38, 11.4}, + {1775137500000LL, 11.4, 11.42, 11.38, 11.42}, + {1775138400000LL, 11.425, 11.43, 11.37, 11.38}, + {1775139300000LL, 11.385, 11.405, 11.36, 11.38}, + {1775140200000LL, 11.385, 11.545, 11.385, 11.43}, +}; +constexpr FeedBar kM10_k4_0410[6] = { // signal bar 2025-04-09 19:45 UTC + {1744227900000LL, 9.435, 9.52, 9.385, 9.485}, + {1744291800000LL, 9.12, 9.275, 9.09, 9.17}, + {1744292700000LL, 9.17, 9.195, 9.035, 9.065}, + {1744293600000LL, 9.06, 9.09, 9.03, 9.07}, + {1744294500000LL, 9.07, 9.14, 9.06, 9.08}, + {1744295400000LL, 9.08, 9.19, 9.08, 9.155}, +}; +constexpr FeedBar kM10_k4_1007[6] = { // signal bar 2025-10-06 19:45 UTC + {1759779900000LL, 12.695, 12.735, 12.685, 12.72}, + {1759843800000LL, 12.5, 12.515, 12.105, 12.13}, + {1759844700000LL, 12.11, 12.14, 11.98, 11.985}, + {1759845600000LL, 11.98, 12, 11.87, 11.87}, + {1759846500000LL, 11.875, 11.93, 11.87, 11.91}, + {1759847400000LL, 11.91, 11.925, 11.86, 11.86}, +}; +constexpr FeedBar kM10_k4_0309[6] = { // signal bar 2026-03-06 20:45 UTC + {1772829900000LL, 12.1, 12.15, 12.07, 12.135}, + {1773063000000LL, 11.91, 11.995, 11.835, 11.98}, + {1773063900000LL, 11.98, 11.985, 11.85, 11.87}, + {1773064800000LL, 11.86, 11.925, 11.83, 11.83}, + {1773065700000LL, 11.83, 11.915, 11.81, 11.85}, + {1773066600000LL, 11.85, 11.91, 11.835, 11.9}, +}; +constexpr FeedBar kM10_gapup_0501[6] = { // signal bar 2025-04-30 19:45 UTC + {1746042300000LL, 9.945, 10.04, 9.945, 9.99}, + {1746106200000LL, 10.15, 10.23, 10.025, 10.07}, + {1746107100000LL, 10.07, 10.12, 10.05, 10.1}, + {1746108000000LL, 10.095, 10.245, 10.095, 10.225}, + {1746108900000LL, 10.225, 10.245, 10.22, 10.24}, + {1746109800000LL, 10.235, 10.255, 10.195, 10.245}, +}; +constexpr FeedBar kM10_gapup_0331[6] = { // signal bar 2026-03-30 19:45 UTC + {1774899900000LL, 11.17, 11.21, 11.165, 11.2}, + {1774963800000LL, 11.37, 11.37, 11.28, 11.31}, + {1774964700000LL, 11.325, 11.42, 11.325, 11.42}, + {1774965600000LL, 11.41, 11.435, 11.35, 11.405}, + {1774966500000LL, 11.41, 11.41, 11.37, 11.41}, + {1774967400000LL, 11.41, 11.43, 11.37, 11.37}, +}; +constexpr FeedBar kM10_gapup_0424[6] = { // signal bar 2025-04-23 19:45 UTC + {1745437500000LL, 9.77, 9.795, 9.755, 9.765}, + {1745501400000LL, 9.88, 9.97, 9.815, 9.955}, + {1745502300000LL, 9.955, 10.015, 9.94, 10}, + {1745503200000LL, 9.99, 10.06, 9.985, 10}, + {1745504100000LL, 9.995, 10.035, 9.97, 10}, + {1745505000000LL, 9.99, 10.01, 9.97, 10.01}, +}; +constexpr FeedBar kM10_gapup_0527[6] = { // signal bar 2025-05-23 19:45 UTC + {1748029500000LL, 10.4, 10.4, 10.35, 10.36}, + {1748352600000LL, 10.46, 10.475, 10.35, 10.355}, + {1748353500000LL, 10.36, 10.38, 10.27, 10.28}, + {1748354400000LL, 10.28, 10.335, 10.27, 10.305}, + {1748355300000LL, 10.3, 10.355, 10.29, 10.325}, + {1748356200000LL, 10.33, 10.35, 10.32, 10.35}, +}; diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/meta.json b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/meta.json new file mode 100644 index 00000000..58a586e9 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-m10-f-gapup-0331", + "slug": "pm-m10-f-gapup-0331", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:25:57.462Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2026-03-24", + "to": "2026-04-03" + }, + "pine_sha256": "8c19125d636f498a1bb819879d71d5ea189908effc6f011344fd27ad018cf869", + "tv_trades_sha256": "8f54192f161b9013c958737b557244686e00fd285c965001188a5b3159ae3e5d", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/metrics.json b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/metrics.json new file mode 100644 index 00000000..5cdd22b2 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-m10-f-gapup-0331", + "symbol": "NYSE:F", + "interval": "15", + "from": "2026-03-24", + "to": "2026-04-03", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "8c19125d636f498a1bb819879d71d5ea189908effc6f011344fd27ad018cf869", + "tvTradesCsvHash": "8f54192f161b9013c958737b557244686e00fd285c965001188a5b3159ae3e5d", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74615", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1774310400000, + "to": 1775174400000 + }, + "returnedRange": { + "from": 1774359000000, + "to": 1775159100000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f3c03e8a7a2e6438f2871a8949c9e02927538f1ff116d69a0c14e16d86c9972f", + "decodedReportSha256": "344bf5592dea852a9cda8a7cca49046ad3e7638b65d42c45b733f3f1619f0242", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:25:57.462Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/strategy.pine b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/strategy.pine new file mode 100644 index 00000000..1258522f --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/strategy.pine @@ -0,0 +1,14 @@ +// R5 lane PAR-MARGIN M10 (the frozen shape): a default-percent (390 %) buy stop placed ABOVE the +// session's last close (2026-3-30 15:45 America/New_York), not marketable when placed, gapped +// through by the next session's open. Margin 25: the entry bar's low breaches. Which book does +// TradingView size the entry, and its entry-bar margin call, on -- the signal-time units or the +// units the fill opens? Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-m10-f-gapup-0331", overlay=true, initial_capital=10000, + default_qty_type=strategy.percent_of_equity, default_qty_value=390, pyramiding=1, + margin_long=25, margin_short=25, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("America/New_York", 2026, 3, 30, 15, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, stop = math.round(close * 1.002, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/tv_trades.csv new file mode 100644 index 00000000..9816bdaf --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0331/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2026-03-31 21:30,Margin call,11.28,156,1773.72,-14.04,-0.79,0,0,0.00,-14.04,-0.79,-14.04,-0.14,0 +1,Entry long,2026-03-31 21:30,L,11.37,156,1773.72,-14.04,-0.79,0,0,0.00,-14.04,-0.79,-14.04,-0.14,0 +2,Exit long,2026-03-31 22:30,timeout,11.41,3319,37737.03,132.76,0.35,0,232.33,0.62,-298.71,-0.79,118.72,1.19,4 +2,Entry long,2026-03-31 21:30,L,11.37,3319,37737.03,132.76,0.35,0,232.33,0.62,-298.71,-0.79,118.72,1.19,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/meta.json b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/meta.json new file mode 100644 index 00000000..d9bf7487 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-m10-f-gapup-0424", + "slug": "pm-m10-f-gapup-0424", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:26:01.212Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-17", + "to": "2025-04-29" + }, + "pine_sha256": "83352a6556a1affb5cc8da8a9d8a5846b881dda55b0081ec3746e2278b974fe6", + "tv_trades_sha256": "79a6aa531e7bfbb3cc82d46f3a4b8283efdb1c7140c7a5236b87bf88706517ca", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/metrics.json b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/metrics.json new file mode 100644 index 00000000..71d5bdfd --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-m10-f-gapup-0424", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-17", + "to": "2025-04-29", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "83352a6556a1affb5cc8da8a9d8a5846b881dda55b0081ec3746e2278b974fe6", + "tvTradesCsvHash": "79a6aa531e7bfbb3cc82d46f3a4b8283efdb1c7140c7a5236b87bf88706517ca", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74616", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1744848000000, + "to": 1745884800000 + }, + "returnedRange": { + "from": 1744896600000, + "to": 1745869500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "c1fc9360f4647084aa87914f9cd565ca2043ff04c07fefdc27ebf0d3029744db", + "decodedReportSha256": "83905ac33b015f0baa2f3a90ad7accd138f002a32d13edd5e7d26a742110d804", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:26:01.208Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/strategy.pine b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/strategy.pine new file mode 100644 index 00000000..70e7590b --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/strategy.pine @@ -0,0 +1,14 @@ +// R5 lane PAR-MARGIN M10 (the frozen shape): a default-percent (390 %) buy stop placed ABOVE the +// session's last close (2025-4-23 15:45 America/New_York), not marketable when placed, gapped +// through by the next session's open. Margin 25: the entry bar's low breaches. Which book does +// TradingView size the entry, and its entry-bar margin call, on -- the signal-time units or the +// units the fill opens? Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-m10-f-gapup-0424", overlay=true, initial_capital=10000, + default_qty_type=strategy.percent_of_equity, default_qty_value=390, pyramiding=1, + margin_long=25, margin_short=25, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("America/New_York", 2025, 4, 23, 15, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, stop = math.round(close * 1.002, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/tv_trades.csv new file mode 100644 index 00000000..0e1d9241 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0424/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-24 21:30,Margin call,9.81,92,908.96,-6.44,-0.71,0,0,0.00,-6.44,-0.71,-6.44,-0.06,0 +1,Entry long,2025-04-24 21:30,L,9.88,92,908.96,-6.44,-0.71,0,0,0.00,-6.44,-0.71,-6.44,-0.06,0 +2,Exit long,2025-04-24 22:30,timeout,9.99,3895,38482.6,428.45,1.11,0,701.1,1.82,-272.65,-0.71,422.01,4.22,4 +2,Entry long,2025-04-24 21:30,L,9.88,3895,38482.6,428.45,1.11,0,701.1,1.82,-272.65,-0.71,422.01,4.22,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/meta.json b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/meta.json new file mode 100644 index 00000000..6ddcb56d --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-m10-f-gapup-0501", + "slug": "pm-m10-f-gapup-0501", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:25:53.574Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-24", + "to": "2025-05-06" + }, + "pine_sha256": "954ea444c286b7ab9d9f75bc4f13a3225472a078dcb00ff166a40662c97cc18c", + "tv_trades_sha256": "c672eb182acd2a16837cdcc9922dbc6dfa612d3e136bd13c06dcc799097690b3", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/metrics.json b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/metrics.json new file mode 100644 index 00000000..eaa56e36 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-m10-f-gapup-0501", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-24", + "to": "2025-05-06", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "954ea444c286b7ab9d9f75bc4f13a3225472a078dcb00ff166a40662c97cc18c", + "tvTradesCsvHash": "c672eb182acd2a16837cdcc9922dbc6dfa612d3e136bd13c06dcc799097690b3", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74614", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1745452800000, + "to": 1746489600000 + }, + "returnedRange": { + "from": 1745501400000, + "to": 1746474300000 + }, + "rangeProof": "covered", + "rawFrameSha256": "94e3f59dbba9d251f5dbaf9c1a10d73e9fb04975ef57058fa6a0639330fffcb6", + "decodedReportSha256": "f36e403cf4a537071291547dbcbea2dd7e79867ff36d9b89804b7bda37009cd0", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:25:53.572Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/strategy.pine b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/strategy.pine new file mode 100644 index 00000000..32b2d6f3 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/strategy.pine @@ -0,0 +1,14 @@ +// R5 lane PAR-MARGIN M10 (the frozen shape): a default-percent (390 %) buy stop placed ABOVE the +// session's last close (2025-4-30 15:45 America/New_York), not marketable when placed, gapped +// through by the next session's open. Margin 25: the entry bar's low breaches. Which book does +// TradingView size the entry, and its entry-bar margin call, on -- the signal-time units or the +// units the fill opens? Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-m10-f-gapup-0501", overlay=true, initial_capital=10000, + default_qty_type=strategy.percent_of_equity, default_qty_value=390, pyramiding=1, + margin_long=25, margin_short=25, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("America/New_York", 2025, 4, 30, 15, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, stop = math.round(close * 1.002, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/tv_trades.csv new file mode 100644 index 00000000..685ca194 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0501/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-05-01 21:30,Margin call,10.03,376,3816.4,-45.12,-1.18,0,30.08,0.79,-45.12,-1.18,-45.12,-0.45,0 +1,Entry long,2025-05-01 21:30,L,10.15,376,3816.4,-45.12,-1.18,0,30.08,0.79,-45.12,-1.18,-45.12,-0.45,0 +2,Exit long,2025-05-01 22:30,timeout,10.23,3520,35728,281.6,0.79,0,352,0.99,-422.4,-1.18,236.48,2.36,4 +2,Entry long,2025-05-01 21:30,L,10.15,3520,35728,281.6,0.79,0,352,0.99,-422.4,-1.18,236.48,2.36,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/meta.json b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/meta.json new file mode 100644 index 00000000..c14a996e --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-m10-f-gapup-0527", + "slug": "pm-m10-f-gapup-0527", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:26:06.177Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-05-19", + "to": "2025-05-30" + }, + "pine_sha256": "5940b3d3da7e392153da2dd4c932180e9a76464978198cec72a3e15cf244e2be", + "tv_trades_sha256": "f6222f524d46cd379d087e1ad82acd1b501ac0fc3903c7d283d986caa6ccbcf7", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/metrics.json b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/metrics.json new file mode 100644 index 00000000..d10dae78 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-m10-f-gapup-0527", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-05-19", + "to": "2025-05-30", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "5940b3d3da7e392153da2dd4c932180e9a76464978198cec72a3e15cf244e2be", + "tvTradesCsvHash": "f6222f524d46cd379d087e1ad82acd1b501ac0fc3903c7d283d986caa6ccbcf7", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74617", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1747612800000, + "to": 1748563200000 + }, + "returnedRange": { + "from": 1747661400000, + "to": 1748547900000 + }, + "rangeProof": "covered", + "rawFrameSha256": "ddbb7c15c68af9a03332fe695691cecbce928432fc7ea142c8535e01457cb4a3", + "decodedReportSha256": "cf4e02071f0a41c85a0fd79950facd8c745abc7c72d92d14c7b1034568e0a4dd", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:26:06.176Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/strategy.pine b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/strategy.pine new file mode 100644 index 00000000..abf63811 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/strategy.pine @@ -0,0 +1,14 @@ +// R5 lane PAR-MARGIN M10 (the frozen shape): a default-percent (390 %) buy stop placed ABOVE the +// session's last close (2025-5-23 15:45 America/New_York), not marketable when placed, gapped +// through by the next session's open. Margin 25: the entry bar's low breaches. Which book does +// TradingView size the entry, and its entry-bar margin call, on -- the signal-time units or the +// units the fill opens? Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-m10-f-gapup-0527", overlay=true, initial_capital=10000, + default_qty_type=strategy.percent_of_equity, default_qty_value=390, pyramiding=1, + margin_long=25, margin_short=25, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("America/New_York", 2025, 5, 23, 15, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, stop = math.round(close * 1.002, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/tv_trades.csv new file mode 100644 index 00000000..b0c1586a --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-gapup-0527/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-05-27 21:30,Margin call,10.35,204,2133.84,-22.44,-1.05,0,4.08,0.19,-22.44,-1.05,-22.44,-0.22,0 +1,Entry long,2025-05-27 21:30,L,10.46,204,2133.84,-22.44,-1.05,0,4.08,0.19,-22.44,-1.05,-22.44,-0.22,0 +2,Exit long,2025-05-27 22:30,timeout,10.33,3553,37164.38,-461.89,-1.24,0,71.06,0.19,-675.07,-1.82,-484.33,-4.84,4 +2,Entry long,2025-05-27 21:30,L,10.46,3553,37164.38,-461.89,-1.24,0,71.06,0.19,-675.07,-1.82,-484.33,-4.84,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/meta.json b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/meta.json new file mode 100644 index 00000000..e3d8546c --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-m10-f-k4-0309", + "slug": "pm-m10-f-k4-0309", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:20:55.270Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2026-03-02", + "to": "2026-03-12" + }, + "pine_sha256": "155da1ded84ce659bf3b5a9966e5bcf000dff6b7653d81115abc22a5229eb6e1", + "tv_trades_sha256": "ab895fec3d4760662672df6d15b1e447162365ec5c349acfb03bf9ba4dc6559b", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/metrics.json b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/metrics.json new file mode 100644 index 00000000..7a0171e3 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-m10-f-k4-0309", + "symbol": "NYSE:F", + "interval": "15", + "from": "2026-03-02", + "to": "2026-03-12", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "155da1ded84ce659bf3b5a9966e5bcf000dff6b7653d81115abc22a5229eb6e1", + "tvTradesCsvHash": "ab895fec3d4760662672df6d15b1e447162365ec5c349acfb03bf9ba4dc6559b", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74612", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1772409600000, + "to": 1773273600000 + }, + "returnedRange": { + "from": 1772461800000, + "to": 1773258300000 + }, + "rangeProof": "covered", + "rawFrameSha256": "c70d38ec114a828fa84c7fb7698662bb23f52cd732af5e78176ef5f482fc139f", + "decodedReportSha256": "3d05052afe3614b03cdad1cd2d11974081de46fc2d11967d7fab758841105293", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:20:55.269Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/strategy.pine b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/strategy.pine new file mode 100644 index 00000000..8ebce68c --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/strategy.pine @@ -0,0 +1,14 @@ +// R5 lane PAR-MARGIN M10: a default-percent (400 %) buy stop already marketable when +// placed on the session's last bar (2026-03-06 15:45 America/New_York) fills at the +// next session's gapped-down open. Margin 25: the entry bar's low breaches. Which book +// does TradingView size the entry, and its entry-bar margin call, on -- the signal-time +// units or the units the fill opens? Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-m10-f-k4-0309", overlay=true, initial_capital=10000, + default_qty_type=strategy.percent_of_equity, default_qty_value=400, pyramiding=1, + margin_long=25, margin_short=25, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("America/New_York", 2026, 3, 6, 15, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, stop = close * 0.9) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/tv_trades.csv new file mode 100644 index 00000000..884bd926 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0309/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2026-03-09 22:00,Margin call,11.83,4,47.64,-0.32,-0.67,0,0.36,0.76,-0.32,-0.67,-0.32,0.00,2 +1,Entry long,2026-03-09 21:30,L,11.91,4,47.64,-0.32,-0.67,0,0.36,0.76,-0.32,-0.67,-0.32,0.00,2 +2,Exit long,2026-03-09 22:15,Margin call,11.81,56,666.96,-5.6,-0.84,0,5.04,0.76,-5.6,-0.84,-5.92,-0.06,3 +2,Entry long,2026-03-09 21:30,L,11.91,56,666.96,-5.6,-0.84,0,5.04,0.76,-5.6,-0.84,-5.92,-0.06,3 +3,Exit long,2026-03-09 22:30,timeout,11.85,3234,38516.94,-194.04,-0.50,0,291.06,0.76,-323.4,-0.84,-199.96,-2.00,4 +3,Entry long,2026-03-09 21:30,L,11.91,3234,38516.94,-194.04,-0.50,0,291.06,0.76,-323.4,-0.84,-199.96,-2.00,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/meta.json b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/meta.json new file mode 100644 index 00000000..76a20573 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-m10-f-k4-0402", + "slug": "pm-m10-f-k4-0402", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:20:43.423Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2026-03-25", + "to": "2026-04-07" + }, + "pine_sha256": "07650a0d5113075f42b3d2fb23a6fd00ad6c041a8f2eb30b533057fc5cb8805f", + "tv_trades_sha256": "3bd3141a7089f30cd71e26754480084af927924b0f09d958941a07f3b82e5826", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/metrics.json b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/metrics.json new file mode 100644 index 00000000..bd6dbba0 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-m10-f-k4-0402", + "symbol": "NYSE:F", + "interval": "15", + "from": "2026-03-25", + "to": "2026-04-07", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "07650a0d5113075f42b3d2fb23a6fd00ad6c041a8f2eb30b533057fc5cb8805f", + "tvTradesCsvHash": "3bd3141a7089f30cd71e26754480084af927924b0f09d958941a07f3b82e5826", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74609", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1774396800000, + "to": 1775520000000 + }, + "returnedRange": { + "from": 1774445400000, + "to": 1775504700000 + }, + "rangeProof": "covered", + "rawFrameSha256": "d700746ae2825a2407161886fca631904a7a3ade59bfcc30a6efff16942419ab", + "decodedReportSha256": "cf96bb54e3192f21d21de6d2e6524b154da7c1648d619181731fe0edf13140db", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:20:43.422Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/strategy.pine b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/strategy.pine new file mode 100644 index 00000000..1caeeb5e --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/strategy.pine @@ -0,0 +1,14 @@ +// R5 lane PAR-MARGIN M10: a default-percent (400 %) buy stop already marketable when +// placed on the session's last bar (2026-04-01 15:45 America/New_York) fills at the +// next session's gapped-down open. Margin 25: the entry bar's low breaches. Which book +// does TradingView size the entry, and its entry-bar margin call, on -- the signal-time +// units or the units the fill opens? Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-m10-f-k4-0402", overlay=true, initial_capital=10000, + default_qty_type=strategy.percent_of_equity, default_qty_value=400, pyramiding=1, + margin_long=25, margin_short=25, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("America/New_York", 2026, 4, 1, 15, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, stop = close * 0.9) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/tv_trades.csv new file mode 100644 index 00000000..239be004 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0402/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2026-04-02 21:30,Margin call,11.38,176,2022.24,-19.36,-0.96,0,1.76,0.09,-19.36,-0.96,-19.36,-0.19,0 +1,Entry long,2026-04-02 21:30,L,11.49,176,2022.24,-19.36,-0.96,0,1.76,0.09,-19.36,-0.96,-19.36,-0.19,0 +2,Exit long,2026-04-02 22:30,timeout,11.39,3251,37353.99,-325.1,-0.87,0,32.51,0.09,-422.63,-1.13,-344.46,-3.44,4 +2,Entry long,2026-04-02 21:30,L,11.49,3251,37353.99,-325.1,-0.87,0,32.51,0.09,-422.63,-1.13,-344.46,-3.44,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/meta.json b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/meta.json new file mode 100644 index 00000000..a24bb720 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-m10-f-k4-0410", + "slug": "pm-m10-f-k4-0410", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:20:47.352Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-02", + "to": "2025-04-15" + }, + "pine_sha256": "405e720a1f0939f47c0e35f15422e0aa4fad80431381e32e2ab828ad78b71a33", + "tv_trades_sha256": "8911072fc9bfb7ad785e582d82855bde50c4c33b4c498c2d9a45ac356d02df6f", + "tv_trades_rows": 2 +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/metrics.json b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/metrics.json new file mode 100644 index 00000000..5241c27f --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-m10-f-k4-0410", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-02", + "to": "2025-04-15", + "deepBacktesting": true, + "trades": 1, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "405e720a1f0939f47c0e35f15422e0aa4fad80431381e32e2ab828ad78b71a33", + "tvTradesCsvHash": "8911072fc9bfb7ad785e582d82855bde50c4c33b4c498c2d9a45ac356d02df6f", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74610", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1743552000000, + "to": 1744675200000 + }, + "returnedRange": { + "from": 1743600600000, + "to": 1744659900000 + }, + "rangeProof": "covered", + "rawFrameSha256": "ebc5c2fd60448e8e99df36ae7b362ba1e4d9adde3c691c66c25fdbe61f3fa468", + "decodedReportSha256": "5c3c18829f26c2c1a24e01ec9443e4125330e1fbbda91f10ab5fd31568e6c6dc", + "closedTrades": 1, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:20:47.351Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/strategy.pine b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/strategy.pine new file mode 100644 index 00000000..565a2f49 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/strategy.pine @@ -0,0 +1,14 @@ +// R5 lane PAR-MARGIN M10: a default-percent (400 %) buy stop already marketable when +// placed on the session's last bar (2025-04-09 15:45 America/New_York) fills at the +// next session's gapped-down open. Margin 25: the entry bar's low breaches. Which book +// does TradingView size the entry, and its entry-bar margin call, on -- the signal-time +// units or the units the fill opens? Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-m10-f-k4-0410", overlay=true, initial_capital=10000, + default_qty_type=strategy.percent_of_equity, default_qty_value=400, pyramiding=1, + margin_long=25, margin_short=25, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("America/New_York", 2025, 4, 9, 15, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, stop = close * 0.9) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/tv_trades.csv new file mode 100644 index 00000000..805582c7 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-0410/tv_trades.csv @@ -0,0 +1,3 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-10 22:30,timeout,9.08,4219,38477.28,-168.76,-0.44,0,675.04,1.75,-379.71,-0.99,-168.76,-1.69,4 +1,Entry long,2025-04-10 21:30,L,9.12,4219,38477.28,-168.76,-0.44,0,675.04,1.75,-379.71,-0.99,-168.76,-1.69,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/meta.json b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/meta.json new file mode 100644 index 00000000..ee9b44fa --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-m10-f-k4-1007", + "slug": "pm-m10-f-k4-1007", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:20:51.013Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-09-29", + "to": "2025-10-10" + }, + "pine_sha256": "c262c857a205565ac7406dd110687711422bb2ab9e50d5c9217aa0a2c8b86836", + "tv_trades_sha256": "bd8d6bc27e858bfb046a2b514ab7c425121747d1e0fdc2ff509d203e70130664", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/metrics.json b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/metrics.json new file mode 100644 index 00000000..db5b5f15 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-m10-f-k4-1007", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-09-29", + "to": "2025-10-10", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "c262c857a205565ac7406dd110687711422bb2ab9e50d5c9217aa0a2c8b86836", + "tvTradesCsvHash": "bd8d6bc27e858bfb046a2b514ab7c425121747d1e0fdc2ff509d203e70130664", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74611", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1759104000000, + "to": 1760054400000 + }, + "returnedRange": { + "from": 1759152600000, + "to": 1760039100000 + }, + "rangeProof": "covered", + "rawFrameSha256": "09cfef429d59768ded842836221e5b378c7ca84b3fdf38d4b52ead1b46656ca6", + "decodedReportSha256": "c137fe41516f70355731ae0b6cde7fc329722eea7225809783a084f340f5e290", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:20:51.012Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/strategy.pine b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/strategy.pine new file mode 100644 index 00000000..63a05526 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/strategy.pine @@ -0,0 +1,14 @@ +// R5 lane PAR-MARGIN M10: a default-percent (400 %) buy stop already marketable when +// placed on the session's last bar (2025-10-06 15:45 America/New_York) fills at the +// next session's gapped-down open. Margin 25: the entry bar's low breaches. Which book +// does TradingView size the entry, and its entry-bar margin call, on -- the signal-time +// units or the units the fill opens? Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-m10-f-k4-1007", overlay=true, initial_capital=10000, + default_qty_type=strategy.percent_of_equity, default_qty_value=400, pyramiding=1, + margin_long=25, margin_short=25, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("America/New_York", 2025, 10, 6, 15, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, stop = close * 0.9) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/tv_trades.csv new file mode 100644 index 00000000..70abec22 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-10-07 21:30,Margin call,12.11,980,12250,-382.2,-3.12,0,19.6,0.16,-382.2,-3.12,-382.2,-3.82,0 +1,Entry long,2025-10-07 21:30,L,12.5,980,12250,-382.2,-3.12,0,19.6,0.16,-382.2,-3.12,-382.2,-3.82,0 +2,Exit long,2025-10-07 22:30,timeout,11.91,2164,27050,-1276.76,-4.72,0,43.28,0.16,-1363.32,-5.04,-1658.96,-16.59,4 +2,Entry long,2025-10-07 21:30,L,12.5,2164,27050,-1276.76,-4.72,0,43.28,0.16,-1363.32,-5.04,-1658.96,-16.59,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/meta.json b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/meta.json new file mode 100644 index 00000000..d303ddd2 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-m10-f-ohlc", + "slug": "pm-m10-f-ohlc", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:24:13.672Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10" + }, + "pine_sha256": "67220a7e5e2664f5be255c0cfa453feda33d5dbb0b7a0fe64af1cb8860604432", + "tv_trades_sha256": "8b006a00aff016906276eaa53ca2fc2f569b8ec5b14988ef4481feac9da10e8a", + "tv_trades_rows": 48 +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/metrics.json b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/metrics.json new file mode 100644 index 00000000..4c9f9923 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-m10-f-ohlc", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10", + "deepBacktesting": true, + "trades": 24, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "67220a7e5e2664f5be255c0cfa453feda33d5dbb0b7a0fe64af1cb8860604432", + "tvTradesCsvHash": "8b006a00aff016906276eaa53ca2fc2f569b8ec5b14988ef4481feac9da10e8a", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74613", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1775779200000 + }, + "returnedRange": { + "from": 1743514200000, + "to": 1775763900000 + }, + "rangeProof": "covered", + "rawFrameSha256": "421eefa4957229170db49864231645b2d0dec72ae1992a403b8df6e5f05a32f1", + "decodedReportSha256": "0031ef4c36934ca79b562ac991f65d142dc097e0c346955dae00037bac3d238e", + "closedTrades": 24, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:24:13.671Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/strategy.pine b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/strategy.pine new file mode 100644 index 00000000..d89ef4b6 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN M10: TradingView's own OHLC of the NYSE:F 15m bars the four M10 tapes +// trade on (each signal bar through its entry bar + 4), printed as order comments. +//@version=6 +strategy("pm-m10-f-ohlc", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=100) +var int k = -1 +sig = time == timestamp("America/New_York", 2026, 4, 1, 15, 45) or time == timestamp("America/New_York", 2025, 4, 9, 15, 45) or time == timestamp("America/New_York", 2025, 10, 6, 15, 45) or time == timestamp("America/New_York", 2026, 3, 6, 15, 45) +if sig + k := 0 +if k >= 0 and k <= 5 + strategy.entry("b" + str.tostring(bar_index), strategy.long, comment = str.format("{0}|{1}|{2}|{3}|{4}", str.tostring(time), str.tostring(open, "#.#####"), str.tostring(high, "#.#####"), str.tostring(low, "#.#####"), str.tostring(close, "#.#####"))) + k += 1 +if k > 5 + strategy.close_all(comment = "x") + k := -1 diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/tv_trades.csv new file mode 100644 index 00000000..56d280d2 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc/tv_trades.csv @@ -0,0 +1,49 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-10 22:45,x,9.15,1,9.12,0.03,0.33,0,0.16,1.75,-0.09,-0.99,0.03,0.00,5 +1,Entry long,2025-04-10 21:30,1744227900000|9.435|9.52|9.385|9.485,9.12,1,9.12,0.03,0.33,0,0.16,1.75,-0.09,-0.99,0.03,0.00,5 +2,Exit long,2025-04-10 22:45,x,9.15,1,9.17,-0.02,-0.22,0,0.03,0.33,-0.14,-1.53,0.01,0.00,4 +2,Entry long,2025-04-10 21:45,1744291800000|9.12|9.275|9.09|9.17,9.17,1,9.17,-0.02,-0.22,0,0.03,0.33,-0.14,-1.53,0.01,0.00,4 +3,Exit long,2025-04-10 22:45,x,9.15,1,9.06,0.09,0.99,0,0.13,1.43,-0.03,-0.33,0.1,0.00,3 +3,Entry long,2025-04-10 22:00,1744292700000|9.17|9.195|9.035|9.065,9.06,1,9.06,0.09,0.99,0,0.13,1.43,-0.03,-0.33,0.1,0.00,3 +4,Exit long,2025-04-10 22:45,x,9.15,1,9.07,0.08,0.88,0,0.12,1.32,-0.01,-0.11,0.18,0.00,2 +4,Entry long,2025-04-10 22:15,1744293600000|9.06|9.09|9.03|9.07,9.07,1,9.07,0.08,0.88,0,0.12,1.32,-0.01,-0.11,0.18,0.00,2 +5,Exit long,2025-04-10 22:45,x,9.15,1,9.08,0.07,0.77,0,0.11,1.21,0,0.00,0.25,0.00,1 +5,Entry long,2025-04-10 22:30,1744294500000|9.07|9.14|9.06|9.08,9.08,1,9.08,0.07,0.77,0,0.11,1.21,0,0.00,0.25,0.00,1 +6,Exit long,2025-10-07 22:45,x,11.86,1,9.15,2.71,29.62,0,3.65,39.89,-0.26,-2.84,2.96,0.00,3186 +6,Entry long,2025-04-10 22:45,1744295400000|9.08|9.19|9.08|9.155,9.15,1,9.15,2.71,29.62,0,3.65,39.89,-0.26,-2.84,2.96,0.00,3186 +7,Exit long,2025-10-07 22:45,x,11.86,1,12.5,-0.64,-5.12,0,0.02,0.16,-0.64,-5.12,2.32,0.00,5 +7,Entry long,2025-10-07 21:30,1759779900000|12.695|12.735|12.685|12.72,12.5,1,12.5,-0.64,-5.12,0,0.02,0.16,-0.64,-5.12,2.32,0.00,5 +8,Exit long,2025-10-07 22:45,x,11.86,1,12.11,-0.25,-2.06,0,0.03,0.25,-0.25,-2.06,2.07,0.00,4 +8,Entry long,2025-10-07 21:45,1759843800000|12.5|12.515|12.105|12.13,12.11,1,12.11,-0.25,-2.06,0,0.03,0.25,-0.25,-2.06,2.07,0.00,4 +9,Exit long,2025-10-07 22:45,x,11.86,1,11.98,-0.12,-1.00,0,0.02,0.17,-0.12,-1.00,1.95,0.00,3 +9,Entry long,2025-10-07 22:00,1759844700000|12.11|12.14|11.98|11.985,11.98,1,11.98,-0.12,-1.00,0,0.02,0.17,-0.12,-1.00,1.95,0.00,3 +10,Exit long,2025-10-07 22:45,x,11.86,1,11.88,-0.02,-0.17,0,0.05,0.42,-0.02,-0.17,1.93,0.00,2 +10,Entry long,2025-10-07 22:15,1759845600000|11.98|12|11.87|11.87,11.88,1,11.88,-0.02,-0.17,0,0.05,0.42,-0.02,-0.17,1.93,0.00,2 +11,Exit long,2025-10-07 22:45,x,11.86,1,11.91,-0.05,-0.42,0,0.02,0.17,-0.05,-0.42,1.88,0.00,1 +11,Entry long,2025-10-07 22:30,1759846500000|11.875|11.93|11.87|11.91,11.91,1,11.91,-0.05,-0.42,0,0.02,0.17,-0.05,-0.42,1.88,0.00,1 +12,Exit long,2026-03-09 22:45,x,11.9,1,11.86,0.04,0.34,0,2.94,24.79,-0.52,-4.38,1.92,0.00,2680 +12,Entry long,2025-10-07 22:45,1759847400000|11.91|11.925|11.86|11.86,11.86,1,11.86,0.04,0.34,0,2.94,24.79,-0.52,-4.38,1.92,0.00,2680 +13,Exit long,2026-03-09 22:45,x,11.9,1,11.91,-0.01,-0.08,0,0.09,0.76,-0.1,-0.84,1.91,0.00,5 +13,Entry long,2026-03-09 21:30,1772829900000|12.1|12.15|12.07|12.135,11.91,1,11.91,-0.01,-0.08,0,0.09,0.76,-0.1,-0.84,1.91,0.00,5 +14,Exit long,2026-03-09 22:45,x,11.9,1,11.98,-0.08,-0.67,0,0.01,0.08,-0.17,-1.42,1.83,0.00,4 +14,Entry long,2026-03-09 21:45,1773063000000|11.91|11.995|11.835|11.98,11.98,1,11.98,-0.08,-0.67,0,0.01,0.08,-0.17,-1.42,1.83,0.00,4 +15,Exit long,2026-03-09 22:45,x,11.9,1,11.86,0.04,0.34,0,0.07,0.59,-0.05,-0.42,1.87,0.00,3 +15,Entry long,2026-03-09 22:00,1773063900000|11.98|11.985|11.85|11.87,11.86,1,11.86,0.04,0.34,0,0.07,0.59,-0.05,-0.42,1.87,0.00,3 +16,Exit long,2026-03-09 22:45,x,11.9,1,11.83,0.07,0.59,0,0.09,0.76,-0.02,-0.17,1.94,0.00,2 +16,Entry long,2026-03-09 22:15,1773064800000|11.86|11.925|11.83|11.83,11.83,1,11.83,0.07,0.59,0,0.09,0.76,-0.02,-0.17,1.94,0.00,2 +17,Exit long,2026-03-09 22:45,x,11.9,1,11.85,0.05,0.42,0,0.06,0.51,-0.01,-0.08,1.99,0.00,1 +17,Entry long,2026-03-09 22:30,1773065700000|11.83|11.915|11.81|11.85,11.85,1,11.85,0.05,0.42,0,0.06,0.51,-0.01,-0.08,1.99,0.00,1 +18,Exit long,2026-04-02 22:45,x,11.42,1,11.9,-0.48,-4.03,0,0.61,5.13,-0.78,-6.55,1.51,0.00,468 +18,Entry long,2026-03-09 22:45,1773066600000|11.85|11.91|11.835|11.9,11.9,1,11.9,-0.48,-4.03,0,0.61,5.13,-0.78,-6.55,1.51,0.00,468 +19,Exit long,2026-04-02 22:45,x,11.42,1,11.49,-0.07,-0.61,0,0.06,0.52,-0.13,-1.13,1.44,0.00,5 +19,Entry long,2026-04-02 21:30,1775072700000|11.655|11.69|11.645|11.67,11.49,1,11.49,-0.07,-0.61,0,0.06,0.52,-0.13,-1.13,1.44,0.00,5 +20,Exit long,2026-04-02 22:45,x,11.42,1,11.4,0.02,0.18,0,0.15,1.32,-0.04,-0.35,1.46,0.00,4 +20,Entry long,2026-04-02 21:45,1775136600000|11.485|11.5|11.38|11.4,11.4,1,11.4,0.02,0.18,0,0.15,1.32,-0.04,-0.35,1.46,0.00,4 +21,Exit long,2026-04-02 22:45,x,11.42,1,11.43,-0.01,-0.09,0,0.12,1.05,-0.07,-0.61,1.45,0.00,3 +21,Entry long,2026-04-02 22:00,1775137500000|11.4|11.42|11.38|11.42,11.43,1,11.43,-0.01,-0.09,0,0.12,1.05,-0.07,-0.61,1.45,0.00,3 +22,Exit long,2026-04-02 22:45,x,11.42,1,11.39,0.03,0.26,0,0.16,1.40,-0.03,-0.26,1.48,0.00,2 +22,Entry long,2026-04-02 22:15,1775138400000|11.425|11.43|11.37|11.38,11.39,1,11.39,0.03,0.26,0,0.16,1.40,-0.03,-0.26,1.48,0.00,2 +23,Exit long,2026-04-02 22:45,x,11.42,1,11.39,0.03,0.26,0,0.16,1.40,0,0.00,1.51,0.00,1 +23,Entry long,2026-04-02 22:30,1775139300000|11.385|11.405|11.36|11.38,11.39,1,11.39,0.03,0.26,0,0.16,1.40,0,0.00,1.51,0.00,1 +24,Exit long,2026-04-10 03:45,,12.25,1,11.42,0.83,7.27,0,0.85,7.44,-0.07,-0.61,2.34,0.00,124 +24,Entry long,2026-04-02 22:45,1775140200000|11.385|11.545|11.385|11.43,11.42,1,11.42,0.83,7.27,0,0.85,7.44,-0.07,-0.61,2.34,0.00,124 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/meta.json b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/meta.json new file mode 100644 index 00000000..7a49715d --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-m10-f-ohlc2", + "slug": "pm-m10-f-ohlc2", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:31:51.967Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10" + }, + "pine_sha256": "28a155d4601dd4f32df5853c7c193ca962eaa562f917cc29ca3f00585b7ed192", + "tv_trades_sha256": "52160db7208aea53a0baa090fa91239aee3e38c6af73e470056110fe0768da7f", + "tv_trades_rows": 48 +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/metrics.json b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/metrics.json new file mode 100644 index 00000000..2469f5ac --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-m10-f-ohlc2", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-04-01", + "to": "2026-04-10", + "deepBacktesting": true, + "trades": 24, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "28a155d4601dd4f32df5853c7c193ca962eaa562f917cc29ca3f00585b7ed192", + "tvTradesCsvHash": "52160db7208aea53a0baa090fa91239aee3e38c6af73e470056110fe0768da7f", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74618", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1775779200000 + }, + "returnedRange": { + "from": 1743514200000, + "to": 1775763900000 + }, + "rangeProof": "covered", + "rawFrameSha256": "22b938103f996b3fdc106d82e319da082b2755309b2e3dd51e9f490ff3eb8084", + "decodedReportSha256": "7691528274bc058347efffc89fc396321c726a586c7d3bef577866da0bfa5b98", + "closedTrades": 24, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:31:51.966Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/strategy.pine b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/strategy.pine new file mode 100644 index 00000000..16a895fd --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN M10: TradingView's own OHLC of the NYSE:F 15m bars the four M10 gap-up tapes +// trade on (each signal bar through its entry bar + 4), printed as order comments. +//@version=6 +strategy("pm-m10-f-ohlc2", overlay=true, initial_capital=100000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=100) +var int k = -1 +sig = time == timestamp("America/New_York", 2025, 4, 30, 15, 45) or time == timestamp("America/New_York", 2026, 3, 30, 15, 45) or time == timestamp("America/New_York", 2025, 4, 23, 15, 45) or time == timestamp("America/New_York", 2025, 5, 23, 15, 45) +if sig + k := 0 +if k >= 0 and k <= 5 + strategy.entry("b" + str.tostring(bar_index), strategy.long, comment = str.format("{0}|{1}|{2}|{3}|{4}", str.tostring(time), str.tostring(open, "#.#####"), str.tostring(high, "#.#####"), str.tostring(low, "#.#####"), str.tostring(close, "#.#####"))) + k += 1 +if k > 5 + strategy.close_all(comment = "x") + k := -1 diff --git a/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/tv_trades.csv new file mode 100644 index 00000000..61f21627 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm-m10-f-ohlc2/tv_trades.csv @@ -0,0 +1,49 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-24 22:45,x,10.02,1,9.88,0.14,1.42,0,0.18,1.82,-0.07,-0.71,0.14,0.00,5 +1,Entry long,2025-04-24 21:30,1745437500000|9.77|9.795|9.755|9.765,9.88,1,9.88,0.14,1.42,0,0.18,1.82,-0.07,-0.71,0.14,0.00,5 +2,Exit long,2025-04-24 22:45,x,10.02,1,9.96,0.06,0.60,0,0.1,1.00,-0.02,-0.20,0.2,0.00,4 +2,Entry long,2025-04-24 21:45,1745501400000|9.88|9.97|9.815|9.955,9.96,1,9.96,0.06,0.60,0,0.1,1.00,-0.02,-0.20,0.2,0.00,4 +3,Exit long,2025-04-24 22:45,x,10.02,1,9.99,0.03,0.30,0,0.07,0.70,-0.02,-0.20,0.23,0.00,3 +3,Entry long,2025-04-24 22:00,1745502300000|9.955|10.015|9.94|10,9.99,1,9.99,0.03,0.30,0,0.07,0.70,-0.02,-0.20,0.23,0.00,3 +4,Exit long,2025-04-24 22:45,x,10.02,1,9.99,0.03,0.30,0,0.05,0.50,-0.02,-0.20,0.26,0.00,2 +4,Entry long,2025-04-24 22:15,1745503200000|9.99|10.06|9.985|10,9.99,1,9.99,0.03,0.30,0,0.05,0.50,-0.02,-0.20,0.26,0.00,2 +5,Exit long,2025-04-24 22:45,x,10.02,1,9.99,0.03,0.30,0,0.03,0.30,-0.02,-0.20,0.29,0.00,1 +5,Entry long,2025-04-24 22:30,1745504100000|9.995|10.035|9.97|10,9.99,1,9.99,0.03,0.30,0,0.03,0.30,-0.02,-0.20,0.29,0.00,1 +6,Exit long,2025-05-01 22:45,x,10.25,1,10.02,0.23,2.30,0,0.24,2.40,-0.15,-1.50,0.52,0.00,130 +6,Entry long,2025-04-24 22:45,1745505000000|9.99|10.01|9.97|10.01,10.02,1,10.02,0.23,2.30,0,0.24,2.40,-0.15,-1.50,0.52,0.00,130 +7,Exit long,2025-05-01 22:45,x,10.25,1,10.15,0.1,0.99,0,0.11,1.08,-0.12,-1.18,0.62,0.00,5 +7,Entry long,2025-05-01 21:30,1746042300000|9.945|10.04|9.945|9.99,10.15,1,10.15,0.1,0.99,0,0.11,1.08,-0.12,-1.18,0.62,0.00,5 +8,Exit long,2025-05-01 22:45,x,10.25,1,10.07,0.18,1.79,0,0.19,1.89,-0.02,-0.20,0.8,0.00,4 +8,Entry long,2025-05-01 21:45,1746106200000|10.15|10.23|10.025|10.07,10.07,1,10.07,0.18,1.79,0,0.19,1.89,-0.02,-0.20,0.8,0.00,4 +9,Exit long,2025-05-01 22:45,x,10.25,1,10.1,0.15,1.49,0,0.16,1.58,0,0.00,0.95,0.00,3 +9,Entry long,2025-05-01 22:00,1746107100000|10.07|10.12|10.05|10.1,10.1,1,10.1,0.15,1.49,0,0.16,1.58,0,0.00,0.95,0.00,3 +10,Exit long,2025-05-01 22:45,x,10.25,1,10.23,0.02,0.20,0,0.03,0.29,-0.03,-0.29,0.97,0.00,2 +10,Entry long,2025-05-01 22:15,1746108000000|10.095|10.245|10.095|10.225,10.23,1,10.23,0.02,0.20,0,0.03,0.29,-0.03,-0.29,0.97,0.00,2 +11,Exit long,2025-05-01 22:45,x,10.25,1,10.23,0.02,0.20,0,0.03,0.29,-0.03,-0.29,0.99,0.00,1 +11,Entry long,2025-05-01 22:30,1746108900000|10.225|10.245|10.22|10.24,10.23,1,10.23,0.02,0.20,0,0.03,0.29,-0.03,-0.29,0.99,0.00,1 +12,Exit long,2025-05-27 22:45,x,10.35,1,10.25,0.1,0.98,0,0.59,5.76,-0.19,-1.85,1.09,0.00,442 +12,Entry long,2025-05-01 22:45,1746109800000|10.235|10.255|10.195|10.245,10.25,1,10.25,0.1,0.98,0,0.59,5.76,-0.19,-1.85,1.09,0.00,442 +13,Exit long,2025-05-27 22:45,x,10.35,1,10.46,-0.11,-1.05,0,0.02,0.19,-0.19,-1.82,0.98,0.00,5 +13,Entry long,2025-05-27 21:30,1748029500000|10.4|10.4|10.35|10.36,10.46,1,10.46,-0.11,-1.05,0,0.02,0.19,-0.19,-1.82,0.98,0.00,5 +14,Exit long,2025-05-27 22:45,x,10.35,1,10.36,-0.01,-0.10,0,0.02,0.19,-0.09,-0.87,0.97,0.00,4 +14,Entry long,2025-05-27 21:45,1748352600000|10.46|10.475|10.35|10.355,10.36,1,10.36,-0.01,-0.10,0,0.02,0.19,-0.09,-0.87,0.97,0.00,4 +15,Exit long,2025-05-27 22:45,x,10.35,1,10.28,0.07,0.68,0,0.08,0.78,-0.01,-0.10,1.04,0.00,3 +15,Entry long,2025-05-27 22:00,1748353500000|10.36|10.38|10.27|10.28,10.28,1,10.28,0.07,0.68,0,0.08,0.78,-0.01,-0.10,1.04,0.00,3 +16,Exit long,2025-05-27 22:45,x,10.35,1,10.3,0.05,0.49,0,0.06,0.58,-0.01,-0.10,1.09,0.00,2 +16,Entry long,2025-05-27 22:15,1748354400000|10.28|10.335|10.27|10.305,10.3,1,10.3,0.05,0.49,0,0.06,0.58,-0.01,-0.10,1.09,0.00,2 +17,Exit long,2025-05-27 22:45,x,10.35,1,10.33,0.02,0.19,0,0.02,0.19,-0.01,-0.10,1.11,0.00,1 +17,Entry long,2025-05-27 22:30,1748355300000|10.3|10.355|10.29|10.325,10.33,1,10.33,0.02,0.19,0,0.02,0.19,-0.01,-0.10,1.11,0.00,1 +18,Exit long,2026-03-31 22:45,x,11.37,1,10.35,1.02,9.86,0,4.45,43.00,-0.46,-4.44,2.13,0.00,5476 +18,Entry long,2025-05-27 22:45,1748356200000|10.33|10.35|10.32|10.35,10.35,1,10.35,1.02,9.86,0,4.45,43.00,-0.46,-4.44,2.13,0.00,5476 +19,Exit long,2026-03-31 22:45,x,11.37,1,11.37,0,0.00,0,0.07,0.62,-0.09,-0.79,2.13,0.00,5 +19,Entry long,2026-03-31 21:30,1774899900000|11.17|11.21|11.165|11.2,11.37,1,11.37,0,0.00,0,0.07,0.62,-0.09,-0.79,2.13,0.00,5 +20,Exit long,2026-03-31 22:45,x,11.37,1,11.33,0.04,0.35,0,0.11,0.97,0,0.00,2.17,0.00,4 +20,Entry long,2026-03-31 21:45,1774963800000|11.37|11.37|11.28|11.31,11.33,1,11.33,0.04,0.35,0,0.11,0.97,0,0.00,2.17,0.00,4 +21,Exit long,2026-03-31 22:45,x,11.37,1,11.41,-0.04,-0.35,0,0.03,0.26,-0.06,-0.53,2.13,0.00,3 +21,Entry long,2026-03-31 22:00,1774964700000|11.325|11.42|11.325|11.42,11.41,1,11.41,-0.04,-0.35,0,0.03,0.26,-0.06,-0.53,2.13,0.00,3 +22,Exit long,2026-03-31 22:45,x,11.37,1,11.41,-0.04,-0.35,0,0.02,0.18,-0.04,-0.35,2.09,0.00,2 +22,Entry long,2026-03-31 22:15,1774965600000|11.41|11.435|11.35|11.405,11.41,1,11.41,-0.04,-0.35,0,0.02,0.18,-0.04,-0.35,2.09,0.00,2 +23,Exit long,2026-03-31 22:45,x,11.37,1,11.41,-0.04,-0.35,0,0.02,0.18,-0.04,-0.35,2.05,0.00,1 +23,Entry long,2026-03-31 22:30,1774966500000|11.41|11.41|11.37|11.41,11.41,1,11.41,-0.04,-0.35,0,0.02,0.18,-0.04,-0.35,2.05,0.00,1 +24,Exit long,2026-04-10 03:45,,12.25,1,11.37,0.88,7.74,0,0.9,7.92,-0.03,-0.26,2.93,0.00,176 +24,Entry long,2026-03-31 22:45,1774967400000|11.41|11.43|11.37|11.37,11.37,1,11.37,0.88,7.74,0,0.9,7.92,-0.03,-0.26,2.93,0.00,176 \ No newline at end of file diff --git a/tests/test_adapter_margin_schedule_differential.cpp b/tests/test_adapter_margin_schedule_differential.cpp index fb43d8d2..78134886 100644 --- a/tests/test_adapter_margin_schedule_differential.cpp +++ b/tests/test_adapter_margin_schedule_differential.cpp @@ -32,8 +32,10 @@ * M10 (G2-12) the pre-open slice. Same money through the pre-open x4 * (schedule_preopen_margin_slice), through the shared rule * (source_margin_units via the kernel's point) and through the kernel's - * AfterApplied point: they part on the one-contract band, on the +1e-6 - * restore fudge, and on the frozen (signal-time) units. + * AfterApplied point: they part on the one-contract band and on the + * +1e-6 restore fudge. They parted on the frozen units too until R5 + * lane PAR-MARGIN froze a default-percent stop above 100 % at its + * snapped level, as TradingView does (the M10 tapes below). * M11 (G2-13) opening admission. AdmitWithHostMargin admits an opening the * kernel's initial-margin gate declines (a percent entry fee), and * refuses an add the kernel admits (signal-time equity vs marked). @@ -81,6 +83,7 @@ struct FeedBar { double open, high, low, close; }; #include "fixtures/margin_entry_bar/bars.inc" +#include "fixtures/margin_entry_bar/m10_bars.inc" // ------------------------------------------------------------ TV policy // The twin's TradingView money and slice (test_native_margin_hooks_twin.cpp, @@ -195,6 +198,7 @@ void check_fill(const MarginFill& f, int bar, NativePathPhase phase, double unit struct PineSide final : source::PineStrategyHost { std::function script; int calls = 0; + double last_close = 0.0; explicit PineSide(const Config& c) { auto& tv = fixture_configuration(); tv.initial_capital = c.capital; @@ -212,8 +216,9 @@ struct PineSide final : source::PineStrategyHost { set_margin_call_enabled(true); fixture_retain_all_events(); } - void on_source_bar(const Bar&) override { + void on_source_bar(const Bar& bar) override { ++calls; + last_close = bar.close; if (script) script(*this, pine_bar_index()); } void entry(const char* id, bool is_long, double limit, double stop, double qty) { @@ -600,10 +605,14 @@ void m10_same_money(const M10Case& m) { check_fill(native[0], 2, NativePathPhase::Low, m.shared_units, m.low, true); } -// The frozen tuple: signal close 100, gap-through open 102. The pre-open slice -// sizes on the frozen 19 lots (sized at the 100 signal) while the book the -// kernel fills holds 18 (sized at the 102 fill): 4 lots against the kernel's -// one-contract band on the real book. +// The frozen tuple: signal close 100, a 101 stop gapped through by the 102 +// open. Until R5 lane PAR-MARGIN the stop's units were frozen at the 100 +// signal (19) and the book sized again at the 102 fill (18), so the pre-open +// slice sized 4 lots on a 19-lot book the run did not hold. TradingView +// freezes a stop not yet marketable at its snapped level, above 100 % too +// (the M10 tapes below): both are now the 101 level's 18 lots, and on them +// the pre-open slice's own x4 sizes nothing -- the band row above, whose +// one contract the kernel's point books (1 @90). void m10_frozen_units() { std::printf("-- M10 frozen: signal 100, fill 102, low 90\n"); Config c; @@ -622,9 +631,8 @@ void m10_frozen_units() { kernel.run_bars(pine_margin_spec("m10-frozen", c, 0.0), bars); const auto native = margin_fills(kernel); print_side("kernel", native, kernel.position(), kernel.balance()); - REQUIRE(preopen.size() == 1); - CHECK(stop.position() + preopen[0].units == 18.0); // the book is 18 lots - check_fill(preopen[0], 1, NativePathPhase::Low, 4.0, 90.0, false); + CHECK(preopen.empty()); + CHECK(stop.position() == 18.0); // the 101 level's 18 lots REQUIRE(native.size() == 1); check_fill(native[0], 1, NativePathPhase::Low, 1.0, 90.0, true); } @@ -985,6 +993,120 @@ void m7_tradingview_tapes() { } } +// ================================================================= M10 on tapes +// Which book does TradingView size a default-percent stop entry above 100 % on, +// and so its entry-bar margin call? Eight lab tv tapes on NYSE:F 15m +// (tests/fixtures/margin_entry_bar/pm-m10-*, ws-report-v1, rangeProof +// covered), margin 25, one buy stop placed on the session's last bar and +// filled at the next session's open, closed 3 bars after the entry bar: +// k4-* : 400 %, the stop at 0.9 x close -- marketable when placed; +// gapup-* : 390 %, the stop at round(1.002 x close, 2) -- not marketable +// when placed, gapped through by the open. +// TradingView opens the quotient of the signal close (on its tick) for the +// first and of the snapped stop level for the second -- 3427, 4219, 3144, +// 3294 and 3896, 3475, 3987, 3757 shares -- never the fill's, and books its +// margin calls on that book. ab9714be froze a default-percent stop only up to +// 100 % (pine_strategy_commands.cpp:307-310) and sized a larger one at the +// fill, and so did the adapter until R5 lane PAR-MARGIN: 0 of 8 books (3481, +// 4385, 3200, 3358; 3842, 3430, 3947, 3728), with margin calls sized to match +// or fired where TradingView fired none. It now freezes above 100 % too and +// books every tape's rows: the same bars, quantities and signals, and the +// same prices but two. +// The two (recorded, open): a margin call at an exact half-cent low -- 1007's +// 12.105, 0501's 10.025 -- books a tick below TradingView (12.10 and 10.02 +// against 12.11 and 10.03). Both decimals are stored a hair above the half +// cent, and TradingView rounds the stored value to its nearest tick; 0424's +// 9.815, stored a hair below, books 9.81 on both sides. +struct M10Tape { + const char* slug; + const FeedBar* bars; + double percent; + bool marketable; + int half_cent_row; // the row booked a tick below the tape's; -1: none +}; + +double tape_entry_price(const char* slug) { + std::ifstream in(std::string(PINEFORGE_HM_M7A_FIXTURE_DIR) + "/" + slug + "/tv_trades.csv"); + std::string line; + std::getline(in, line); + while (std::getline(in, line)) { + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() >= 6 && cell[1].rfind("Entry", 0) == 0) return std::stod(cell[4]); + } + return kNaN; +} + +void m10_tradingview_tapes() { + const M10Tape tapes[] = { + {"pm-m10-f-k4-0402", kM10_k4_0402, 400.0, true, -1}, + {"pm-m10-f-k4-0410", kM10_k4_0410, 400.0, true, -1}, + {"pm-m10-f-k4-1007", kM10_k4_1007, 400.0, true, 0}, + {"pm-m10-f-k4-0309", kM10_k4_0309, 400.0, true, -1}, + {"pm-m10-f-gapup-0501", kM10_gapup_0501, 390.0, false, 0}, + {"pm-m10-f-gapup-0331", kM10_gapup_0331, 390.0, false, -1}, + {"pm-m10-f-gapup-0424", kM10_gapup_0424, 390.0, false, -1}, + {"pm-m10-f-gapup-0527", kM10_gapup_0527, 390.0, false, -1}, + }; + for (const M10Tape& tape : tapes) { + std::printf("-- M10 tape %s (%.0f %%, a stop %s when placed)\n", tape.slug, tape.percent, + tape.marketable ? "marketable" : "not yet marketable"); + std::vector bars; + for (int i = 0; i < 6; ++i) { + const FeedBar& r = tape.bars[i]; + bars.push_back({r.open, r.high, r.low, r.close, 1.0, r.ts}); + } + Config c; + c.capital = 10000.0; c.margin_long = 25.0; c.margin_short = 25.0; + c.qty_step = 1.0; c.mintick = 0.01; + c.qty_type = QtyType::PERCENT_OF_EQUITY; c.qty_value = tape.percent; + PineSide pine(c); + const bool marketable = tape.marketable; + pine.script = [marketable](PineSide& h, int bar) { + if (bar == 0) { + const double stop = marketable ? h.last_close * 0.9 + : std::round(h.last_close * 1.002 * 100.0) / 100.0; + h.entry("L", true, kNaN, stop, kNaN); + } + if (h.position() > 0.0 && bar - h.entry_bar_index() >= 3) h.close("L"); + }; + pine.run_bars(bars); + const auto adapter = engine_exits(pine); + const auto tv = tape_exits(tape.slug, tape.bars); + print_rows("tape", tv); + print_rows("adapter", adapter); + + // TradingView's book: the quotient at the signal close on its tick, or + // at the snapped stop level -- never at the fill. + const double close = tape.bars[0].close; + const double sizing_price = marketable ? std::round(close / 0.01) * 0.01 + : std::round(close * 1.002 * 100.0) / 100.0; + const double frozen = std::floor(tape.percent / 100.0 * 10000.0 / sizing_price); + double tv_book = 0.0; + double adapter_book = 0.0; + for (const auto& r : tv) tv_book += r.qty; + for (const auto& r : adapter) adapter_book += r.qty; + CHECK(tv_book == frozen); + CHECK(adapter_book == frozen); + REQUIRE(pine.trade_count() > 0); + CHECK(std::abs(pine.get_trade(0).entry_price - tape_entry_price(tape.slug)) <= 5e-3); + REQUIRE(adapter.size() == tv.size()); + for (std::size_t i = 0; i < tv.size(); ++i) { + CHECK(adapter[i].bar == tv[i].bar); + CHECK(std::abs(adapter[i].qty - tv[i].qty) <= 5e-5); + CHECK((tv[i].signal == "Margin call") == (adapter[i].signal == "Margin call")); + if (static_cast(i) == tape.half_cent_row) { + // Recorded: a tick below the tape's price. + CHECK(std::abs(adapter[i].price - (tv[i].price - 0.01)) <= 5e-3); + } else { + CHECK(std::abs(adapter[i].price - tv[i].price) <= 5e-3); + } + } + } +} + } // namespace int main() { @@ -1001,6 +1123,7 @@ int main() { test("M11-B add 104", [] { m11_add(104.0, true); }); test("E20 f1 probe tail", e20_probe_tail_margin_call); test("M7 TradingView tapes", m7_tradingview_tapes); + test("M10 TradingView tapes", m10_tradingview_tapes); std::printf("%d checks, %d failures\n", checks, failures); return failures == 0 ? 0 : 1; } From ed2abe49f548b1eb03aecccf0961a9b1581859e6 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 07:10:55 +0800 Subject: [PATCH 40/83] Kernel + adapter: the margin model is checked at each sample of a continuous intrabar path, as TradingView's magnifier is (R5 lane PAR-MARGIN, item 3) H-MEASURE's acid finding A: under IntrabarPath::lower_tf the kernel checked the margin model at the script bar's first sample (BarOpen) and after fills only, while native-engine.md, native_run_spec.hpp and two consumer comments said it re-evaluates at each delivered sample. Its intrabar probe booked no liquidation on the magnified run of a bar whose spike crossed the level. The decision, from TradingView. Four pairs of lab tv tapes on BINANCE:ETHUSDT.P 15m (tests/fixtures/intrabar_margin, rangeProof covered, notes tv-tape-pm-i3-*): one leveraged long (margin 20) carried into a bar that first crosses its line at an early 1-minute low and reaches a deeper low later in the bar, the same script with use_bar_magnifier on and off. - off: one margin call at the chart bar's low, 4 of 4; - on: the call at the FIRST 1-minute low that crosses, sized there, 4 of 4 (0.1052 @2223.05, 0.1088 @2760, 0.1224 @4820, 0.418 @1574.55 -- against 1.064 @2159.28 etc. with it off), and again at each later 1-minute low that crosses the reduced book's line: every row on 0622 (three calls), 0824 and 0406. TradingView checks per sample. - residual, recorded: on 0130 TradingView books its second call at the 01:43 low (0.4824 @2673.33) on the book the first left, though the 01:42 low (2690) already crosses that book's line; the adapter books 0.3228 @2690 there. TradingView's own 1-minute lows (pm-i3-eth-ltf-lows, read through request.security_lower_tf) equal the feed's, and the tape repeats over three other windows. Kernel (ADR-0001 rule 2, row added): NativeMarginCheckKind::IntrabarSample = 4, appended (C PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE = 4, pinned in native_c_host.cpp beside the other kinds; the surface checker twins it, and its self-test's two enum mutations now append after and insert before the new last kind). At every delivered sample after the bar's first on a path matched as continuous segments the margin model is offered that point, measured at the sample's price immediately before it is matched, so a mark-check reduction rests at the sample that crossed and the path reaches it there. (a) mechanism: where a run without a whole-bar waypoint model is measured, venue-neutral; (b) knob: none. A one-price distribution path (synthesized, DistributionSamples) is not offered it -- a request armed at a discrete point is matched only at a later one, so a reduction re-armed at every sample would never be -- and keeps its first sample and the re-arm after fills; the docs now say exactly that. No state is added; no pinned value moved (the full local suite, 659 rows, passes with the kernel half alone). Adapter: margin_check_allowed admits IntrabarSample on a magnified run's leveraged book (intrabar_sample_checked); a full-margin book, process_orders_on_close and calc_on_order_fills keep their routes, unmeasured under the magnifier, and a live adapter slice keeps its bar. Before -> after: - the acid intrabar probe (copied to the lane scratch): lower_tf 0 IntrabarSample checks and 0 liquidations -> 55 checks and a slice on bar 2 at the crossing sample; the whole-bar run unchanged; - the adapter on the tapes: magnified rows 0/4 tapes (a call a bar late, e.g. 0.5232 @2194.80 on 0622) -> 3/4 exact and 0130's first call; the chart path 4/4 before and after. New rows: tests/test_native_margin_intrabar_samples.cpp (kernel-only, 36 checks: the points, the crossing sample against the whole-bar extreme, a host that refuses them, the one-price path) and the "intrabar margin on tapes" section of the margin differential. Fail-before: the kernel row against the base tree and the frozen v18 closure -- test_native_margin_intrabar_samples.cpp:52:55: error: no member named 'IntrabarSample' in 'pineforge::NativeMarginCheckKind'; the differential section against the base library, 475 checks, 2 failures. The corpus's magnifier probes trade one contract on large capital, so no margin check fires there. Docs: native-engine.md (the intrabar path, arming, the check-kind list, the adapter paragraph), native_run_spec.hpp, the consumer comments, native_host.hpp, native_c_api.h, pine-to-native.md's check-kind row, ADR-0001 (Margin bullet and the ruling row), design MG15; anchors carried by an exact line map. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 7c37ae41b7e78addd4381d29ba31e9413f28bc2a) INT26 pick note: the kernel, header, C-host, adapter, test and fixture hunks applied without conflict. The ADR ruling row sits after K-OCA-KEEP's two rows (0a046423, fda14042) and before H-DOCGATES' inventory rows; the other pages merged on citation digits only. The new C enumerator is recorded across the C surface's lists in INT26's own commit. --- docs/adr/0001-kernel-adapter-boundary.md | 133 ++++--- docs/design/native-feature-parity.md | 174 ++++---- docs/pages/contributing-llm.md | 2 +- docs/pages/native-engine.md | 131 +++--- docs/pages/pine-to-native.md | 275 ++++++------- include/pineforge/native_c_api.h | 9 +- include/pineforge/native_host.hpp | 16 +- include/pineforge/native_run_spec.hpp | 7 +- include/pineforge/source/pine_adapter.hpp | 1 + scripts/test_check_native_c_api_surface.py | 17 +- src/native_c_host.cpp | 3 + src/native_execution_consumer.cpp | 20 +- src/source/pine_adapter.cpp | 28 ++ tests/CMakeLists.txt | 4 + tests/fixtures/intrabar_margin/README.md | 38 ++ tests/fixtures/intrabar_margin/bars_1m.inc | 373 ++++++++++++++++++ .../pm-i3-eth-chart-0130/meta.json | 16 + .../pm-i3-eth-chart-0130/metrics.json | 40 ++ .../pm-i3-eth-chart-0130/strategy.pine | 15 + .../pm-i3-eth-chart-0130/tv_trades.csv | 5 + .../pm-i3-eth-chart-0406/meta.json | 16 + .../pm-i3-eth-chart-0406/metrics.json | 40 ++ .../pm-i3-eth-chart-0406/strategy.pine | 15 + .../pm-i3-eth-chart-0406/tv_trades.csv | 5 + .../pm-i3-eth-chart-0622/meta.json | 16 + .../pm-i3-eth-chart-0622/metrics.json | 40 ++ .../pm-i3-eth-chart-0622/strategy.pine | 15 + .../pm-i3-eth-chart-0622/tv_trades.csv | 5 + .../pm-i3-eth-chart-0824/meta.json | 16 + .../pm-i3-eth-chart-0824/metrics.json | 40 ++ .../pm-i3-eth-chart-0824/strategy.pine | 15 + .../pm-i3-eth-chart-0824/tv_trades.csv | 5 + .../pm-i3-eth-ltf-lows/meta.json | 16 + .../pm-i3-eth-ltf-lows/metrics.json | 40 ++ .../pm-i3-eth-ltf-lows/strategy.pine | 15 + .../pm-i3-eth-ltf-lows/tv_trades.csv | 9 + .../pm-i3-eth-mag-0130/meta.json | 16 + .../pm-i3-eth-mag-0130/metrics.json | 40 ++ .../pm-i3-eth-mag-0130/strategy.pine | 15 + .../pm-i3-eth-mag-0130/tv_trades.csv | 7 + .../pm-i3-eth-mag-0406/meta.json | 16 + .../pm-i3-eth-mag-0406/metrics.json | 40 ++ .../pm-i3-eth-mag-0406/strategy.pine | 15 + .../pm-i3-eth-mag-0406/tv_trades.csv | 5 + .../pm-i3-eth-mag-0622/meta.json | 16 + .../pm-i3-eth-mag-0622/metrics.json | 40 ++ .../pm-i3-eth-mag-0622/strategy.pine | 15 + .../pm-i3-eth-mag-0622/tv_trades.csv | 9 + .../pm-i3-eth-mag-0824/meta.json | 16 + .../pm-i3-eth-mag-0824/metrics.json | 40 ++ .../pm-i3-eth-mag-0824/strategy.pine | 15 + .../pm-i3-eth-mag-0824/tv_trades.csv | 5 + ...t_adapter_margin_schedule_differential.cpp | 123 +++++- tests/test_native_margin_intrabar_samples.cpp | 266 +++++++++++++ 54 files changed, 1954 insertions(+), 360 deletions(-) create mode 100644 tests/fixtures/intrabar_margin/README.md create mode 100644 tests/fixtures/intrabar_margin/bars_1m.inc create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/tv_trades.csv create mode 100644 tests/test_native_margin_intrabar_samples.cpp diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 91110031..13c0df50 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -29,7 +29,7 @@ claim here is read off the code, and three checkers hold the parts of it that ca Three layers, two of them meant to be optional: 1. **Generic kernel — Pine-agnostic (the reusable core).** The public host is - `pineforge::NativeStrategyHost` (`native_host.hpp:833`), an abstract subclass of + `pineforge::NativeStrategyHost` (`native_host.hpp:845`), an abstract subclass of `BacktestEngine`. Kernel translation units are named, not inferred: they are the thirty-five members of `PINEFORGE_KERNEL_SOURCES` (`CMakeLists.txt:113`) — the `engine_*`, `native_*`, `ta_*`, `market_driver`, `magnifier`, `math`, `matrix`, `session_time`, @@ -64,7 +64,7 @@ this runtime **on** the kernel; it did **not** move it to codegen. ## How the kernel works (driving it without Pine) -Subclass `NativeStrategyHost`: a **zero-argument** host (`native_host.hpp:833`) carrying the +Subclass `NativeStrategyHost`: a **zero-argument** host (`native_host.hpp:845`) carrying the callbacks, the answering hooks and the request API. The `CapAttachment` constructor belongs to the adapter class `PineStrategyHost` (`pine_strategy_host.hpp:242`), not here. The whole surface, field by field, is `docs/pages/native-engine.md`; this section is the boundary's @@ -72,15 +72,15 @@ summary of it. - **Lifecycle.** `Unconfigured → Ready → Running → Completed | Failed` (`native_host.hpp:20-26`); phases `Batch | Warmup | Realtime` (`native_host.hpp:28-32`) — one path runs the batch, then - continues live. Configure with `configure_native(spec)` (`native_host.hpp:1184`), then feed. + continues live. Configure with `configure_native(spec)` (`native_host.hpp:1196`), then feed. - **Callbacks are not close-only.** `on_native_bar` is the pure-virtual script-bar calculation - (`native_host.hpp:881`); also `on_native_input` (`native_host.hpp:863`), `on_native_tick` - (`native_host.hpp:866`), `on_native_timeframe_bar` (`native_host.hpp:873`), + (`native_host.hpp:893`); also `on_native_input` (`native_host.hpp:875`), `on_native_tick` + (`native_host.hpp:878`), `on_native_timeframe_bar` (`native_host.hpp:885`), `on_native_bar_open` — at the modeled opening, before its matching pass - (`native_host.hpp:877`, `on_native_bar_open` `native_execution_consumer.cpp:7065-7067`) — - `on_native_recalculate` for every calculation of the run (`native_host.hpp:901`), - `on_native_sub_bar` (`native_host.hpp:916`) and post-fill - `on_native_applied` (`native_host.hpp:926`). `on_bar` is `final` (`native_host.hpp:845`). + (`native_host.hpp:889`, `on_native_bar_open` `native_execution_consumer.cpp:7063-7065`) — + `on_native_recalculate` for every calculation of the run (`native_host.hpp:913`), + `on_native_sub_bar` (`native_host.hpp:928`) and post-fill + `on_native_applied` (`native_host.hpp:938`). `on_bar` is `final` (`native_host.hpp:857`). - **Aggregated coordinates (K-IDX, Option A).** The kernel's `NativeCoordinate::interval_index` (`market_driver.hpp:74`) is the sequential SCRIPT-bar index even when `input_tf` is finer than `script_tf`. The same @@ -95,8 +95,8 @@ summary of it. (`pine_adapter.cpp:845-851`) where Pine still needs input cadence, and does not rewrite the kernel's lot or row indices. - **Requests.** `submit` / `replace` / `cancel` / `submit_market` / `replace_market` - (`native_host.hpp:1215-1220`), plus `cancel_all` (`native_host.hpp:1255`) and `cancel_where` - (`native_host.hpp:1260`). A request carries one of five triggers — Market, Limit (with a + (`native_host.hpp:1227-1232`), plus `cancel_all` (`native_host.hpp:1267`) and `cancel_where` + (`native_host.hpp:1272`). A request carries one of five triggers — Market, Limit (with a generic market-if-touched `fill_through`, `native_order.hpp:239-242`), Stop, StopLimit, Trail (`Trigger` `native_order.hpp:296`); one of **six** intents — `Flatten`, `Reduce`, `Transact`, `ReverseTo`, `HostSized` and the kernel-resolved `Sized` (`OrderIntent` @@ -109,12 +109,12 @@ summary of it. fills with slippage, fees, admission and settlement. Sizing has **two** routes: `Sized` (`native_order.hpp:164`) is resolved by the kernel from a cash or equity-fraction basis, so a bare host needs no override at all, while `HostSized` hands the units to the host — and an - unresolved `HostSized` whose `resolve_execution_terms` (`native_host.hpp:935`) returns no + unresolved `HostSized` whose `resolve_execution_terms` (`native_host.hpp:947`) returns no units is a `TermsUnresolved` rejection - (`TermsUnresolved` `native_execution_consumer.cpp:4957`). -- **Observing executions.** Implement `on_native_applied` (`native_host.hpp:926`) — notifications + (`TermsUnresolved` `native_execution_consumer.cpp:4964`). +- **Observing executions.** Implement `on_native_applied` (`native_host.hpp:938`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` - (`native_host.hpp:1317`). Do *not* implement + (`native_host.hpp:1329`). Do *not* implement `NativeExecutionConsumer`: it is the kernel's internal matcher, declared `final` (`src/native_execution_consumer.hpp:58`) and bound by the host. - **The rest of the host surface**, all on `NativeStrategyHost`: @@ -122,15 +122,15 @@ summary of it. | What | Surface | |---|---| | Run state | `native_state()` → `NativeStateView` — kind, phase, `NativeCompletion` BatchComplete / StreamEnded, failure, high water (`NativeCompletion` `native_host.hpp:50`, `NativeStateView` `:282`, `NativeRunPhase` `:41`) | - | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:4930`) | - | Cohorts | `cohort_open` (`native_host.hpp:1270`) / `cohort_add` (`:1273`) / `cohort_remove` (`:1276`) — the handle a `BindCohort` owner names | - | Mid-callback execution | `current_execution_point` (`native_host.hpp:1075`), `inspect_current_execution` (`native_host.hpp:1087`) / `execute_current` (`native_host.hpp:1092`) | - | Margin seams | four, not one: the run spec's own `margin` model (`NativeMarginModel` `native_run_spec.hpp:240`) with its three hooks — `resolve_margin_requirement` (`native_host.hpp:964`), `margin_check_allowed` (`native_host.hpp:976`), `resolve_margin_call_units` (`native_host.hpp:983`) — and `validate_execution_precommit` (`native_host.hpp:948`), whose `AdmitWithHostMargin` verdict hands the opening check to the host | - | Risk seam | `NativeRiskLimits` (`native_run_spec.hpp:310`) with `native_risk_state()` (`native_host.hpp:1311`); the breach appends a `NativeRiskEvent` to the command history | - | State reads | `physical_position()` (`native_host.hpp:1280`) / `native_marked_equity(mark)` (`native_host.hpp:1289`) / `native_open_lots(mark)` (`native_host.hpp:1285`), `trail_state(handle)` (`native_host.hpp:1080`), and the rest of the query surface in `docs/pages/native-engine.md`, "Reading the run back" | - | Lookahead hazard | the `Bar` given to `on_native_bar_open` is the *complete* script bar; `current_partial_bar()` (`native_host.hpp:1061`) is the lookahead-free bar so far, and `NativeOpenBarView::OpenOnly` (`native_run_spec.hpp:109`) masks that one callback | + | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:4937`) | + | Cohorts | `cohort_open` (`native_host.hpp:1282`) / `cohort_add` (`:1285`) / `cohort_remove` (`:1288`) — the handle a `BindCohort` owner names | + | Mid-callback execution | `current_execution_point` (`native_host.hpp:1087`), `inspect_current_execution` (`native_host.hpp:1099`) / `execute_current` (`native_host.hpp:1104`) | + | Margin seams | four, not one: the run spec's own `margin` model (`NativeMarginModel` `native_run_spec.hpp:240`) with its three hooks — `resolve_margin_requirement` (`native_host.hpp:976`), `margin_check_allowed` (`native_host.hpp:988`), `resolve_margin_call_units` (`native_host.hpp:995`) — and `validate_execution_precommit` (`native_host.hpp:960`), whose `AdmitWithHostMargin` verdict hands the opening check to the host | + | Risk seam | `NativeRiskLimits` (`native_run_spec.hpp:310`) with `native_risk_state()` (`native_host.hpp:1323`); the breach appends a `NativeRiskEvent` to the command history | + | State reads | `physical_position()` (`native_host.hpp:1292`) / `native_marked_equity(mark)` (`native_host.hpp:1301`) / `native_open_lots(mark)` (`native_host.hpp:1297`), `trail_state(handle)` (`native_host.hpp:1092`), and the rest of the query surface in `docs/pages/native-engine.md`, "Reading the run back" | + | Lookahead hazard | the `Bar` given to `on_native_bar_open` is the *complete* script bar; `current_partial_bar()` (`native_host.hpp:1073`) is the lookahead-free bar so far, and `NativeOpenBarView::OpenOnly` (`native_run_spec.hpp:109`) masks that one callback | - **The run spec is generic, and no longer narrow.** `NativeRunSpec` - (`native_run_spec.hpp:553`) owns instrument and clock facts, fees + (`native_run_spec.hpp:554`) owns instrument and clock facts, fees (`NativeFeeKind` `native_run_spec.hpp:19`), a scalar account FX, close timing, the quantity grid, direction and size caps, the intrabar path — and, each opt-in and each folded into the continuation identity only when set: the instrument price grid (`NativePriceGrid` @@ -139,9 +139,9 @@ summary of it. `native_run_spec.hpp:310`), the report policy (`NativeReportPolicy` `native_run_spec.hpp:61`), calculation timing (`NativeCalculationTrigger` `native_run_spec.hpp:94`) and the open-bar view, declared higher-timeframe series - (`NativeTimeframeSubscription` `native_run_spec.hpp:485`) and the auxiliary finer feed - (`NativeAuxiliaryFeed` `native_run_spec.hpp:514`). `initial_margin_fraction` - (`native_run_spec.hpp:597`) remains the one-scalar admission-only spelling and is mutually + (`NativeTimeframeSubscription` `native_run_spec.hpp:486`) and the auxiliary finer feed + (`NativeAuxiliaryFeed` `native_run_spec.hpp:515`). `initial_margin_fraction` + (`native_run_spec.hpp:598`) remains the one-scalar admission-only spelling and is mutually exclusive with `margin`. Percent and cash sizing are a *request* value, not a spec one (`Sized` `native_order.hpp:164`). What the spec still owns none of is source strategy policy: the field comments say so, and `scripts/check_adapter_spec_shadowing.py` holds the adapter to @@ -155,9 +155,9 @@ summary of it. - **Data / HTF.** The magnifier (`magnifier.hpp`) reconstructs intrabar fills. `request.security`-style series **are** reachable from a bare host, as declared series of the run's own symbol: `NativeRunSpec::subscriptions`, or `declare_timeframe_subscriptions` - (`native_host.hpp:1124`) from inside `on_native_run_begin`, with - `on_native_timeframe_bar` (`native_host.hpp:873`) and `native_series_bar` - (`native_host.hpp:1107`) reading them back, and `NativeAuxiliaryFeed` + + (`native_host.hpp:1136`) from inside `on_native_run_begin`, with + `on_native_timeframe_bar` (`native_host.hpp:885`) and `native_series_bar` + (`native_host.hpp:1119`) reading them back, and `NativeAuxiliaryFeed` + `NativeSeriesSource::AuxiliaryFeed` for a series finer than the input. The kernel owns the aggregation, the `lookahead`/`gaps` delivery rules and the lazy-seal chronology; TradingView's `request.security` *semantics* are not in it. `prepare_native_security_feeds` stays protected @@ -167,7 +167,7 @@ summary of it. authoritative bars and still throws in-run (`guard_native_mutation` `engine_aux_security.cpp:78`). Indicators are `ta_*.cpp`, session/calendar `session_time.cpp` / `native_calendar.cpp`, FX - `configure_native_fx_curve` (`native_host.hpp:1190`). + `configure_native_fx_curve` (`native_host.hpp:1202`). - **Reporting.** `fill_report` (`engine.hpp:1843`, `src/engine_report.cpp:40`) gives a bare host trade stats, and the equity curve is a run-spec choice rather than a host chore. `NativeReportPolicy::HostRecorded` (the default) leaves the series to the host, whose @@ -180,7 +180,7 @@ summary of it. `compute_equity_stats` answers its all-NaN value with no error (`src/engine_metrics.cpp:219`, consumed by `fill_metrics_section`, `src/engine_report.cpp:119-139`) — which is why the policy exists. - **Reproducibility.** Two digests, one wrapping the other. `native_continuation_hash()` - (`native_host.hpp:1358`) returns the consumer's event/continuation hash. **A digest names + (`native_host.hpp:1370`) returns the consumer's event/continuation hash. **A digest names the run's inputs** (rule 2): the timezone identity enters it as the zone's *content* — kind, effective definition and `TimezoneIdentityDescriptor::resource_digest`, an FNV-1a over the zone files the resolver read (`native_calendar.hpp:233`) — and never as @@ -245,12 +245,12 @@ kernel and own these quirks, each at its site: quantities leave no remnant row on a `lab tv` tape, which the Pine host books row for row while a bare host books four 2.8e-17-unit dust rows beside them. - **Close reservations.** `strategy.close` callsite batching, two-call provenance and the entry-id - ledger (`close_logical_units_` `pine_adapter.hpp:2016-2027`); the POOC reservation-growth population predicate + ledger (`close_logical_units_` `pine_adapter.hpp:2027-2038`); 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:14100`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14136`) for the ten-significant-digit money, + `resolve_margin_requirement` (`pine_adapter.cpp:14164`) 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 @@ -261,7 +261,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:16835`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16863`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -286,7 +286,13 @@ kernel and own these quirks, each at its site: `process_orders_on_close`, `calc_on_order_fills`, magnified or timestamped-FX run keeps its own route, unmeasured against TradingView, an add to a carried book is still refused there, and the default-percent stop entry the pre-open slice answers for keeps that slice's verdict - (`preopen_slice_class`). + (`preopen_slice_class`). Under the bar magnifier the adapter admits the kernel's + `IntrabarSample` points on a leveraged book (`intrabar_sample_checked`): TradingView's magnified + broker books its call at the first lower-timeframe low that crosses and again at each later one + that crosses the reduced book's line (`tests/fixtures/intrabar_margin`: every row on 3 of 4 + tapes; on the fourth it books its second call a minute after the low this check books it at -- + a recorded divergence, open). A full-margin book, `process_orders_on_close` and + `calc_on_order_fills` keep their routes there, unmeasured. - **Calculation timing.** The cadence itself is the kernel's: a `calc_on_order_fills` strategy projects `NativeCalculationTrigger::BarCloseAndFills` with TradingView's guard literal as `max_recalculations_per_point`. What stays are the specifics COOF adds on top — @@ -337,7 +343,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13, for instance, pulls only `engine_internal.hpp`, `ta.hpp` and std headers. No kernel signature names an adapter type either: the rich begin bridge takes the host overrides as a plain `const void*` (`engine.hpp`, `execution_consumer.hpp`) - and forwards it as `overrides_opaque` (`native_host.hpp:746`), which the kernel never dereferences; the + and forwards it as `overrides_opaque` (`native_host.hpp:758`), which the kernel never dereferences; the source host casts it back (`pine_strategy_host.cpp`). `check_native_include_independence.py` therefore whitelists no source symbol at all. - **The four rules earlier drafts named — one survives as code, and it is hashed.** The @@ -346,7 +352,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:454-460`). `strategy.close` batching and the entry-id ledger - are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2016-2027`) and + are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2027-2038`) 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:424`); what enables the generic model is the presence of `NativeRunSpec::margin`. So is the string-sentinel decoding of adapter-written comments: @@ -377,11 +383,11 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `src/c_abi.cpp` implements the **58** codegen-facing runtime `PF_API` symbols that `scripts/check_c_abi_runtime.py` pins by name, and `src/native_c_host.cpp` implements the **43** additive symbols of ``. A C host creates a host from a - callback table (`strategy_native_host_create_v1` `native_c_api.h:2638`), runs a batch - (`strategy_native_run_v1` `native_c_api.h:2651`), and **submits, replaces, cancels and - executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2679`), `_replace_v1` - (`native_c_api.h:2701`), `_cancel_v1` (`native_c_api.h:2741`), `_cancel_all_v1` (`native_c_api.h:2745`), `_cancel_where_v1` (`native_c_api.h:2766`), - `_execute_current_v1` (`native_c_api.h:2787`) — under the kernel's own legality rule. Streaming needs no new + callback table (`strategy_native_host_create_v1` `native_c_api.h:2646`), runs a batch + (`strategy_native_run_v1` `native_c_api.h:2659`), and **submits, replaces, cancels and + executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2687`), `_replace_v1` + (`native_c_api.h:2709`), `_cancel_v1` (`native_c_api.h:2749`), `_cancel_all_v1` (`native_c_api.h:2753`), `_cancel_where_v1` (`native_c_api.h:2774`), + `_execute_current_v1` (`native_c_api.h:2795`) — under the kernel's own legality rule. Streaming needs no new symbol: the whole `strategy_stream_*` family takes that handle unchanged. The header's COVERAGE block carries one line per public member of `NativeStrategyHost` with either its C spelling or the reason it has none, and `scripts/check_native_c_api_surface.py` proves the @@ -544,6 +550,7 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | an OCA-Reduce sibling's deduction binary64 cannot take -- at most half an ulp of what it is taken from, so that fl(units - d) is the units again -- absorbed where the request core deducts a member's fill (`absorbed_deduction`, `pending_sum` and `absorbed_pending_delta` in `src/native_order.cpp`, read by `WorkingRequestCore::prepare_group_effect` / `apply_group_effect`, by `prepare_terms` / `apply_terms` through `effective_host_units`, and by `prepare_owner_applied` / `apply_owner_applied`), and recorded as a `ReservationReducedEvent` with `actual_deduction` 0, a `DeferredGroupAdjustmentEvent` with `deferred_delta` 0 that joins no chain, or a `TermsResolvedEvent` / `QuantityBoundEvent` with `effective_deduction` 0 beside a positive `pending_total` (`include/pineforge/native_order.hpp`; C: `closed_units` 0 on a `PF_NATIVE_EVENT_RESERVATION_REDUCED` or `PF_NATIVE_EVENT_DEFERRED_GROUP` row) | any host, by the OCA-Reduce groups it submits (C: `PF_NATIVE_GROUP_REDUCE`), and the Pine adapter by the `strategy.oca.reduce` groups it maps (`PineExecutionAdapter::group_for`, `src/source/pine_adapter.cpp`) | **absorbed as a no-op** (R5 lane K-ULP5, the owner-level ruling on K-ULP4's finding 2). Rule 2 (a), mechanism: binary64 bookkeeping is venue-neutral -- a deduction too small to move the sibling's remaining units (or its pending total) leaves them where binary64 subtraction puts them, unchanged, for a host of any venue, and the member's fill stands; a pending total below half an ulp of a later fill rounds into it, as the addition does. Rule 2 (b), knob: none -- it replaces a whole-run `SettlementFailure` (code 6, discriminator 7, after the fill was booked) that no host could want, a deduction binary64 can take is taken as before, bit for bit, and only a pending total that overflows binary64 still fails the run with that discriminator. What moved: runs that stopped on such a deduction now complete -- K-ULP4's p5b probe on the C++ and the C host, and the 914 of the 3,000 runs of the seeded OCA-Reduce battery that stopped on f1af50dc -- and the rows that pinned the old contract now pin the absorption (the G5 rows of `tests/test_native_resting_acceptance.cpp` and `tests/test_native_resting_replay_contract.cpp`, `checked_reservation_arithmetic` of `tests/test_native_order_resting_core.cpp`, A-T4d of `tests/test_native_execution_terms.cpp`, `numeric_absorption_and_chain_authentication` of `tests/test_native_order_terms_core.cpp`); the overflow rows still pin the failure. No completed run's deductions move -- each was one binary64 could take -- and `inspect_current_execution`, which threw `std::overflow_error` for a host-sized request whose pending total its units absorb, now answers it. Pinned by `tests/test_native_group_absorption.cpp` (kernel-only) and `check_reservation_absorbed` of `tests/test_native_c_api.c`. | | the order of one cause's group-effect receipts -- the queue order `WorkingRequestCore::group_recipients` walks, which a `ReplaceOptions::keep_handle` re-price parts from incarnation order: `receipt_lookup` bisects the receipts by cause and walks that cause's, and `prepare_group_effect` / `apply_group_effect` refuse only a cause older than the newest receipt's (`src/native_order.cpp`; `WorkingRequestCore::group_effect_receipt`, `include/pineforge/native_order.hpp`) | any C++ host that re-prices a group member with `keep_handle` and then fills a sibling (`NativeStrategyHost::replace(handle, request, options)`; no C spelling, the C replace takes no options); the Pine adapter never sets `keep_handle` | **generic: a drain applies a group's effect in queue order, whatever the handles' numbers** (R5 lane K-OCA-KEEP, K-ULP5's finding 1). Rule 2 (a), mechanism: which live siblings a member's fill reaches, and in what order, is the book's own queue for a host of any venue, and the receipts are that drain's idempotence record, in the order it applied them; a request re-priced with its handle kept is reached where a plain replace's successor is, so the two runs record one timeline, as V19-D's contract already said. Rule 2 (b), knob: none -- it replaces a whole-run `Contract` failure (code 2, discriminator 1, `CoreFailure::InvalidCause`, after the member's fill was booked) that no host could want: the receipt check held one cause's receipts to incarnation order, and the re-priced member, drained behind a younger sibling, was refused; a cause older than the newest receipt's is still refused. What moved: runs that stopped on such a drain now complete -- K-ULP5's `probe_keep_handle_oca`, staged and direct, under `Cancel` and `Reduce`, and all 180 runs of the seeded battery under a keep_handle option set (of its 360), which stopped on 2a03c658 -- each recording the plain replace's run event for event. No completed run moves and no hash value moved: a completed run's receipts were already in incarnation order within each cause, and the lookup answers such a store as before (the K-ULP4 and K-ULP5 batteries' per-run hashes unchanged; corpus parity unchanged by construction, the Pine adapter never setting `keep_handle`). Pinned by `tests/test_native_group_keep_handle.cpp` (kernel-only). | | a request on a quantity grid whose pending group deduction its units absorb -- it settles them all in one fill (`settles_in_one_fill`, `src/native_execution_consumer.cpp`, which asks `WorkingRequestCore::effective_host_units` whether the pending total takes any), so a whole-scope `ScopeFraction` resolves to its scope's held total unfloored and a host-sized close answered with that total is its own quantity, at the match and at the current execution alike | any host, by the OCA-Reduce groups and the quantity grid it declares (C: a fraction, and a close sized by `on_close_units`); the Pine adapter does not reach it: it emits no `ScopeFraction`, and it answers a close of its scope's whole total with `ExecutionGridPolicy::ExplicitUnits`, whose grid check never asks whether the request settles in one fill | **one definition of settling in one fill** (R5 lane K-OCA-KEEP, K-ULP5's finding 2). Rule 2 (a), mechanism: K-ULP4's grid rule admits the book's own quantity for a request that settles it in one fill, and K-ULP5's absorption makes a sub-ulp pending total take nothing, for a host of any venue; the two now agree. Rule 2 (b), knob: none. What moved: a run in which such a request closes its scope's total now closes it whole -- a whole-scope fraction was floored onto the grid and closed short, leaving a dust lot (before K-ULP5 such a run stopped wherever the floored units absorbed the total too, code 6, discriminator 7), and a host-sized close answered with its scope's total was refused `InvalidTerms` and left the lot open, before K-ULP5 as after: the `grid-absorbed-*` rows of `tests/test_native_group_keep_handle.cpp`, whose fraction closed 1.1000000000000001 of a 1.1000000000000227 lot on 2a03c658. K-ULP4's own battery answers every scenario's hash as before, K-ULP5's absorption and C-host batteries are unchanged (no grid), `grid-fraction-oca-pending` of `tests/test_native_unrepresentable_refusal.cpp` (a pending total that takes 0.3) is still floored, and no pinned hash moved. Pinned by `tests/test_native_group_keep_handle.cpp` (kernel-only). | +| the margin model's check points on an intrabar path -- `NativeMarginCheckKind::IntrabarSample` (`include/pineforge/native_host.hpp`; C `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE`), offered at every delivered sample after the script bar's first of a `lower_tf` path matched as `ContinuousSegments`, at that sample's price, immediately before it is matched (`NativeExecutionConsumer::deliver_intrabar_script`, `src/native_execution_consumer.cpp`) | every host with a margin model and such a path, through `margin_check_allowed`; the Pine adapter admits it on a magnified run's leveraged book (`PineExecutionAdapter::intrabar_sample_checked`, `src/source/pine_adapter.cpp`) | **a check point of the kernel's own** (R5 lane PAR-MARGIN, item 3; H-MEASURE's acid finding A). Rule 2 (a), mechanism: a run with no whole-bar waypoint model measures its maintenance requirement where its path is -- at each delivered sample -- the rule the docs and `native_run_spec.hpp` stated and the kernel had not kept (it checked the first sample and after fills only); nothing in it knows a venue, and a host that checks somewhere else suppresses the points it does not share, as for every other kind. Rule 2 (b), knob: none -- the path is the run's own declaration, and a one-price distribution path (`synthesized`, `DistributionSamples`) is not offered the point, since a request armed at a discrete point is matched only at a later one. What moved: a margin run on a continuous intrabar path whose book crosses its line inside a bar now books that call at the crossing sample (H-MEASURE's intrabar probe: 55 points offered, a slice at 105 where none was booked). TradingView's magnified broker does the same (`tests/fixtures/intrabar_margin`: the first call at the first lower-timeframe low that crosses on 4 of 4 tapes, every row on 3). No pinned value moved. Pinned by `tests/test_native_margin_intrabar_samples.cpp` (kernel-only) and the "intrabar margin on tapes" section of `tests/test_adapter_margin_schedule_differential.cpp`. | | the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:1825` and `:1934`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:295`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | | the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1478`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | | the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1616`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | @@ -684,30 +691,30 @@ number names its symbol on this tree. | C++ hook | C route | witness in `tests/test_native_c_api.c` | |---|---|---| -| `WaitForApplied::first_match` / `WaitForApplied::scope` | `arm_first_match` (`native_c_api.h:2180`), `pf_native_request_v1`'s fifth published layout; a Book-scoped child may be the host-sized close `on_close_units` sizes | the arm-relation scenario | -| `native_sized_units` | `strategy_native_sized_units_v1` (`native_c_api.h:3048`), reading a SIZED request's own sizing block | the sizing-query scenario | -| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2565`): price, opening shape, grid policy, beside the units half `on_close_units` | the terms-hook scenario | -| `validate_execution_precommit` | `on_precommit` (`native_c_api.h:2574`): the plan, the inspection and the projected account, the closed rows' P&L borrowed for the call | the precommit scenario | -| `resolve_anchored_level` | `on_anchored_level` (`native_c_api.h:2581`) | the anchored-level scenario | -| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2592`): a 64-bit digest folded after the kernel's own bytes | the hash-extension scenario | +| `WaitForApplied::first_match` / `WaitForApplied::scope` | `arm_first_match` (`native_c_api.h:2185`), `pf_native_request_v1`'s fifth published layout; a Book-scoped child may be the host-sized close `on_close_units` sizes | the arm-relation scenario | +| `native_sized_units` | `strategy_native_sized_units_v1` (`native_c_api.h:3060`), reading a SIZED request's own sizing block | the sizing-query scenario | +| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2571`): price, opening shape, grid policy, beside the units half `on_close_units` | the terms-hook scenario | +| `validate_execution_precommit` | `on_precommit` (`native_c_api.h:2580`): the plan, the inspection and the projected account, the closed rows' P&L borrowed for the call | the precommit scenario | +| `resolve_anchored_level` | `on_anchored_level` (`native_c_api.h:2587`) | the anchored-level scenario | +| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2598`): a 64-bit digest folded after the kernel's own bytes | the hash-extension scenario | **What stays as it is, and why.** | surface | ruling | reason | |---|---|---| -| `cancel_all` (`native_host.hpp:1255`) / `cancel_where` answer a count, not a `CancelResult` per request (E12 f7) | **retained** | A bulk cancel is many commands in one call, and each withdrawn request records its own `CancelledEvent` with its own `CancelReason` — dependants of a cancelled owner included — in the history a host already polls (`strategy_native_events_v1`). That is the per-request answer; the count is the call's summary. A result vector would restate the history in a second, allocating shape that C could not take without a caller-sized array. The C spellings answer the same count (`strategy_native_cancel_all_v1` `native_c_api.h:2745`). | -| `cohort_add` (`native_host.hpp:1273`) / `cohort_remove` answer `void` (E12 f7) | **retained; the typed receipt is a named follow-up** | The kernel judges every enrolment into a `CohortReceipt` (Applied, InvalidHandle, UnknownOrigin, TerminalOrigin) and folds it into the continuation (`hash_cohort_receipt` `native_execution_consumer.cpp:890`, since v19 once, at the enrolment, into the running digest the continuation carries), so a replay that diverges there diverges in the hash. Nothing that matches or settles reads it: a roster is a relation the close reads at its match, and a refused enrolment is a member the close does not take. A host that needs the receipt AT the call needs `NativeStrategyHost::cohort_add_result` first, a kernel-header change outside this lane; its C spelling would then be an `_ext_v1` with a receipt word. Until then `strategy_native_cohort_add_v1`'s `PF_NATIVE_OK` means the enrolment was issued. | -| `native_sized_units` (`native_host.hpp:1302`) / `native_liquidation_price` answer `std::optional` (E12 f7) | **retained** | Observation queries, not commands: each empty is documented cause by cause in its own comment (unconfigured, non-positive money or denominator, a below-one-step quotient; no margin model, no maintenance fraction for the side, a flat book, no finite solution), and every cause is a fact the host can read itself. A typed reason would name what the caller already knows. C answers `PF_NATIVE_ABSENT` and NaN (`strategy_native_liquidation_price_v1` `native_c_api.h:3020`). | -| `pf_native_working_v1` without the leg's anchor or owner relation (E13 f4) | **carried** | The C++ host reads them off `NativeWorkingRequest::definition`, so the C readout's relation tail carries them: `anchor` (`native_c_api.h:1683`) through `arm_scope`, the request as the kernel holds it now (an armed leg reads ABSOLUTE with its installed level). | +| `cancel_all` (`native_host.hpp:1267`) / `cancel_where` answer a count, not a `CancelResult` per request (E12 f7) | **retained** | A bulk cancel is many commands in one call, and each withdrawn request records its own `CancelledEvent` with its own `CancelReason` — dependants of a cancelled owner included — in the history a host already polls (`strategy_native_events_v1`). That is the per-request answer; the count is the call's summary. A result vector would restate the history in a second, allocating shape that C could not take without a caller-sized array. The C spellings answer the same count (`strategy_native_cancel_all_v1` `native_c_api.h:2753`). | +| `cohort_add` (`native_host.hpp:1285`) / `cohort_remove` answer `void` (E12 f7) | **retained; the typed receipt is a named follow-up** | The kernel judges every enrolment into a `CohortReceipt` (Applied, InvalidHandle, UnknownOrigin, TerminalOrigin) and folds it into the continuation (`hash_cohort_receipt` `native_execution_consumer.cpp:890`, since v19 once, at the enrolment, into the running digest the continuation carries), so a replay that diverges there diverges in the hash. Nothing that matches or settles reads it: a roster is a relation the close reads at its match, and a refused enrolment is a member the close does not take. A host that needs the receipt AT the call needs `NativeStrategyHost::cohort_add_result` first, a kernel-header change outside this lane; its C spelling would then be an `_ext_v1` with a receipt word. Until then `strategy_native_cohort_add_v1`'s `PF_NATIVE_OK` means the enrolment was issued. | +| `native_sized_units` (`native_host.hpp:1314`) / `native_liquidation_price` answer `std::optional` (E12 f7) | **retained** | Observation queries, not commands: each empty is documented cause by cause in its own comment (unconfigured, non-positive money or denominator, a below-one-step quotient; no margin model, no maintenance fraction for the side, a flat book, no finite solution), and every cause is a fact the host can read itself. A typed reason would name what the caller already knows. C answers `PF_NATIVE_ABSENT` and NaN (`strategy_native_liquidation_price_v1` `native_c_api.h:3032`). | +| `pf_native_working_v1` without the leg's anchor or owner relation (E13 f4) | **carried** | The C++ host reads them off `NativeWorkingRequest::definition`, so the C readout's relation tail carries them: `anchor` (`native_c_api.h:1690`) through `arm_scope`, the request as the kernel holds it now (an armed leg reads ABSOLUTE with its installed level). | | `hash_source_extension` (`engine.hpp:411`) | **C++-only** | The deprecated spelling of `hash_host_extension`, kept so an existing C++ subclass compiles and folds unchanged. A C host has only ever had the current spelling, `on_hash_extension`. | | `NativeReportPolicy::KernelRecordedAtHostMarks` (`native_run_spec.hpp:64`) | **C++-only** (lane E22's ruling, restated) | Under it the host names each report point from inside its own callbacks, and the C table has no call that marks one: a C host could only declare a series nobody records. `pf_native_report_policy_e` (`native_c_api.h:657`) leaves its value unnamed, and the enum guard holds that exclusion. A C++ host now reaches the same kernel producer through `NativeStrategyHost::mark_native_report_point`; a mark outside a running HostMarks run returns false. | | `strategy_set_path_order` (`pineforge.h:967`) clamps an out-of-range mode to AUTO | **retained** | A `void` ABI v4 setter cannot answer a refusal. Its values are now documented as `pf_native_path_order_e`'s (`native_c_api.h:900`); a C host that wants an unknown word refused declares `path_order` in `pf_native_run_spec_ext_v1`, which answers `PF_NATIVE_E_TAG`. | | the enumerated `int32` fields of `pf_pending_order_v1_t` (`strategy_pending_order_get` `pineforge.h:1061`) | **no C names in the header; generator-owned** | The mirror is generated from the Pine source host's resting-order record by `scripts/gen_pending_order_mirror.py` (held by its `--check` source guard) and self-described at run time by `strategy_pending_order_layout`. Its enumerated fields are the source layer's order vocabulary, not a kernel enumeration. A C name for them belongs in that generator, which owns the struct, so that it cannot drift from the mirror — never hand-written beside it. | -| `strategy_configure_native_v1` (`c_abi.cpp:896`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:3979`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | +| `strategy_configure_native_v1` (`c_abi.cpp:896`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:3982`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | | `strategy_configure_native_fx_curve_v1` (`pineforge.h:584`) | **typed additive route** | `strategy_configure_native_fx_curve_ext_v1` writes the C twin of `NativeFxCurveValidation` and its offending index. The old `-1` route remains unchanged, while a C caller can now distinguish a decreasing timestamp, non-positive rate, allocation failure or wrong phase. | -| `on_native_timeframe_bar` (`native_host.hpp:873`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:797`) | **query routed** | The callback signature stays frozen. `strategy_native_timeframe_bar_interval_v1` is valid only during that callback and copies the kernel calendar interval without deriving it from the delivered bar or delivery timestamp. | -| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7644-7656`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1085-1090`) sees the same values, so no hash contract moves. | -| `pf_native_lot_excursion_v1` (`native_c_api.h:1871`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | +| `on_native_timeframe_bar` (`native_host.hpp:885`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:809`) | **query routed** | The callback signature stays frozen. `strategy_native_timeframe_bar_interval_v1` is valid only during that callback and copies the kernel calendar interval without deriving it from the delivered bar or delivery timestamp. | +| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7629-7641`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1085-1090`) sees the same values, so no hash contract moves. | +| `pf_native_lot_excursion_v1` (`native_c_api.h:1876`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | ## TradingView-calibrated kernel mechanisms (rule 2's own account) @@ -764,8 +771,8 @@ The table records, at the tree the doc wave read, what each block described and | `ta::ATR` `include/pineforge/ta.hpp:321` (the `TradingView` comment `sha256:4f6f6939aec2773684b114a13dd002eb58990d6d96c3056ad87361753faf7a0a` :333-340) | average true range over Wilder's `RMA` | Wilder's ATR is the textbook definition; the comment records which convention and the tape that pinned the recompute behaviour | a seeding/averaging field on an indicator whose definition is the convention | `tests/test_ta_rma_warmup.cpp`, `tests/test_recompute.cpp` | | `ta::StdDev` `include/pineforge/ta.hpp:430` (the `TradingView` comment `sha256:7cc1bbcd32dfe673285d51a8ca7080890ac551858099b523480d998f46408666` :420-429) | the population (biased) standard deviation | biased vs sample is a textbook choice, and the kernel states which | a per-instance switch is the *right* answer where two conventions genuinely compete, which is why `ta::EmaSeeding` exists and this one does not need it | `tests/test_ta_indicators_extras.cpp` | | `float_band_eq` `include/pineforge/ta_compare_band.hpp:38` | an absolute 1e-10 float comparison for indicator equality | an epsilon band is needed by any float comparison; the width is the calibration | a band-width field on a comparison the indicators share | `tests/test_dmi_parity.cpp`, `tests/test_ta_osc_edge.cpp` | -| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8605-8608` (lookahead) and `clear_if_gapped` `:8599-8602` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | -| `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2230`, `::gaps` `include/pineforge/native_c_api.h:2234` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:795`, `pf_native_gaps_e` `:806`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | +| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8587-8590` (lookahead) and `clear_if_gapped` `:8581-8584` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | +| `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2235`, `::gaps` `include/pineforge/native_c_api.h:2239` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:795`, `pf_native_gaps_e` `:806`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | | the authoritative-feed partition `src/engine_aux_security.cpp` | a feed is the venue's own bars of one timeframe; its stamps are the period partition | already ruled: design §2.ii row s, and the residual table's own row | the policy knob is installing the feed or not | `tests/test_native_htf_subscriptions.cpp`, `tests/test_native_security_feed.cpp` | | `session_template_knows_early_close` `src/engine_security.cpp` | the instrument-class classification | already ruled: design §2.ii row t; `SymInfo::type`'s vocabulary is fixed by the frozen C ABI | — | `tests/test_native_wm_buckets.cpp` | | `skip_entry_bar_high` / `skip_entry_bar_low` `include/pineforge/engine.hpp:153` | the intrabar-fill excursion mask: the part of a bar traversed before a priced entry filled is not that trade's excursion | the mask is generic — an excursion is measured from the fill — and since R5 lane E6 no host raises the flags: the excursion owner declares where its fill sat and the kernel derives the pair from the bar's path (`declare_opened_lot_entry_bar_mask` `src/engine_path_resolve.cpp:95`); with no declaration the kernel samples the whole bar | already spelled the generic way: `owns_lot_excursions()` + `closed_lot_excursion` hand the whole measurement to a host | `tests/test_l11a_host_excursion.cpp` | @@ -778,10 +785,10 @@ The table records, at the tree the doc wave read, what each block described and | the indicator library -- `include/pineforge/ta.hpp`, `src/ta_moving_averages.cpp`, `src/ta_oscillators.cpp`, `src/ta_extremes_volume.cpp`, `src/ta_volatility_trend.cpp`, `src/ta_misc.cpp`, `include/pineforge/window_sum.hpp`, `include/pineforge/math.hpp` | each indicator implements its Pine `ta.*` / `math.*` definition -- warm-up and na rules, the window call-site ring, the compensated window sum, tie and epsilon rules -- and its comments record the TradingView tape or export that calibrated each number (the `ta::ATR` and standard-deviation rows above are two of them) | an indicator is a function of its inputs, the same for every venue; generated strategies call exactly these classes, and where two textbook conventions genuinely compete the choice is a per-instance option (`ta::EmaSeeding`), never a branch on the platform | a convention switch on every indicator, for a choice the script already made by calling that Pine function | the kernel-only TA suites `tests/test_ta_*.cpp` and the corpus parity gate | | the formatting library -- `include/pineforge/str_utils.hpp`, `src/str_utils.cpp` (`str_tostring`, `str_format_values`, `str_format`) | the number text of Pine's `str.tostring` / `str.format` built-ins: a number's shortest round-trip decimal spelling, scaled and rounded half-up on its digits, the default, `percent`, `volume` (K / M / B / T) and pattern modes, MessageFormat placeholders and quoting, `NaN` and `Infinity` -- the published rendering TradingView's exported tapes pin (R5 lane C6), ported from the codegen formatter (lane C6b) by R5 lane B-ENGINE; the `mintick` mode, which generated code delegates to it, is kept as it was | a formatting library is a function of its inputs, the same for every venue, as the indicator library is: it spells a number the way the source language's own `str.*` library defines, and no kernel order, fill, settlement, margin or hash code calls it -- of the thirty-five kernel translation units only `src/str_utils.cpp` names these functions | a rendering switch on functions whose definition the calling script chose by calling them | `tests/test_str_number_format.cpp` (lane C6b's 42-case edge battery, every numeric string of lane C6's TradingView exports, byte-identical output on macOS arm64 and Linux x86-64) and `tests/test_str_utils.cpp` | | the language calendar functions -- `include/pineforge/session_time.hpp`, `src/session_time.cpp` (`timeframe_time`, `local_hour` and its siblings, `session_in_market`, `session_trading_day_open_ms`, ...) | the definitions of Pine's `time()` / `time_close()` / bare-time / `session.is*` built-ins for generated code: session and timezone parsing, the invalid- and 24-hour-session spellings, `time("60")` day keying, `session.is*` on daily charts, each comment naming the tape that pinned it | the inputs are data -- a session string, a timezone, a timeframe -- so another venue is driven by its own session template; nothing branches on a platform, and a native host's calendar is the separate generic `native_calendar` | a rules switch on functions whose definition the generated script chose by calling them | `tests/test_session_time.cpp`, `tests/test_pine_time_day_stamp_grid.cpp` and the corpus parity gate | -| `present_session_day` `src/native_execution_consumer.cpp:7268`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (`tests/fixtures/session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | `tests/test_native_session_day_facts.cpp` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), the C scenario of `tests/test_native_c_api.c`, and `tests/test_session_day_facts_adapter.cpp` (the adapter selects them flag for flag) | -| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7856-7872` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | -| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7833-7854` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | -| `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6601` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | +| `present_session_day` `src/native_execution_consumer.cpp:7269`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (`tests/fixtures/session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | `tests/test_native_session_day_facts.cpp` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), the C scenario of `tests/test_native_c_api.c`, and `tests/test_session_day_facts_adapter.cpp` (the adapter selects them flag for flag) | +| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7893-7909` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | +| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7870-7891` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | +| `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6606` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | | `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:6342` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | | the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:334`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1541`, `:1556`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:1854`). A bare host never calls the builder, so its calendar stays nominal | `tests/test_o_close_pct_day_anchor.cpp` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | | the bar-time accessors -- `_decompose_bar_time` and `_bar_hour` through `_bar_weekofyear` `include/pineforge/engine.hpp:1066-1093` | the presented bar's stamp broken into calendar fields in UTC, memoized per stamp | a function of the stamp alone, and UTC is the engine's storage timezone | none: they read no spec field and fold into no digest. They are protected `BacktestEngine` members, part of the codegen contract. Their comment claimed that generated code reads them for Pine's bare `hour` / `minute` / `dayofweek`. It does not: the codegen transpiler emits `pine_hour(current_bar_.timestamp, syminfo_.timezone)` and its siblings on the exchange timezone (`pineforge_codegen/codegen/tables.py` at the corpus's pinned commit `f3285d79`), which are the language calendar functions above. No generated strategy in `corpus/` names a `_bar_*` accessor. The comment now says so (R5 lane H-DOCGATES) | `tests/test_chart_timezone.cpp` (`_bar_hour()` is the UTC hour) and `tests/test_intraday_rollover_chart_tz_l4a.cpp` | @@ -868,7 +875,7 @@ no new kernel policy. indicator library, the formatting library, the language calendar functions) apply the test and state their evidence. 2. The kernel changes only for a *generic* capability with a recorded ruling (e.g. per-lot - excursion ownership, `owns_lot_excursions` `native_host.hpp:1011`; the market-if-touched + excursion ownership, `owns_lot_excursions` `native_host.hpp:1023`; the market-if-touched `fill_through` flag, `native_order.hpp:239-242`). **The test is not whether the word "TradingView" appears — a calibrated number has to say what calibrated it. The test is this pair, and both halves must pass:** diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 12d73bad..a2eac908 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,29 +47,29 @@ 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:2738-2749; `entry()` pine_adapter.cpp:5311, `order()` pine_adapter.cpp:10512) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5680-5727; `submit` native_host.hpp:1201 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2738-2749; `entry()` pine_adapter.cpp:5311, `order()` pine_adapter.cpp:10512) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5686-5733; `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:8417 → `Flatten{}` pine_adapter.cpp:8446-8447; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2708-3915 → `host.replace` pine_adapter.cpp:2724-2804) | **yes** — `replace` native_host.hpp:1206-1208, `cancel` native_host.hpp:1237; `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:4723-4769, `cancel_all` pine_adapter.cpp:18063-18078; `live_by_source_key_` pine_adapter.hpp:1988; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **partial** — single-handle `cancel` native_host.hpp:1237; 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:1255, `cancel_where(comment)` native_host.hpp:1260 and `cancel_where(text, NativeRequestField::Label)` native_host.hpp:1260 withdraw by predicate and answer how many left the book; `native_working_requests()` native_host.hpp:1251 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. | +| OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:2708-3915 → `host.replace` pine_adapter.cpp:2724-2804) | **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:4723-4769, `cancel_all` pine_adapter.cpp:18091-18106; `live_by_source_key_` pine_adapter.hpp:1999; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5535-5570, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7505-7522) | **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:1270, `CancelReason::OwnerGone` native_order.hpp:949 | K | — | F:A6 O:A8 O:A9 S:O5 | FIFO *ordering* is host-computed (O) | +| 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:5535-5570, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7505-7522) | **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:2747-2753) | **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:1092-1093 | K | — | F:A9 | (single-source row; O:F2 cites the same call) | +| 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:13035; lowering `submit_one` pine_adapter.cpp:9072-9084, `BindCohort` pine_adapter.cpp:9573-9579, `submit_leg` pine_adapter.cpp:9597-9621; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8677-8680; `materialize_relative_exits` pine_adapter.cpp:17353-17394, pine_adapter.hpp:1823-1835) | **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:5606-5640. `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:1000 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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19134-19163) | **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:1251 returns one owning `NativeWorkingRequest` native_host.hpp:669 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:2000-2018); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10912-10936), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15662, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15896) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:684-686 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8677-8680; `materialize_relative_exits` pine_adapter.cpp:17381-17422, pine_adapter.hpp:1824-1836) | **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:5612-5646. `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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19162-19191) | **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:2001-2019); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10912-10936), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1585-1608, `CloseCallsiteState` pine_adapter.hpp:1679-1694; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15690, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15924) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). What `tests/test_adapter_margin_revival_cancel.cpp` replays on the NYSE:F 15m chart of seven lab tv tapes: badapter-v19dp1-control (no cancel: the margin call closes 928 through X) and v19fix-gapped-stop-control (X's stop at 10.15, gapped by the 05-01 open and parked: the same close), and badapter-v19dp1-cancel, v19fix-cancel-before-pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair (a cancel on 05-01 09:30 while X is dormant, on 04-30 15:45 before the reversal pair and on 04-30 10:00 while X is live, and on 05-01 09:30 and after the pair while X is parked; one tape file: margin calls 12 @10.39 and 92 @10.57, 836 open). Untaped, the same row holds the tapes' trades for `strategy.cancel_all` on 05-01 and before the pair and for `strategy.cancel` after the pair, and on the same bars with no quantity grid — where the pair holds X in the cancel's own callback — every spelling books what the cancel on the dormant exit books | ### 1.2 Trailing | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:5692-5714; `NativeTrailState` native_host.hpp:657-662, `trail_state` native_host.hpp:1080 | K | — | F:B1 O:B1 S:T1 | — | +| TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:5698-5720; `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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11811-11823) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4443-4448) **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:11724-11861) stays A. | -| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4022-4068) | **partial** — the read exists `trail_state` native_host.hpp:1080; `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:287-289) is A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11811-11823) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4452-4457) **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:11724-11861) stays A. | +| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4022-4068) | **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:287-289) is A. | | TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9623-9628, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11724-11861, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8647-8873, `trail_one_shot` pine_adapter.cpp:9712-9841 | n/a | A | — | F:B3 S:T2 | R5-1 | ### 1.3 Sizing bases and reservations @@ -77,92 +77,92 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5924-5928, pine_adapter.cpp:5924) | **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:10989-12193) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:933-937, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4922-4956 | 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:2596-2651) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4957-4959 **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: rounded equity `source_money_round` pine_adapter.cpp:2548, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:10989-12193) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4923-4957 | 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:2596-2651) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4964-4966 **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: rounded equity `source_money_round` pine_adapter.cpp:2548, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2557-2559) | **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:2609, `typed_entry_units` pine_adapter.cpp:2627; `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:2588; `percent_commission_live_equity` pine_adapter.cpp:4329-4369) | **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. | | SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2514-2529, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6672-6698) | **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:591, 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:4973-4976; the book's own quantities are on the grid -- a whole-scope `ScopeFraction` and a `Reduce` of a FIFO boundary of its scope (R5 lane K-ULP4) | K | — | F:C4 O:C5 S:S1 | **Disagree:** F, O partial / S yes. R5-3: the quantity grid is native already. L3's `Sized` intent reuses the snap policy. | +| SZ7 | Quantity step | **yes** — `quantity_grid` admission native_run_spec.hpp:592, 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:4980-4983; 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:12510-12538, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:8999-9309) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4444-4459) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6733), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7945), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12766), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13362, pine_adapter.cpp:13415) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15710) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18730) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7207-7558; ledger `close_batch_bar_` pine_adapter.hpp:2041-2052; 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. | +| 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:6733), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7945), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12766), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13362, pine_adapter.cpp:13415) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15738) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18758) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7207-7558; ledger `close_batch_bar_` pine_adapter.hpp:2042-2053; 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:1074) | 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:2362) | **yes** — `allowed_open_directions` native_run_spec.hpp:596 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:594 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13257-13260) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:595 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6230-6279, cap left unset `max_open_lots` pine_adapter.cpp:2361-2363) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13311-13376 and leaves the kernel gate unset pine_adapter.cpp:13310-13312) | **yes** — `initial_margin_fraction` native_run_spec.hpp:597; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13966-13967, `margin_pct` pine_adapter.cpp:13374) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:597 **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:14066-14244, `submit_margin_call_units` pine_adapter.cpp:14205-14251, `schedule_margin_call_path` pine_adapter.cpp:18216-18314) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:597-598; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13997-14033) | **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:983 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. | -| MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **no** **Closed:** `native_liquidation_price()` native_host.hpp:1308 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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16314-16330, `apply_fx_opening_margin_slice` pine_adapter.cpp:18315-18343) | **no** — staged FX curve `configure_native_fx_curve` native_host.hpp:1190 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:16805-16844) | **no** — observation field only `drawdown_limit` include/pineforge/source/market_admission.hpp:50; composable today from `native_marked_equity` native_host.hpp:1289 **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 | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2362) | **yes** — `allowed_open_directions` native_run_spec.hpp:597 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:595 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13257-13260) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6230-6279, cap left unset `max_open_lots` pine_adapter.cpp:2361-2363) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13311-13376 and leaves the kernel gate unset pine_adapter.cpp:13310-13312) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13966-13967, `margin_pct` pine_adapter.cpp:13374) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:14066-14272, `submit_margin_call_units` pine_adapter.cpp:14233-14279, `schedule_margin_call_path` pine_adapter.cpp:18244-18342) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13997-14033) | **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. | +| MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **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. | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16342-16358, `apply_fx_opening_margin_slice` pine_adapter.cpp:18343-18371) | **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:16833-16872) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13346-13349) | **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:13927-13941, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14953-14983) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14996-15230) | **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:13737-13816) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1311 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:13955, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14014, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14722-14762, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14246-14373, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14246) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15234-15292) | n/a | A | — | S:R2 | (single-source) | +| MG12 | Max intraday loss → flatten + block (`intraday_loss_breached` pine_adapter.cpp:13927-13941, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14981-15011) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15024-15258) | **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:13737-13816) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1323 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:13955, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14014, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14750-14790, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14274-14401, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14274) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged book, where TradingView books its call at the first lower-timeframe low that crosses (`tests/fixtures/intrabar_margin`, every row on 3 of 4 tapes, the fourth a recorded divergence). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15262-15320) | 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:881, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7523-7527 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16149) | **yes** — `on_native_bar_open` native_host.hpp:877 runs before the open match native_execution_consumer.cpp:7065-7069; `execute_current` legal native_host.hpp:1092 | 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:7008 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7059 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1061 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:863, `NativeInputContext` native_host.hpp:761-766 | K | — | O:F3 | (single-source row) | -| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5632, `flush_coof_tail` pine_adapter.cpp:16292, `end_coof_recalc` pine_adapter.cpp:5653) | **partial** — `on_native_applied` native_host.hpp:926 fires mid-path after each fill native_execution_consumer.cpp:6919, native_execution_consumer.cpp:6919-6949; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5606-5640, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | -| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:866 fires per accepted realtime print native_execution_consumer.cpp:7147-7151; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | -| 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:7517-7634 **Closed:** `EveryModeledPoint` recalculates at each intrabar sample, and `on_native_sub_bar` native_host.hpp:916 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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-402, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | +| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7506-7510 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16177) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7063-7067; `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:7010 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7061 (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:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5632, `flush_coof_tail` pine_adapter.cpp:16320, `end_coof_recalc` pine_adapter.cpp:5653) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6917, native_execution_consumer.cpp:6896-6926; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5612-5646, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7145-7149; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | +| 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:7500-7619 **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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:234-320) 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:5779), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1659-1666 | 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:5779), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1660-1667 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1484-1487, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1392-1548; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1485 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:485), or `declare_timeframe_subscriptions` native_host.hpp:1124 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:873 and `native_series_bar(index)` native_host.hpp:1107 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | -| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI `strategy_set_native_security_feed` c_abi.cpp:839) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1429-1445 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1517; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1447-1452 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:487 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8275 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:46-56). | -| HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:637) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:916 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | -| HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:21-220, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:241-413; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:514) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. | +| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1484-1487, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1392-1548; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1485 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:486), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | +| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI `strategy_set_native_security_feed` c_abi.cpp:839) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1429-1445 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1517; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1447-1452 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:488 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8292 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:46-56). | +| HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:637) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:928 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | +| HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:21-220, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:241-413; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:515) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:255-336) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | -| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:863; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7876-7893, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7883, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | +| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:875; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7872-7889, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7879, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | ### 1.7 Fill price, slippage, commission, FX | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:5685-5732, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6268-6276; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:583 applied once native_execution_consumer.cpp:4598-4599, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12032-12041) is A | +| FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:5686-5733, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6269-6277; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:584 applied once native_execution_consumer.cpp:4599-4600, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12032-12041) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:472-479) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11994-12003, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4052; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6345-6346; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11994-12003, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4061; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6351-6352; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:1190 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:8705). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | +| FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1202 in batch; the stream then refused a non-empty curve (today only tick-driven input is refused under one, `refuse_fx_curve_tick_input` native_execution_consumer.cpp:8687). **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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9106-9111, `exit_limit_trigger` pine_adapter.cpp:9640-9661), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11757) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:2613-2626, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1009-1013 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). TV entry-bar masks are A (`sample_open_trade_extremes` pine_adapter.hpp:1226-1233). **Measured against TradingView (R5 lane H-MEASURE, `tests/test_e19_excursion_tape.cpp`):** the host-owned model is `ab9714be`'s, not TradingView's — where it and the kernel sampler differ the kernel's number is TradingView's (entry-bar over-mask, exit-bar over-fold after an at-open exit, a `calc_on_order_fills` scratch TradingView reports as 0 / 0); both miss a bar's open print ahead of a same-bar stop fill. On the corpus the two models differ on 341 of 431 390 trades, and of the 326 of those a TradingView export identifies, the kernel's number is TradingView's on 315 and the host's on none. Recorded divergences, retained for `ab9714be` compatibility. | +| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:2613-2626, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1021-1025 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). TV entry-bar masks are A (`sample_open_trade_extremes` pine_adapter.hpp:1226-1233). **Measured against TradingView (R5 lane H-MEASURE, `tests/test_e19_excursion_tape.cpp`):** the host-owned model is `ab9714be`'s, not TradingView's — where it and the kernel sampler differ the kernel's number is TradingView's (entry-bar over-mask, exit-bar over-fold after an at-open exit, a `calc_on_order_fills` scratch TradingView reports as 0 / 0); both miss a bar's open print ahead of a same-bar stop fill. On the corpus the two models differ on 341 of 431 390 trades, and of the 326 of those a TradingView export identifies, the kernel's number is TradingView's on 315 and the host's on none. Recorded divergences, retained for `ab9714be` compatibility. | | RP2 | Closed-trade list, trade statistics, report struct | **yes** — `fill_report` engine_report.cpp:39-66, `get_trade` engine.hpp:1791-1828, `compute_trade_stats` engine_metrics.cpp:138 | K | — | F:H2 O:I3 S:X2 | — | | RP3 | Equity curve | **no** — `update_equity_extremes` engine.hpp:1362 / `record_equity_point` engine.hpp:1380 are protected and were called only by the adapter **Closed:** `NativeReportPolicy::KernelRecorded` native_run_spec.hpp:63 has the consumer fold the extremes and append one point per script calculation, labelled with the script interval's open; `KernelRecordedAtHostMarks` records the same series at the host's own marks (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I4 S:X2 | S's "partial" folds RP2 in | | RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:219; `fill_metrics_section` engine_report.cpp:119-139 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | | RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1684) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1409) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | -| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9279, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | -| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1358, native_execution_consumer.cpp:10681; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | +| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9252, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | +| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1370, native_execution_consumer.cpp:10638; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | | RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:538; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1931 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:411 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:405 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:411 is its deprecated spelling, which the generic default forwards to (`tests/test_native_host_hash_extension.cpp`). | K | L2 (the virtual rides the shared bump) | F:I3 O:J1 S:X3 | — | @@ -171,12 +171,12 @@ 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:1856, refused in-run by `guard_native_mutation` engine.hpp:1860) | **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:2255-2256, `chart_timezone` pine_adapter.cpp:2296-2298) | **yes** — spec fields `ticker` native_run_spec.hpp:567-577; `parse_session` native_calendar.hpp:173-419; 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:2255-2256, `chart_timezone` pine_adapter.cpp:2296-2298) | **yes** — spec fields `ticker` native_run_spec.hpp:568-578; `parse_session` native_calendar.hpp:173-419; 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:2158, `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:896, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:940, probe `strategy_execution_contract` c_abi.cpp:873, stream `strategy_stream_begin` c_abi.cpp:624 **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:548 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:907 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 58 runtime exports (and eight generated per-strategy exports) (`tests/test_native_c_api.c`, `tests/test_native_c_api_frozen_header.cpp`). | K | L13 | F:J6 O:J5 | R5-7: post-v1. S covers it in its §2.3 without a row. | -| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1454 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7267), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1765 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:857; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | +| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1461 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7267), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1765 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:857; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | | OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:288-290) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | --- @@ -202,17 +202,17 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | # | Item | Where / what it is used for | Class | Plan | Src | |---|---|---|---|---|---| -| a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:82-91) and of the rich `BacktestEngine::run` overload (engine.hpp:1780-1785, engine_consumer.cpp:81-93). The kernel only forwards it as `NativeBeginArgs::overrides_opaque` native_host.hpp:746; the source host casts it back `overrides_opaque` pine_strategy_host.cpp:292-295 | 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) | F O S | +| a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:82-91) and of the rich `BacktestEngine::run` overload (engine.hpp:1780-1785, 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:292-295 | 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) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:29 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:356-359 (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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8766`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2309-2310. 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:260-261. **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:741) | 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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8748`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2309-2310. 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:260-261. **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:741) | 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:454-460) | 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:6420 | NEUTRAL | Reuse as the reference arithmetic of the price-grid and sizing lanes, delete the rest | O (single-source) | +| e | Dead helpers in the public kernel header | **Done.** All four are gone from `engine.hpp`: `round_to_mintick_directional` and `apply_slippage` were the pre-R5 spelling of the directional snap and the slippage step and the kernel now fills on `native_matching::grid_round_directional` / `native_matching::apply_slippage` src/native_matching.hpp:259 (the engine.hpp comment that named them went with R5 lane E10's residue sweep); `apply_limit_fill` and `reserve_percent_commission` are deleted outright, and only the adapter still spells the first two names, in its own comments. `round_to_mintick` engine.hpp:753 and `bar_fill_price` engine.hpp:771 stay, live through the call `bar_fill_price` native_execution_consumer.cpp:6422 | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19134-19163 | 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:19162-19191 | 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:523, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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:603. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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 | +| k | `margin_call_enabled_` | **Done.** The generic kernel model is enabled by `NativeRunSpec::margin` native_run_spec.hpp:604. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1274 | 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) | | m | `request.security` machinery | `register_security_eval` engine_security.cpp:27, `set_native_security_feed` engine_aux_security.cpp:76-127, `SecurityEvalState` engine.hpp:1156-1360 carry Pine semantics (lookahead, gaps, Heikin-Ashi, KI-55 range start) and are driven only by the source layer | HASH-VISIBLE / measured path | F planned to move both TUs and the state into the source host after native HTF. **R5-6 narrows this:** L6 reuses the feed store and routing, so those stay kernel; only the Pine-only evaluator semantics remain candidates. Do last. **Status (lane L12b): done** — the evaluator semantics are `source::PineSecurityEvalState` + `src/source/pine_security_eval.cpp`; the kernel keeps the registry, aggregator, feed store and one generic step | F S | | n | S-only proposals, not adopted | A generic `FillModel` interface with a TV model installed by the adapter (for the TradingView money / tick / path comment blocks in `engine.hpp` and the dead helpers of item e); "emit generic FIFO facts and let an adapter policy produce the KI-62 scratch" | — | Superseded by R5-3 (price-grid lane) and R5-1 (TV fill specifics stay behind `resolve_execution_terms`); the dead-helper part is item e; there is no scratch code left to re-lower (item d) | S | @@ -281,8 +281,8 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the | | F (its §2.iv A) | O (its G10) | S (its P7) | Chosen | |---|---|---|---|---| | API shape | spec `subscriptions` + push callback `on_native_timeframe_bar` | `register_native_series` + pull `native_series_bar(id)` / `native_series_slot_is_new(id)` | spec `security_requests` + `configure_native_security_feed` + `native_security(id)` view | F's subscription in the spec + push callback, plus O's pull accessor | -| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:656-671, `contribute_input` native_execution_consumer.cpp:8012, `seal_script` native_execution_consumer.cpp:7977) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | -| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7097), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | +| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:656-671, `contribute_input` native_execution_consumer.cpp:7996, `seal_script` native_execution_consumer.cpp:7973) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | +| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7095), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | | Authoritative bars | `authoritative_bars` on the subscription | `Source::ExchangeFeed` through `set_native_security_feed` | `configure_native_security_feed(id, bars, tf)` | F's field, installed through the existing store | | Lookahead | none, by construction | `lookahead = false` default, available | `lookahead_on = false`, `gaps_on = true` | open → §4 U1 | @@ -298,7 +298,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2956-3024) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3025-3093) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:45, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -324,12 +324,12 @@ 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:1730, `guard_native_mutation` engine_aux_security.cpp:78 | 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). No kernel TU includes a source header | `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13 | 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:739 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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549; the ledger `close_logical_units_` pine_adapter.hpp:2016-2027; `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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549; the ledger `close_logical_units_` pine_adapter.hpp:2027-2038; `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) | 58 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 58 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:13933f70987f6bfe37d3015ae065a8d5ee5937c37431d48e2ecd1f17a4c29958` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:862, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:590-709, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:515-567, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:494-511, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:802-833, c_abi.cpp:929 | F O S | | 9 | "No examples. `PINEFORGE_BUILD_EXAMPLES` is 'none yet'" (ADR L139) | The option is inert, but two Pine-free `NativeStrategyHost` examples exist, are compiled by the independence checker and tested through the live runner, only under `PINEFORGE_BUILD_LIVE_RUNNER`. Wording: "no top-level examples target" | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | F O S | | 10 | Follow-ups (1)-(4) (ADR L168-170) | Add native reporting, sizing, margin, HTF — and, per R5, calc-timing, price grid and order ergonomics. Without them "usable without Pine" is a build-system claim, not a feature claim | §3.5 | F | | 11 | Build layout | One `pineforge` library always appends the source list; a kernel-only target does not exist yet; `compat/pine/market_admission.cpp` sits in the main list, a boundary bug. **Overtaken:** `PINEFORGE_KERNEL_SOURCES` CMakeLists.txt:113 and `add_library` CMakeLists.txt:153 build `PineForge::kernel`, and the compat unit is in `PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91 | `add_library` CMakeLists.txt:153 | S (F, O agree in §2.i) | -| 12 | Host constructors | `NativeStrategyHost` is a zero-argument C++ host with the callbacks and the request API; `PineStrategyHost` is the class that takes `CapAttachment` — do not attribute it to the native constructor | `NativeStrategyHost` native_host.hpp:833, `PineStrategyHost` pine_strategy_host.hpp:242 | S (single-source) | +| 12 | Host constructors | `NativeStrategyHost` is a zero-argument C++ host with the callbacks and the request API; `PineStrategyHost` is the class that takes `CapAttachment` — do not attribute it to the native constructor | `NativeStrategyHost` native_host.hpp:845, `PineStrategyHost` pine_strategy_host.hpp:242 | S (single-source) | `docs/pages/native-engine.md` fixes, all applied (F unless noted). The page is cited by section rather than by line, because a reference page's line numbers move with every edit and @@ -337,12 +337,12 @@ the campaign's own anchor audit found that the first thing to rot: | Where | Said | Tree says | |---|---|---| -| the page's opening, and its "close-only" claims | callbacks are close-only | The kernel has a bar-open hook (`on_native_bar_open` native_host.hpp:877), a tick hook (`on_native_tick` native_host.hpp:866), a post-fill hook (`on_native_applied` native_host.hpp:926), a recalculation hook (`on_native_recalculate` native_host.hpp:901) and a sub-bar hook (`on_native_sub_bar` native_host.hpp:916). The page's opening now tables all eleven, and `on_native_applied` is documented as the calculate-on-fill hook with suffix eligibility | +| the page's opening, and its "close-only" claims | callbacks are close-only | The kernel has a bar-open hook (`on_native_bar_open` native_host.hpp:889), a tick hook (`on_native_tick` native_host.hpp:878), a post-fill hook (`on_native_applied` native_host.hpp:938), a recalculation hook (`on_native_recalculate` native_host.hpp:913) and a sub-bar hook (`on_native_sub_bar` native_host.hpp:928). The page's opening now tables all eleven, and `on_native_applied` is documented as the calculate-on-fill hook with suffix eligibility | | the request-epoch sentence | `native_order_v4` | `native_order_v7` native_order.hpp:25 | | the two host-epoch sentences | host epoch v16 | `engine_script_run_v19` native_host.hpp:20 | | "What still requires Pine compatibility to build" | `BacktestEngine` still holds `CapAttachment` / `OrderPriority` / `IntradayCap` | `engine.hpp` has zero references to them; the cap type is `CapAttachment` intraday_cap.hpp:18 and the constructor is `PineStrategyHost`'s pine_strategy_host.hpp:244 | -| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9149-9189 shows no such refusal (still unpinned by a test; the page says so) | -| added | — | the self-aggregation recipe beside the subscriptions it is an alternative to; the open-bar lookahead warning with `current_partial_bar` native_host.hpp:1061; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | +| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9122-9162 shows no such refusal (still unpinned by a test; the page says so) | +| added | — | the self-aggregation recipe beside the subscriptions it is an alternative to; the open-bar lookahead warning with `current_partial_bar` native_host.hpp:1073; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -411,7 +411,7 @@ closure markers and `scripts/check_design_inventory.py`). | L2 | `NativeReportPolicy { HostRecorded, KernelRecorded }` (R1 appended a third value, `KernelRecordedAtHostMarks` native_run_spec.hpp:64), `report_open_position_at_end`, `hash_host_extension`, `native_metrics()` → A.1 | at L2 the adapter stayed `HostRecorded`; since R1 it declares `KernelRecordedAtHostMarks` pine_adapter.cpp:2358, 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:5611-5615) and `drain_applied_notifications` (native_execution_consumer.cpp:6272-6285) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | +| L5 | `NativeCalculationTrigger`, `on_native_recalculate(…, reason, cause)`, `on_native_sub_bar`, `current_partial_bar()`, `NativeOpenBarView` → A.4 | the adapter keeps `BarClose` + its own cascade; `born_on_remaining_path` (native_execution_consumer.cpp:5617-5621) and `drain_applied_notifications` (native_execution_consumer.cpp:6278-6291) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | | L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1305-1389 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | | L7 | `native_working_requests()`, `cancel_all()`, `cancel_where(label)`, `TriggerAnchor`, `TrailTicks`, zero offset, `ReplaceOptions`; toolkit `submit_bracket`, `OrderBook` → A.6 | new kinds unused by the adapter; anchor defaults to `Absolute`; the adapter's `tick * 0.5` sentinel keeps working | bracket + trailing probe through the builder: exact where TR5 / FP7 are inactive (on-grid levels) (F); the zero-offset trail test asserts the run no longer fails (O); parent-limit + stop sibling, parent rejection, replacement, parent-cycle revival (S) | | L8 | `NativePriceGrid { None, QuantizeFills, QuantizeFillsAndTriggers }`, `NativeGridRounding` → A.7 | `None` for the adapter, permanently — **native-only by ruling, audit lane P6: §3.6.2** | a host with `QuantizeFills` books on-grid prices; a second test pins that `QuantizeFillsAndTriggers` still differs from TV's half-tick threshold (FP5) (O) | @@ -444,7 +444,7 @@ Flagged: | Tier | Lanes | Why | |---|---|---| -| Day-one blockers (O) | L1, L2, L3, L10 | Without L2 `fill_report` silently returns an empty curve and degenerate equity metrics and drops an open final position. Without L3 every non-unit order needs a hand-written `resolve_execution_terms`, and a mistake is a run-terminating `TermsUnresolved` (native_execution_consumer.cpp:4963-4965). Without L1 "kernel" is a claim, not an artifact. L10 is the minimum buildable proof. | +| Day-one blockers (O) | L1, L2, L3, L10 | Without L2 `fill_report` silently returns an empty curve and degenerate equity metrics and drops an open final position. Without L3 every non-unit order needs a hand-written `resolve_execution_terms`, and a mistake is a run-terminating `TermsUnresolved` (native_execution_consumer.cpp:4964-4966). Without L1 "kernel" is a claim, not an artifact. L10 is the minimum buildable proof. | | Required by the promise (R5-11) | L0, L4, L5, L6, L7, L8 | A kernel that cannot liquidate is not a broker model (O); bracket / trailing corner cases are hit immediately (O); calc-timing and HTF are Pine features the user wants natively (R5-5, R5-6); the price grid is generic and missing (R5-3). | | Wanted with v1, not blocking | L11 a, d | removes the last `source::` name from kernel signatures and the orphaned TV commentary (F) | | In scope, 1.x | L9 | R5-9; a toolkit stand-in is acceptable meanwhile (F) | @@ -504,7 +504,7 @@ projected the spec (pine_strategy_host.cpp:245) and `configure_native` has folded it into the continuation digest. Measured: an env-gated probe printing the adapter's risk configuration inside `project()` on the corpus reads `max_drawdown=0 cap.active=0` for every risk probe. `NativeRunSpec::risk` is a -begin-time declaration (`native_run_spec.hpp:608-615,504`, folded at +begin-time declaration (`native_run_spec.hpp:609-616,504`, folded at native_execution_consumer.cpp:236-237); a statement-time limit has nowhere to land. Routing any rule needs either the transpiler to hoist constant risk statements into the constructor (out of this repository) or a kernel @@ -523,9 +523,9 @@ line numbers are this tree's (683a82f); kernel semantics are |---|---|---|---|---|---| | `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2362); 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:13257-13258; 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:16805, `update_risk_state` pine_adapter.cpp:13898-13925): 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:13250), 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_drawdown` | `update_risk_state(bar.close)` once per script bar at the close mark (`update_risk_state(bar.close)` pine_adapter.cpp:16833, `update_risk_state` pine_adapter.cpp:13898-13925): 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:13250), 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:1200-1208): +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:17724-17733); 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:16308); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16420), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14983-14992, `submit_intraday_loss_close` pine_adapter.cpp:14953-14981), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17162); 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:5983, `:7652`, `:8376`, `:8504`, `:11481`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18061-18064) | `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_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16336); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16448), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15011-15020, `submit_intraday_loss_close` pine_adapter.cpp:14981-15009), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17190); 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:5983, `:7652`, `:8376`, `:8504`, `:11481`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18089-18092) | `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:13889-13891); 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:13795-13816, `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 @@ -759,7 +759,7 @@ both lanes and is not in the tree; its counts above are N13's, not re-run here. **What landed for the ruling.** Until P6 no example exercised the grid (the second audit's §5: "lanes with no example: price grid"), and the C spelling -(`PF_NATIVE_SPEC_EXT_PRICE_GRID`, translated at native_c_host.cpp:3159-3163) +(`PF_NATIVE_SPEC_EXT_PRICE_GRID`, translated at native_c_host.cpp:3162-3166) had no executed consumer anywhere in the repository — no C test and no C example set the bit. `examples/native/native_price_grid_strategy.cpp` runs one strategy over one sub-tick tape (a 0.25 ladder under a composite feed) in the @@ -1299,8 +1299,8 @@ unchanged after it. | E1 | **Spec hashing.** `hash_spec` folds every field unconditionally (native_execution_consumer.cpp:234-256) | 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:1620); 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5779) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6277-6290); 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:16149-16178) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5779) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6278-6291); 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:16177-16206) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | @@ -1345,7 +1345,7 @@ unchanged after it. | **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:16314-16330) 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:2646) 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:16342-16358) 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:2646) 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. | @@ -1367,11 +1367,11 @@ virtual void hash_host_extension(BrokerStateHashSink&) const {} // F (S: hash_ NativeMetricsView native_metrics() const; // S: net_profit, equity, closed_trades, open_lots; non-virtual ``` -- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` + `record_equity_point(script_open_ms)` (engine.hpp:1380, engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7191, native_execution_consumer.cpp:7191, native_execution_consumer.cpp:7191) and after the `AfterCalculation` close; appends the per-bar broker hash when recording is on; synthesizes the open-position row at run end / `stream_end` with the existing `build_close_trade_with_costs` (engine_orders.cpp:70). +- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` + `record_equity_point(script_open_ms)` (engine.hpp:1380, engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7189, native_execution_consumer.cpp:7189, native_execution_consumer.cpp:7189) 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:2358), 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:1011). Same semantics, but it costs a host-epoch bump, so the spec form was chosen; the virtual was never added, and `NativeReportPolicy` is what landed (§3.4 row 3). -- TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5055-5097), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. +- Alternative: F's virtual `owns_equity_recording()` (the A48 ownership pattern of `owns_lot_excursions` native_host.hpp:1023). Same semantics, but it costs a host-epoch bump, so the spec form was chosen; the virtual was never added, and `NativeReportPolicy` is what landed (§3.4 row 3). +- TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5061-5103), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. ### A.2 L3 — sizing, fractional reduce, sibling reservation (F P1, P7 + O G2 + S P2, P3) @@ -1423,7 +1423,7 @@ virtual void on_native_margin_call(const native_order::MarginCallEvent&) {} - 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:13223-13244). 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:2575), 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:4654-4667 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). +- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:4663-4676 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). ### A.4 L5 — calculation timing (F P4 + O G8, G9 + S P6) @@ -1443,9 +1443,9 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1635-1636) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6981-6993) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). -2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5616-5620) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1659-1666) is calibrated against it. -3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6834-6841). `on_native_tick` stays the observation hook (S). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1692-1693) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6979-6991) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5617-5621) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1660-1667) is calibrated against it. +3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6836-6843). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2553); valid in bar-open, applied and tick callbacks. 5. The kernel never attempts language-state rollback; that stays Pine's (`snapshot / restore / commit_coof_script_state`, pine_scheduler_native.cpp:52-104) (O). @@ -1512,7 +1512,7 @@ NativeGridRounding grid_rounding = NativeGridRounding::HalfUp; ``` - `QuantizeFills`: the kernel rounds `resolved_price` onto `price_tick` **before** slippage (`HalfUp`) or in the direction that favours the resting order (`Directional`). `QuantizeFillsAndTriggers`: the matcher additionally compares the quantized path against the raw level. -- TUs: `native_run_spec.cpp` (validation), `native_execution_consumer.cpp` (resolve path native_execution_consumer.cpp:3949-3971, `execute_current` path native_execution_consumer.cpp:3769-3779), `native_matching.hpp` (threshold form). The dead helpers were the reference arithmetic and are deleted (§2.ii e); the kernel's own step is `native_matching::apply_slippage` native_matching.hpp:259. +- TUs: `native_run_spec.cpp` (validation), `native_execution_consumer.cpp` (resolve path native_execution_consumer.cpp:3950-3972, `execute_current` path native_execution_consumer.cpp:3770-3780), `native_matching.hpp` (threshold form). The dead helpers were the reference arithmetic and are deleted (§2.ii e); the kernel's own step is `native_matching::apply_slippage` native_matching.hpp:259. ### A.8 L9 — risk limits (F P3 + O G7 + S P5) @@ -1529,10 +1529,10 @@ struct NativeRiskLimits { 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:1317) (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:373), not a chart clock. +- 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:1329) (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:373), 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:13721-13819); 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:4654-4667 and the per-point walk), `native_order.hpp`, the state hash. +- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:4663-4676 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 7238f907..0c48804c 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -251,7 +251,7 @@ CMakeLists.txt:43 off. policy layer that *uses* kernel features. Where every TradingView rule lives. **front door** — one of the three ways in: PineScript through codegen, C++ -through `NativeStrategyHost` native_host.hpp:833, or C through the +through `NativeStrategyHost` native_host.hpp:845, or C through the `strategy_native_*` surface (`strategy_native_host_create_v1` native_c_api.h:2598). **twin** — a test unit compiled twice, once against a frozen historical header diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 17f4693c..b06d9fe6 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -13,40 +13,40 @@ then `run` or `stream_*`. Submit from native begin/bar callbacks, or between realtime inputs on the same thread. Do not override the inherited `on_bar` (it is `final` and refused). Do not write protected engine fields. -**The callbacks.** Only `on_native_bar` (`native_host.hpp:881`) is +**The callbacks.** Only `on_native_bar` (`native_host.hpp:893`) is pure-virtual: it is the script-bar calculation, and a host that overrides nothing else is a complete, correct host — every other callback below has a default that is the established behaviour. The surface is **not** close-only. | callback | when | section | |---|---|---| -| `prepare_native_begin` (`native_host.hpp:853`) | once, with the begin's own arguments, before the run starts | *Lifecycle and run identity* | -| `on_native_run_begin` (`native_host.hpp:860`) | once, after the reset, before any bar; the one place `declare_timeframe_subscriptions` and `declare_auxiliary_feed` are legal | *Higher-timeframe series for native hosts* | -| `on_native_input` (`native_host.hpp:863`) | once per accepted confirmed input, before it is aggregated or matched | *Calendar, session, timeframes, warmup* | -| `on_native_tick` (`native_host.hpp:866`) | once per accepted realtime print, before it is matched | *Batch OHLCV vs ticks vs quiet* | -| `on_native_timeframe_bar` (`native_host.hpp:873`) | once per delivered bucket of a declared series | *Higher-timeframe series for native hosts* | -| `on_native_bar_open` (`native_host.hpp:877`) | at the modeled opening, before that point's matching pass | *Native requests* (and its lookahead warning) | -| `on_native_bar` (`native_host.hpp:881`) | the script bar's own calculation | *Calculation timing* | -| `on_native_recalculate` (`native_host.hpp:901`) | every calculation of the run, tagged with its reason; the default forwards to `on_native_bar` | *Calculation timing* | -| `on_native_sub_bar` (`native_host.hpp:916`) | after each retained lower-timeframe sub-bar's whole path | *Sub-bars* | -| `on_native_applied` (`native_host.hpp:926`) | after each applied execution — the calculate-on-fill point | *Native requests* | -| `on_native_margin_call` (`native_host.hpp:989`) | right after the `on_native_applied` of a kernel liquidation's own fill | *Margin and liquidation* | +| `prepare_native_begin` (`native_host.hpp:865`) | once, with the begin's own arguments, before the run starts | *Lifecycle and run identity* | +| `on_native_run_begin` (`native_host.hpp:872`) | once, after the reset, before any bar; the one place `declare_timeframe_subscriptions` and `declare_auxiliary_feed` are legal | *Higher-timeframe series for native hosts* | +| `on_native_input` (`native_host.hpp:875`) | once per accepted confirmed input, before it is aggregated or matched | *Calendar, session, timeframes, warmup* | +| `on_native_tick` (`native_host.hpp:878`) | once per accepted realtime print, before it is matched | *Batch OHLCV vs ticks vs quiet* | +| `on_native_timeframe_bar` (`native_host.hpp:885`) | once per delivered bucket of a declared series | *Higher-timeframe series for native hosts* | +| `on_native_bar_open` (`native_host.hpp:889`) | at the modeled opening, before that point's matching pass | *Native requests* (and its lookahead warning) | +| `on_native_bar` (`native_host.hpp:893`) | the script bar's own calculation | *Calculation timing* | +| `on_native_recalculate` (`native_host.hpp:913`) | every calculation of the run, tagged with its reason; the default forwards to `on_native_bar` | *Calculation timing* | +| `on_native_sub_bar` (`native_host.hpp:928`) | after each retained lower-timeframe sub-bar's whole path | *Sub-bars* | +| `on_native_applied` (`native_host.hpp:938`) | after each applied execution — the calculate-on-fill point | *Native requests* | +| `on_native_margin_call` (`native_host.hpp:1001`) | right after the `on_native_applied` of a kernel liquidation's own fill | *Margin and liquidation* | **The answering hooks** — each is consulted, and each has a default that is -the kernel's own answer: `resolve_execution_terms` (`native_host.hpp:935`), -`validate_execution_precommit` (`native_host.hpp:948`), -`resolve_margin_requirement` (`native_host.hpp:964`), `margin_check_allowed` -(`native_host.hpp:976`), `resolve_margin_call_units` (`native_host.hpp:983`), -`resolve_anchored_level` (`native_host.hpp:1000`), `owns_lot_excursions` / -`closed_lot_excursion` (`native_host.hpp:1018`) and the hash seam +the kernel's own answer: `resolve_execution_terms` (`native_host.hpp:947`), +`validate_execution_precommit` (`native_host.hpp:960`), +`resolve_margin_requirement` (`native_host.hpp:976`), `margin_check_allowed` +(`native_host.hpp:988`), `resolve_margin_call_units` (`native_host.hpp:995`), +`resolve_anchored_level` (`native_host.hpp:1012`), `owns_lot_excursions` / +`closed_lot_excursion` (`native_host.hpp:1030`) and the hash seam `hash_host_extension`. Each is documented beside the feature it shapes. **Declaring a hook the host does not have.** Two of those defaults still cost the kernel work at every bar or fill. A host whose `on_native_bar_open` does nothing says so with `declare_native_bar_open_hook(false)` -(`native_host.hpp:1035`), and one that keeps the default +(`native_host.hpp:1047`), and one that keeps the default `validate_execution_precommit` with `declare_native_precommit_hook(false)` -(`native_host.hpp:1043`). The kernel then makes no bar-open call, and neither +(`native_host.hpp:1055`). The kernel then makes no bar-open call, and neither consults the precommit hook nor builds the settlement preview it would have been shown, unless the host owns lot excursions, which that preview's closing rows consult. It keeps every effect of its own that the skipped call's boundary has, @@ -274,8 +274,9 @@ script bar: - `IntrabarPath::lower_tf{bars, tf, samples, distribution, volume_weighted, volume_weighted_min_samples, volume_weighted_max_samples, sample_eligibility}` — a **retained** finer feed. It is what makes - `on_native_sub_bar` reachable, and what gives the margin model a sample to - re-evaluate at instead of a whole-bar waypoint. + `on_native_sub_bar` reachable, and, matched as continuous segments, what + gives the margin model a sample to re-evaluate at instead of a whole-bar + waypoint (`NativeMarginCheckKind::IntrabarSample`, below). `IntrabarPath::SampleEligibility::ContinuousSegments` (default) keeps continuous matching between generated samples; `DistributionSamples` restricts eligibility to the sample points themselves. @@ -292,12 +293,12 @@ the sub-bar sections of `tests/test_native_calc_timing.cpp`. ### Validating a spec -`validate_native_run_spec(spec)` (`native_run_spec.hpp:819`) answers a -`NativeRunSpecValidation` (`native_run_spec.hpp:802`): a -`NativeRunSpecError` (`native_run_spec.hpp:691`) and the -`NativeRunSpecField` (`native_run_spec.hpp:658`) it first failed on, with +`validate_native_run_spec(spec)` (`native_run_spec.hpp:820`) answers a +`NativeRunSpecValidation` (`native_run_spec.hpp:803`): a +`NativeRunSpecError` (`native_run_spec.hpp:692`) and the +`NativeRunSpecField` (`native_run_spec.hpp:659`) it first failed on, with `ok()` and an explicit `operator bool`. `normalize_native_run_spec(spec)` -(`native_run_spec.hpp:829`) validates and rewrites the one admitted literal — +(`native_run_spec.hpp:830`) validates and rewrites the one admitted literal — a numeric `-0` fee becomes `+0` — leaving every other literal alone. Neither allocates on the failure path, neither changes a spec it rejects, and the field order is deterministic, so a host can report "which field" rather than @@ -315,7 +316,7 @@ field order is deterministic, so a host can report "which field" rather than valid and clear the curve. `validate_native_fx_curve` (`native_fx_curve.hpp:48`) is the same judgement as a pure query. -`native_run_spec_digest(spec)` (`native_run_spec.hpp:905`) is the portable +`native_run_spec_digest(spec)` (`native_run_spec.hpp:906`) is the portable constant described under *Lifecycle and run identity*: exactly the fields the consumer folds into a run's continuation identity, and nothing else. Each feature suite pins the refusals of the fields it owns — the eighteen @@ -462,11 +463,11 @@ spec fields. The rich `run(bars, n, input_tf, script_tf, inputs, syminfo, overrides, …)` overload (`engine.hpp:1623-1634`) is **not** refused as a source mutation: it reaches `NativeExecutionConsumer::run_rich` -(`native_execution_consumer.cpp:9149-9189`), which admits the begin, checks the +(`native_execution_consumer.cpp:9122-9162`), which admits the begin, checks the timeframe arguments against the spec, preflights and pumps the batch exactly like the plain overload. `inputs` / `syminfo` / `overrides` are carried only as `NativeBeginArgs` fields to `prepare_native_begin` — the overrides as the -opaque `overrides_opaque` (`native_host.hpp:746`), which the kernel forwards +opaque `overrides_opaque` (`native_host.hpp:758`), which the kernel forwards and never dereferences. No test pins either behaviour; prefer the plain overload. What *is* refused is source **mutation** through the setters (below). @@ -490,7 +491,7 @@ Serialized external C++ calls may command only **between realtime inputs**, never reentrantly during input processing. A host written in C issues the same five commands through `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` -(`native_c_api.h:2729-2779`), under the same legality rule; see *Driving the +(`native_c_api.h:2737-2787`), under the same legality rule; see *Driving the kernel from C* below. `native_order::Request` values belong to `native_order_v7` @@ -630,17 +631,17 @@ a host reacts to its own execution and may submit again. A request born there, mid-bar on a continuous segment, is eligible on the **remaining path suffix** of that segment — the birth is admitted at the current cursor and the geometric search then sees only the unconsumed suffix (`born_on_remaining_path`, -`native_execution_consumer.cpp:5611-5615`). Requests accepted before the +`native_execution_consumer.cpp:5617-5621`). Requests accepted before the segment, and discrete points, keep the ordinary birth gate above. `on_native_bar_open` fires at the modeled opening, before that point's matching -pass (`native_execution_consumer.cpp:7065-7067`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:7063-7065`). **Lookahead warning:** the `Bar` it receives is the *complete* script bar — the consumer has already set -`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6957`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6955`), the complete bar unless the spec asks for `NativeOpenBarView::OpenOnly` — so its high, low and close are the finished bar's, not what is known at the open. A host that must decide on open-only information reads -`current_partial_bar()` (`native_host.hpp:1061`; C: +`current_partial_bar()` (`native_host.hpp:1073`; C: `strategy_native_partial_bar_v1`), the lookahead-free bar so far (at the bar open, the open alone), or declares `NativeOpenBarView::OpenOnly`, which masks this one callback's bar down to its open. Both are under *The bar so far, and the @@ -1291,7 +1292,19 @@ mark breaches, the kernel rests its own `Reduce` (or `Flatten`) with `Stop{L}`, bound to the live book. The reduction therefore *fills at the liquidation level*, where the account actually runs out of margin, while it is *sized at* the adverse mark. With a declared `IntrabarPath` there is no -whole-bar waypoint model: the kernel re-evaluates at each delivered sample. +whole-bar waypoint model. On a path matched as continuous segments (a +`lower_tf` path under `ContinuousSegments`) the kernel re-evaluates at each +delivered sample: the bar's first sample is its `BarOpen` point, and every +later one is an `IntrabarSample` point, measured at that sample's own price +immediately before the sample is matched, so a breach inside the bar rests its +reduction at the sample that crossed, and the path reaches it there +(`tests/test_native_margin_intrabar_samples.cpp`). A one-price distribution +path (`synthesized`, or `DistributionSamples`) is checked at its first sample +and after fills only: a request armed at a discrete point is matched only at a +later one, so a reduction re-armed at every sample would never be. The same +money under the magnifier on TradingView books its call at the first +lower-timeframe low that crosses, sized there +(`tests/fixtures/intrabar_margin`). `basis` chooses the equity side of that comparison, at every check point. `MarkedEquity` (the default) is `marked_equity(mark)` itself, which the open @@ -1368,6 +1381,7 @@ test, the host is offered the two numbers the kernel is about to compare: ```cpp struct NativeMarginRequirementView { NativeMarginCheckKind kind; // BarOpen | AfterApplied | Calculation | FxRoll + // | IntrabarSample NativePhysicalPosition position; double mark, equity, required; // equity on the model's basis native_order::MatchCursor cursor; @@ -1458,7 +1472,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:14100-14124`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:14100-14133`), 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 @@ -1507,7 +1521,14 @@ a `process_orders_on_close`, `calc_on_order_fills`, magnified or timestamped-FX run keeps its own route, unmeasured against TradingView, the `AfterApplied` point after an add to a carried book is still refused, and a default-percent stop entry taken at its bar's open keeps the pre-open slice's -verdict. +verdict. Under the bar magnifier the adapter admits the kernel's +`IntrabarSample` points on a leveraged book: TradingView's magnified broker +books its call at the first lower-timeframe low that crosses and again at each +later one that crosses the reduced book's line (`tests/fixtures/intrabar_margin`, +every row on 3 of 4 tapes; on the fourth TradingView books its second call a +minute after the low the check books it at, a recorded divergence). A +full-margin book and the `process_orders_on_close` and `calc_on_order_fills` +runs keep their routes there, unmeasured. ### Risk limits @@ -1879,21 +1900,21 @@ there lists what it leaves out. | `physical_position()` | `NativePhysicalPosition` `native_host.hpp:296`: `signed_units`, `average_price`, `lot_count` | `strategy_native_position_v1` | | `native_open_lots(mark)` | one `NativeOpenLot` `native_host.hpp:328` per open physical lot, oldest first, marked at `mark` | `strategy_native_open_lot_count_v1` / `_get_v1` | | `native_marked_equity(mark)` | the account's marked equity at `mark` | `strategy_native_marked_equity_v1` | -| `native_working_requests()` | one `NativeWorkingRequest` `native_host.hpp:669` per live request: its `definition`, its `remaining` and its `trigger_state` | `strategy_native_working_len_v1` / `_get_v1` (partial: part of the definition) | -| `trail_state(handle)` | `NativeTrailState` `native_host.hpp:657`: `activated`, `best_price`, `current_level`, `activation_ordinal`; `nullopt` when the handle is not a live trail | `strategy_native_trail_state_v1` | +| `native_working_requests()` | one `NativeWorkingRequest` `native_host.hpp:681` per live request: its `definition`, its `remaining` and its `trigger_state` | `strategy_native_working_len_v1` / `_get_v1` (partial: part of the definition) | +| `trail_state(handle)` | `NativeTrailState` `native_host.hpp:669`: `activated`, `best_price`, `current_level`, `activation_ordinal`; `nullopt` when the handle is not a live trail | `strategy_native_trail_state_v1` | | `native_events(after)` | `NativeMarketEvent` `native_host.hpp:370` rows: a `NativeEventKind` `native_host.hpp:360` (`Command`, `Driver`, `Account`) and exactly one of `command`, `driver` (`NativeDriverPoint` `market_driver.hpp:97`) or `account` (`NativeAccountObservation` `native_host.hpp:351`) — the rows the run's `event_retention` keeps (*What a run keeps of its events*) | `strategy_native_events_v1` (partial: each kind's documented fields) | | `native_event_window_start()` | the oldest ordinal a read can still return: every command event at or above it is retained; 1 while nothing was dropped | `strategy_native_event_window_v1` | -| `current_execution_point()` | `NativeCurrentPointView` `native_host.hpp:646`: the active callback's decision context, its price, the `NativeCurrentQuoteKind` `native_host.hpp:451` and the ordinal the quote came from; `nullopt` outside a decision point | `pf_native_decision_v1::price` / `::quote_kind` (partial: no quote origin) | -| `native_risk_state()` | `NativeRiskState` `native_host.hpp:631`: whether openings are blocked and why, the risk day, the fills counted in it, the loss-day streak, the peak equity and the day's opening equity | `strategy_native_risk_state_v1` | +| `current_execution_point()` | `NativeCurrentPointView` `native_host.hpp:658`: the active callback's decision context, its price, the `NativeCurrentQuoteKind` `native_host.hpp:451` and the ordinal the quote came from; `nullopt` outside a decision point | `pf_native_decision_v1::price` / `::quote_kind` (partial: no quote origin) | +| `native_risk_state()` | `NativeRiskState` `native_host.hpp:643`: whether openings are blocked and why, the risk day, the fills counted in it, the loss-day streak, the peak equity and the day's opening equity | `strategy_native_risk_state_v1` | | `native_liquidation_price()` | the solved level `L`, or `nullopt` | `strategy_native_liquidation_price_v1` | | `current_partial_bar()` | the lookahead-free bar so far, through the last path point consumed | `strategy_native_partial_bar_v1` | | `native_series_bar(i)` | the latest delivered bucket of subscription `i` | `strategy_native_series_bar_v1` | | `native_recalculation_count()` / `native_recalculations_skipped()` | the calculations the cadence drove and the ones its per-point bound dropped | `strategy_native_recalculations_v1` | -| `native_decision_floor()` (`native_host.hpp:1343`) | the run's monotonic decision floor in epoch ms — the same value `NativeStateView::decision_floor_ms` carries, and the lower bound every request's birth is compared against | `pf_native_state_v1::decision_floor_ms` | -| `native_consumed_high_water()` (`native_host.hpp:1348`) | the highest `run_number` this host has consumed. It lives **outside** per-run reset, so the next configure on the same host needs a strictly larger number; a fresh host reads 0 | `pf_native_state_v1::consumed_high_water` | +| `native_decision_floor()` (`native_host.hpp:1355`) | the run's monotonic decision floor in epoch ms — the same value `NativeStateView::decision_floor_ms` carries, and the lower bound every request's birth is compared against | `pf_native_state_v1::decision_floor_ms` | +| `native_consumed_high_water()` (`native_host.hpp:1360`) | the highest `run_number` this host has consumed. It lives **outside** per-run reset, so the next configure on the same host needs a strictly larger number; a fresh host reads 0 | `pf_native_state_v1::consumed_high_water` | | `native_continuation_hash()` | the consumer's continuation identity: a fold of its live state (*What the continuation and the broker-state hash fold*), the timezone folded by its content, so the same spec over the same bars and zone rules answers the same value on every host | `strategy_native_continuation_hash_v1` | | `native_sized_units(sized, price, equity, fx)` | the kernel's own `Sized` resolution as a pure query | `strategy_native_sized_units_v1` | -| `inspect_current_execution(cmd)` | `NativeCurrentExecutionPreview`, with a `NativeCurrentRefusal` `native_host.hpp:685` when the command cannot be consumed here | none in 1.0 (scheduled for 1.1.0): `strategy_native_execute_current_v1` applies the command and is no preview | +| `inspect_current_execution(cmd)` | `NativeCurrentExecutionPreview`, with a `NativeCurrentRefusal` `native_host.hpp:697` when the command cannot be consumed here | none in 1.0 (scheduled for 1.1.0): `strategy_native_execute_current_v1` applies the command and is no preview | **Reading a failure.** `native_state().failure` is a `NativeFailure` (`native_host.hpp:237`): a `NativeFailureCode`, the `NativeFailureOperation` @@ -1932,23 +1953,23 @@ bar instead, for a host whose last input is still forming; a D/W/M bar holds whole days, so all four are true. Every callback of one script bar carries the same four, fills and fill recalculations included, and a C host reads the first three from `pf_native_decision_v1`'s session bytes -(`native_c_api.h:1454`). The kernel resolves each session day once through +(`native_c_api.h:1461`). The kernel resolves each session day once through `native_calendar::session_day_at` (`native_calendar.hpp:412`), which a host may call too. `tests/test_native_session_day_facts.cpp` replays the TradingView session tapes through a bare host. It is a presentation snapshot copied onto the callback stack: writing to it cannot move the floor, the matching time or the after-calculation coordinate, and the session-day facts fold into no digest. `on_native_input` receives -`NativeInputContext` (`native_host.hpp:761`) instead — the two intervals, the +`NativeInputContext` (`native_host.hpp:773`) instead — the two intervals, the input index and whether this input completes the script interval — and -`on_native_tick` receives `NativeTickContext` (`native_host.hpp:773`), the +`on_native_tick` receives `NativeTickContext` (`native_host.hpp:785`), the decision context plus the print's sequence, where zero keeps the public `TradeTick` sentinel meaning "the provider supplied none". **Cohorts.** A cohort is a host-built roster of openings a later close binds to: `cohort_open()` answers a `native_order::CohortHandle`, `cohort_add` -(`native_host.hpp:1273`) enrolls one accepted opening's handle, -`cohort_remove` (`native_host.hpp:1276`) takes it back off, and a request with +(`native_host.hpp:1285`) enrolls one accepted opening's handle, +`cohort_remove` (`native_host.hpp:1288`) takes it back off, and a request with `owner = native_order::BindCohort{cohort}` closes what the roster holds at the match. The C spellings are `strategy_native_cohort_open_v1` / `_add_v1` / `_remove_v1`; in C the pairing is fixed — a `BindCohort` owner is reachable @@ -2053,7 +2074,7 @@ default, set while no run is active — because each row is a full the live state, not the run's length: the closed rows enter through a running digest). With the switch on, one row follows each point, after the extremes that point just folded -(`record_script_report_point`, `native_execution_consumer.cpp:7801`), so +(`record_script_report_point`, `native_execution_consumer.cpp:7799`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -2838,7 +2859,7 @@ A host that wants only the running aggregate — no series instance, no aggregates the input itself. `TimeframeAggregator` (`timeframe.hpp:288`) is public and engine-free; feed it from `on_native_input`, which runs once per accepted confirmed input bar before that bar is aggregated or matched -(`on_native_input` `native_host.hpp:863`). Include +(`on_native_input` `native_host.hpp:875`). Include ``: ```cpp @@ -2864,7 +2885,7 @@ Only completed buckets are published, so this recipe has no lookahead by construction. It is the same class the kernel's own subscription evaluator aggregates with, and the one the kernel's `script_bucket_completions` query feeds when the Pine scheduler asks how its input span buckets -(`TimeframeAggregator` `native_execution_consumer.cpp:7883`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:7879`). What it does **not** give you is what a declared subscription does: an `authoritative_bars` feed, the `gaps` and `lookahead` delivery rules, the lazy-seal chronology, a C spelling, and the @@ -2950,7 +2971,7 @@ These are existing refusals, not implied future features: A C host has the same stream and the same commands. Streaming needs no new symbol — `strategy_stream_begin` and its family (`pineforge.h:748`) take a `pf_strategy_t` from `strategy_native_host_create_v1` unchanged — and -`strategy_native_submit_v1` (`native_c_api.h:2679`) obeys the one legality +`strategy_native_submit_v1` (`native_c_api.h:2687`) obeys the one legality rule its C++ spelling does. Rebuild strategy libraries against this engine. An ABI-v4 module without the @@ -4262,7 +4283,7 @@ These are contract, not pending work. The standalone native host has no Pine decision path at runtime, and the constructor/member cut has since landed: `engine.hpp` has **zero** references to `CapAttachment`, `OrderPriority` or `IntradayCap`. `NativeStrategyHost` is -zero-argument (`native_host.hpp:833`); the `CapAttachment` constructor belongs +zero-argument (`native_host.hpp:845`); the `CapAttachment` constructor belongs to `source::PineStrategyHost` (`pine_strategy_host.hpp:241-244`), and the cap type itself lives in the adapter (`IntradayCap` `intraday_cap.hpp:83`). diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index aa7a6ad5..abcc04ea 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -60,9 +60,9 @@ books lots and settles. A native host talks to the kernel directly. What you gain: - **Your own decision model.** `on_native_bar` is an ordinary C++ member - function (native_host.hpp:881). Any data structure, any library, any + function (native_host.hpp:893). Any data structure, any library, any precomputation — no Pine type system, no series-of-everything. -- **Explicit setup.** One `NativeRunSpec` (native_run_spec.hpp:553) names the +- **Explicit setup.** One `NativeRunSpec` (native_run_spec.hpp:554) names the clock, instrument, account, fees and caps. Nothing is inferred from the bars and nothing comes from a chart. - **Typed orders and typed outcomes.** A request is a value @@ -70,9 +70,9 @@ gain: (native_order.hpp:1001), `ExecutionAppliedEvent` (native_order.hpp:1146), `MatchRejectedEvent` (native_order.hpp:1108), `CancelledEvent` (native_order.hpp:1043) — readable in order from `native_events` - (native_host.hpp:1317). + (native_host.hpp:1329). - **Any language.** The same kernel drives from C through a callback table - (`pf_native_callbacks_v1` native_c_api.h:2456), so a host in Rust, Go, Python or Zig needs no C++ + (`pf_native_callbacks_v1` native_c_api.h:2463), so a host in Rust, Go, Python or Zig needs no C++ unless it needs a capability the 1.0 C surface does not spell (the 1.0 C boundary table, @ref native_engine). - **One binary, no transpiler.** Link `libpineforge`, ship an executable or a @@ -112,32 +112,32 @@ missing summary line (examples/native/run_example.cmake:1). A Pine script declares its broker once, in the `strategy()` call, and inherits the rest from the chart. A native host declares *everything* in one -`NativeRunSpec` (native_run_spec.hpp:553) applied by `configure_native` -(native_host.hpp:1184); a C host sends `pf_native_run_spec_v1` -(pineforge.h:548) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2269) -through `strategy_configure_native_ext_v1` (native_c_api.h:3108). Setup is +`NativeRunSpec` (native_run_spec.hpp:554) applied by `configure_native` +(native_host.hpp:1196); a C host sends `pf_native_run_spec_v1` +(pineforge.h:548) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2274) +through `strategy_configure_native_ext_v1` (native_c_api.h:3120). Setup is atomic: `Ready` → begin → `Completed`, and an incomplete value is refused rather than defaulted. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy()` | `NativeRunSpec` native_run_spec.hpp:553 | `pf_native_run_spec_v1` pineforge.h:548 | `hello_kernel.cpp` / `hello_kernel_c.c` | One atomic setup value. `validate_native_run_spec` native_run_spec.hpp:819 names the offending field before a run begins. | -| `initial_capital` | `initial_capital` native_run_spec.hpp:579 | `initial_capital` pineforge.h:539 | `hello_kernel.cpp` | Required, positive. | -| `currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:537 | `hello_kernel.cpp` | With `ticker` native_run_spec.hpp:567, `tickerid` native_run_spec.hpp:568, `type` native_run_spec.hpp:569, `basecurrency` native_run_spec.hpp:571, `description` native_run_spec.hpp:572 and `volumetype` native_run_spec.hpp:573 this is Pine's whole `syminfo` block, named rather than inherited from a chart. | +| `strategy()` | `NativeRunSpec` native_run_spec.hpp:554 | `pf_native_run_spec_v1` pineforge.h:548 | `hello_kernel.cpp` / `hello_kernel_c.c` | One atomic setup value. `validate_native_run_spec` native_run_spec.hpp:820 names the offending field before a run begins. | +| `initial_capital` | `initial_capital` native_run_spec.hpp:580 | `initial_capital` pineforge.h:539 | `hello_kernel.cpp` | Required, positive. | +| `currency` | `currency` native_run_spec.hpp:571 | `currency` pineforge.h:537 | `hello_kernel.cpp` | With `ticker` native_run_spec.hpp:568, `tickerid` native_run_spec.hpp:569, `type` native_run_spec.hpp:570, `basecurrency` native_run_spec.hpp:572, `description` native_run_spec.hpp:573 and `volumetype` native_run_spec.hpp:574 this is Pine's whole `syminfo` block, named rather than inherited from a chart. | | `commission_type` | `NativeFeeKind` native_run_spec.hpp:19 | `fee_kind` pineforge.h:540 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | -| `commission_value` | `fee_value` native_run_spec.hpp:589 | `fee_value` pineforge.h:540 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | -| `slippage` | `slippage_ticks` native_run_spec.hpp:583 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3950-3951. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7523-7524, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | -| `pyramiding` | `max_open_lots` native_run_spec.hpp:595 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:2914). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). | -| `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:344 with `size_basis` native_c_api.h:2106 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | -| `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2110 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | -| `calc_on_order_fills` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2280 | `native_calc_on_fills_strategy.cpp` | `BarCloseAndFills` recalculates once at the cursor of each applied execution, bounded by `max_recalculations_per_point` native_run_spec.hpp:626. The generic cadence only: TradingView's waypoint-only refill, its two-fills-at-open rule and its script-state rollback stay in the adapter. | -| `calc_on_every_tick` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2280 | `native_calc_on_fills_strategy.cpp` | `EveryModeledPoint` additionally recalculates at every modeled point of the delivered path and at every observed print. In a stream, `on_native_tick` native_host.hpp:866 is one call per accepted realtime print whatever the trigger is. | -| `margin_long` | `NativeMarginModel::initial_long` native_run_spec.hpp:241 | `margin_initial_long` native_c_api.h:2290 | `native_margin_strategy.cpp` | A fraction, not a percent. `0.0` is the maintenance-only spelling for that side: the kernel enforces no opening requirement there and the host owns opening admission. | -| `margin_short` | `NativeMarginModel::initial_short` native_run_spec.hpp:242 | `margin_initial_short` native_c_api.h:2294 | `native_margin_strategy.cpp` | Per side, unlike the one-scalar `initial_margin_fraction` native_run_spec.hpp:597 the model replaces. `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 add the liquidation half Pine has no word for. | +| `commission_value` | `fee_value` native_run_spec.hpp:590 | `fee_value` pineforge.h:540 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | +| `slippage` | `slippage_ticks` native_run_spec.hpp:584 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3959-3960. TradingView's round-then-slip order is adapter-only. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7506-7507, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | +| `pyramiding` | `max_open_lots` native_run_spec.hpp:596 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:2914). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). | +| `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:344 with `size_basis` native_c_api.h:2111 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | +| `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2115 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | +| `calc_on_order_fills` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2285 | `native_calc_on_fills_strategy.cpp` | `BarCloseAndFills` recalculates once at the cursor of each applied execution, bounded by `max_recalculations_per_point` native_run_spec.hpp:627. The generic cadence only: TradingView's waypoint-only refill, its two-fills-at-open rule and its script-state rollback stay in the adapter. | +| `calc_on_every_tick` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2285 | `native_calc_on_fills_strategy.cpp` | `EveryModeledPoint` additionally recalculates at every modeled point of the delivered path and at every observed print. In a stream, `on_native_tick` native_host.hpp:878 is one call per accepted realtime print whatever the trigger is. | +| `margin_long` | `NativeMarginModel::initial_long` native_run_spec.hpp:241 | `margin_initial_long` native_c_api.h:2295 | `native_margin_strategy.cpp` | A fraction, not a percent. `0.0` is the maintenance-only spelling for that side: the kernel enforces no opening requirement there and the host owns opening admission. | +| `margin_short` | `NativeMarginModel::initial_short` native_run_spec.hpp:242 | `margin_initial_short` native_c_api.h:2299 | `native_margin_strategy.cpp` | Per side, unlike the one-scalar `initial_margin_fraction` native_run_spec.hpp:598 the model replaces. `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 add the liquidation half Pine has no word for. | | `close_entries_rule` | none — a native close names the lots it closes | — | `native_selected_strategy.cpp` | Pine picks FIFO or ANY for the whole script. A native reduce is explicit instead: `BindOpening` native_order.hpp:445 closes one opening's lots, `BindOpenings` native_order.hpp:454 a chosen set, a bare `Reduce` native_order.hpp:220 the book in FIFO order. The adapter keeps the run-level rule (`close_entries_rule` pine_adapter.hpp:88). | | `backtest_fill_limits_assumption` | none — a generic limit fills when the level is reached | — | `tests/test_native_order_core.cpp` | TradingView's assumption is "the price must exceed the limit by *n* ticks". The generic knob beside it is `Limit::fill_through` native_order.hpp:239, which makes a limit market-if-touched; the *n*-tick assumption itself is a TradingView rule and stays in the adapter. | -| `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:378 | `intrabar_kind` native_c_api.h:2333 | `tests/test_native_calc_timing.cpp` | The host owns the lower bars, as `lower_tf` native_run_spec.hpp:393 or `synthesized` native_run_spec.hpp:407. The C magnifier arguments of `run_backtest_full` pineforge.h:648 configure a *compiled Pine strategy*, never a native host. | +| `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:378 | `intrabar_kind` native_c_api.h:2338 | `tests/test_native_calc_timing.cpp` | The host owns the lower bars, as `lower_tf` native_run_spec.hpp:394 or `synthesized` native_run_spec.hpp:408. The C magnifier arguments of `run_backtest_full` pineforge.h:648 configure a *compiled Pine strategy*, never a native host. | | `fill_orders_on_standard_ohlc` | none — the kernel matches the bars you feed it | — | — | TradingView's switch exists because a Heikin-Ashi chart's candles are not the traded prices. A native host feeds the standard OHLC it wants matched, and derives any transformed series itself. | | `max_bars_back` | none — a native host owns its own history | — | `hello_kernel.cpp` | Pine infers a buffer length for every series. `pineforge::Series` series.hpp:94 is a fixed-capacity ring you size yourself, so nothing is inferred and nothing silently truncates. | | `risk_free_rate` | none — the ratio is a report field with TradingView's fixed rate | `sharpe_monthly` pineforge.h:276 | `native_sized_report_strategy.cpp` | `pf_metrics_t::equity.sharpe_monthly` and `pf_metrics_t::equity.sortino_monthly` (`sortino_monthly` pineforge.h:282) use a fixed 2 %/yr, the TradingView default; nothing in the kernel takes a declared rate. Their pre-1.0 spellings `sharpe_tv` / `sortino_tv` were removed for 1.0; the serialized report keys stay `sharpe_tv` / `sortino_tv`. A host that needs another rate computes it from `pf_report_t::equity_curve`. | @@ -267,31 +267,31 @@ source-only features state why a native counterpart does not exist. `HostSized` native_order.hpp:73 is the sixth intent and the escape hatch: the host answers the units itself from `resolve_execution_terms` -(native_host.hpp:935), which sees the kernel-resolved quantity as the facts' +(native_host.hpp:947), which sees the kernel-resolved quantity as the facts' `RemainingUnits` native_order.hpp:550 and may return its own. A C host answers it only for the two host-sized closes the C surface admits, a cohort close and a Book-scoped `WAIT_FOR_APPLIED` child, through `on_close_units` -native_c_api.h:2548; a host-sized opening, and replacing the kernel's own `Sized` +native_c_api.h:2555; a host-sized opening, and replacing the kernel's own `Sized` or `ScopeFraction` units, are C++-only in 1.0. ## Orders {#pine_to_native_map_orders} -Everything here is one call: `submit` native_host.hpp:1201, or `submit_market` -native_host.hpp:1215 for the market-only shorthand, which *refuses* non-market +Everything here is one call: `submit` native_host.hpp:1213, or `submit_market` +native_host.hpp:1227 for the market-only shorthand, which *refuses* non-market extras rather than dropping them. `submit` returns a `RequestHandle` native_order_identity.hpp:135; keep it, it is the order id. In C the same call -is `strategy_native_submit_v1` native_c_api.h:2679 over a -`pf_native_request_v1` native_c_api.h:2097. +is `strategy_native_submit_v1` native_c_api.h:2687 over a +`pf_native_request_v1` native_c_api.h:2102. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2679 | `native_market_strategy.cpp` | Pine's entry *reverses* an opposite position; `Transact` does not. Spell the reversal explicitly with the row below. | -| `strategy.order()` | the same `Request` native_order.hpp:483 | `strategy_native_submit_v1` native_c_api.h:2679 | `native_market_strategy.cpp` | `strategy.order` is the non-reversing entry, so it maps 1:1 with no caveat. | -| `strategy.exit()` | `submit_bracket` native_toolkit.hpp:160 over a `BracketSpec` native_toolkit.hpp:37 | `strategy_native_submit_v1` native_c_api.h:2679 per leg | `native_bracket_strategy.cpp` | The full bracket is described below in [Brackets](@ref pine_to_native_brackets). | +| `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2687 | `native_market_strategy.cpp` | Pine's entry *reverses* an opposite position; `Transact` does not. Spell the reversal explicitly with the row below. | +| `strategy.order()` | the same `Request` native_order.hpp:483 | `strategy_native_submit_v1` native_c_api.h:2687 | `native_market_strategy.cpp` | `strategy.order` is the non-reversing entry, so it maps 1:1 with no caveat. | +| `strategy.exit()` | `submit_bracket` native_toolkit.hpp:160 over a `BracketSpec` native_toolkit.hpp:37 | `strategy_native_submit_v1` native_c_api.h:2687 per leg | `native_bracket_strategy.cpp` | The full bracket is described below in [Brackets](@ref pine_to_native_brackets). | | `strategy.close()` | `Reduce` native_order.hpp:220 with `OwnerOpenedUnits` native_order.hpp:191 and `BindOpening` native_order.hpp:445 | `PF_NATIVE_REDUCE_OWNER_OPENED` native_c_api.h:350 with `PF_NATIVE_OWNER_BIND_OPENING` native_c_api.h:455 | `native_selected_strategy.cpp` | Closes the lots one opening produced. The cycle comes from `ExecutionAppliedEvent::cycle_after` native_order.hpp:1165. A fractional close is `ScopeFraction` native_order.hpp:207 — `qty_percent = 50` is `fraction = 0.5`, with `ScopeClaim` native_order.hpp:195 and `ScopeBasis` native_order.hpp:202 saying whether siblings are netted and when the scope is measured. On a `quantity_grid` a fraction below 1 is floored onto the grid, and `qty_percent = 100` (`fraction = 1`) of the gross scope, settled in one fill, closes the whole scope unfloored. | | `strategy.close_all()` | `Flatten` native_order.hpp:36 | `PF_NATIVE_INTENT_FLATTEN` native_c_api.h:333 | `hello_kernel.cpp` / `hello_kernel_c.c` | The whole book, in one settlement cycle. | -| `strategy.cancel()` | `cancel_where` native_host.hpp:1266 with `NativeRequestField::Label` native_host.hpp:679 | `strategy_native_cancel_where_v1` native_c_api.h:2766 | `native_trail_risk_strategy.cpp` | Withdraws exactly the live requests whose `Request::label` native_order.hpp:485 is that id, and answers how many. `cancel` native_host.hpp:1237 is the single-request form for a host that kept the handle. | -| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1255 | `strategy_native_cancel_all_v1` native_c_api.h:2745 | `native_trail_risk_strategy.cpp` | Neither field is indexed: every form walks the live book once. A `PendingUntilArmed` native_order.hpp:394 child is absent from `native_working_requests` native_host.hpp:1251 before its arm, and all three cancels still address it. | +| `strategy.cancel()` | `cancel_where` native_host.hpp:1278 with `NativeRequestField::Label` native_host.hpp:691 | `strategy_native_cancel_where_v1` native_c_api.h:2774 | `native_trail_risk_strategy.cpp` | Withdraws exactly the live requests whose `Request::label` native_order.hpp:485 is that id, and answers how many. `cancel` native_host.hpp:1249 is the single-request form for a host that kept the handle. | +| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1267 | `strategy_native_cancel_all_v1` native_c_api.h:2753 | `native_trail_risk_strategy.cpp` | Neither field is indexed: every form walks the live book once. A `PendingUntilArmed` native_order.hpp:394 child is absent from `native_working_requests` native_host.hpp:1263 before its arm, and all three cancels still address it. | | `strategy.oca.cancel` | `GroupEffect::Cancel` native_order.hpp:466 | `PF_NATIVE_GROUP_CANCEL` native_c_api.h:474 | `native_bracket_strategy.cpp` | Membership is `Member` native_order.hpp:468 inside `Group` native_order.hpp:473. | | `strategy.oca.reduce` | `GroupEffect::Reduce` native_order.hpp:466 | `PF_NATIVE_GROUP_REDUCE` native_c_api.h:475 | `tests/test_native_resting_group_contract.cpp` | The sibling's quantity is reduced instead of cancelled. | | `strategy.oca.none` | `NoGroup` native_order.hpp:467 | `PF_NATIVE_GROUP_NONE` native_c_api.h:468 | `hello_kernel.cpp` | The default: no group, no sibling effect. | @@ -307,7 +307,7 @@ Two more order concepts Pine has no word for, both generic: ticket and one settlement cycle — the explicit form of Pine's implicit reversal; and `PointBudget` native_order.hpp:379 caps how much of a request may fill at a single matching point, where Pine only ever uses the whole remaining -quantity. Amending a live order is `replace` native_host.hpp:1208, which emits +quantity. Amending a live order is `replace` native_host.hpp:1220, which emits `ReplacedEvent` native_order.hpp:1017 and can carry the predecessor's live trigger state forward through `ReplaceOptions` native_order.hpp:367 -- or keep its handle, or carry a close's book binding, from C++ only. Its C @@ -320,9 +320,9 @@ when a rejection's `RequestRejectReason` native_order.hpp:821 matters. | --- | --- | --- | --- | --- | | `limit=` | `Limit` native_order.hpp:237 | `PF_NATIVE_TRIGGER_LIMIT` native_c_api.h:387 | `native_calc_on_fills_strategy.cpp` | `fill_through` native_order.hpp:239 makes it market-if-touched: the level gates when the request becomes executable, and the fill is no longer bounded by it. | | `stop=` | `Stop` native_order.hpp:246 | `PF_NATIVE_TRIGGER_STOP` native_c_api.h:388 | `native_price_grid_strategy.cpp` | Stop-limit is `StopLimit` native_order.hpp:252. | -| `trail_points` | `FromOwnerFill{+ticks, true}` native_order.hpp:331 | `anchor = PF_NATIVE_ANCHOR_FROM_OWNER_FILL`, `anchor_offset = ticks`, `anchor_offset_in_ticks = 1` native_c_api.h:2112-2119 | `native_bracket_strategy.cpp` | Pine points are the arm distance from the entry, in ticks. The adapter lowers that anchor distance separately from the ride offset; it is not `TrailTicks`. | -| `trail_offset` | `TrailTicks` native_order.hpp:293 (resolved into `Trail::offset` native_order.hpp:291) | `p1 = ticks`, `trail_offset_in_ticks = 1` native_c_api.h:2113-2117 | `native_trail_risk_strategy.cpp` | Pine offset is the ride distance behind the running best, also expressed in ticks. Zero is legal and means "ride the best"; the first adverse move past it exits. | -| `trail_price` | `Trail::arm_price` native_order.hpp:292 | `trail_has_arm_price` native_c_api.h:2120 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1080 (C: `strategy_native_trail_state_v1` native_c_api.h:2987). The kernel compares `arm_price` with the **raw** path. TradingView tests the activation on the **tick-quantized** bar, with or without an offset and the placement close included (R5 lanes E5 and E9's tapes), so the Pine adapter arms at the activation's half-tick threshold — a placement close whose tick reaches the activation arms there — and books from a running best that starts at the activation; a one-shot (`trail_offset` 0) books the tick its resting threshold stands for, rounded away from the position. Since R5 lane E14 that running best is the **kernel's** own: the trailing leg names the activation — or the favourable one of the activation and the placement print when that print armed it — in the generic `Trail::best_seed` native_order.hpp:294, so the ride no longer starts half a tick short of the activation. | +| `trail_points` | `FromOwnerFill{+ticks, true}` native_order.hpp:331 | `anchor = PF_NATIVE_ANCHOR_FROM_OWNER_FILL`, `anchor_offset = ticks`, `anchor_offset_in_ticks = 1` native_c_api.h:2119-2126 | `native_bracket_strategy.cpp` | Pine points are the arm distance from the entry, in ticks. The adapter lowers that anchor distance separately from the ride offset; it is not `TrailTicks`. | +| `trail_offset` | `TrailTicks` native_order.hpp:293 (resolved into `Trail::offset` native_order.hpp:291) | `p1 = ticks`, `trail_offset_in_ticks = 1` native_c_api.h:2120-2124 | `native_trail_risk_strategy.cpp` | Pine offset is the ride distance behind the running best, also expressed in ticks. Zero is legal and means "ride the best"; the first adverse move past it exits. | +| `trail_price` | `Trail::arm_price` native_order.hpp:292 | `trail_has_arm_price` native_c_api.h:2125 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1092 (C: `strategy_native_trail_state_v1` native_c_api.h:2999). The kernel compares `arm_price` with the **raw** path. TradingView tests the activation on the **tick-quantized** bar, with or without an offset and the placement close included (R5 lanes E5 and E9's tapes), so the Pine adapter arms at the activation's half-tick threshold — a placement close whose tick reaches the activation arms there — and books from a running best that starts at the activation; a one-shot (`trail_offset` 0) books the tick its resting threshold stands for, rounded away from the position. Since R5 lane E14 that running best is the **kernel's** own: the trailing leg names the activation — or the favourable one of the activation and the placement print when that print armed it — in the generic `Trail::best_seed` native_order.hpp:294, so the ride no longer starts half a tick short of the activation. | `limit=` and `stop=` are listed once here rather than once per command, because a native trigger is a property of the `Request` native_order.hpp:483 @@ -344,11 +344,11 @@ once. Natively they are separate and each is nameable: the parent's fill the kernel materializes `fill + offset` — in ticks when `FromOwnerFill::ticks` native_order.hpp:333 is set — snaps it per `NativeAnchorRounding` native_order.hpp:309, offers it once to - `resolve_anchored_level` native_host.hpp:1000, and carries the installed level + `resolve_anchored_level` native_host.hpp:1012, and carries the installed level in `ArmedEvent` native_order.hpp:1246. - **Invisible until armed.** `NativeArmVisibility::PendingUntilArmed` native_order.hpp:394 keeps the legs out of `native_working_requests` - native_host.hpp:1251 until the fill, like Pine's pending exit and like a + native_host.hpp:1263 until the fill, like Pine's pending exit and like a broker that shows no working child before the parent trades. - **First match.** `NativeArmFirstMatch::AfterArmPrint` native_order.hpp:412 gives the leg the birth rule of a request submitted from the fill callback: @@ -377,10 +377,10 @@ of a predicate, and it reports the kernel verbatim too: `ReplaceResult` and `SubmitResult` of every command that actually reached the kernel, and `cancel_outcome` says whether a cancel was commanded at all. Pine's `strategy.cancel` tells a script neither. In C -each leg is its own `strategy_native_submit_v1` native_c_api.h:2679 with +each leg is its own `strategy_native_submit_v1` native_c_api.h:2687 with `PF_NATIVE_OWNER_WAIT_FOR_APPLIED` native_c_api.h:454, and its `first_match` and `scope` are the request's `arm_first_match` / `arm_scope` -native_c_api.h:2180-2181, so a C bracket child arms after its owner's print or over +native_c_api.h:2185-2186, so a C bracket child arms after its owner's print or over the whole book exactly as a C++ one does. The Pine adapter lowers its own queued relative legs exactly this way — a @@ -405,12 +405,12 @@ block is the `PF_NATIVE_SPEC_EXT_RISK` native_c_api.h:735 tail. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:596 | `allowed_open_directions` pineforge.h:541 | `native_risk_limits_strategy.cpp` | The one `strategy.risk.*` rule that is already the kernel's, per ADR-0001's `risk` ruling row. Rejects with `OpeningDirection` native_execution_consumer.cpp:2947. | -| `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:594 | `max_abs_units` pineforge.h:542 | `tests/test_native_run_spec.cpp` | Tests the *resulting* book. TradingView gates the *live* book before the fill, so an entry is refused only once the book already holds the limit — ruled **adapter-policy** in ADR-0001's `max_abs_units` row, with lane N12's measurement (the adapter ends at 4 where the kernel cap ends at 2). | -| `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2316 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:259 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:261 is set. Blocks openings to the end of the run. | -| `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:312 | `risk_max_intraday_loss` native_c_api.h:2319 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | -| `strategy.risk.max_cons_loss_days()` | `NativeRiskLimits::max_consecutive_loss_days` native_run_spec.hpp:313 | `risk_max_consecutive_loss_days` native_c_api.h:2321 | `native_risk_limits_strategy.cpp` | Counts *days* that close with a realized loss; TradingView's streak counts trades. `NativeRiskDay` native_run_spec.hpp:270 chooses the session day or the civil date in the spec timezone. | -| `strategy.risk.max_intraday_filled_orders()` | `NativeRiskLimits::max_fills_per_day` native_run_spec.hpp:314 | `risk_max_fills_per_day` native_c_api.h:2323 | `native_trail_risk_strategy.cpp` | Applied fills counted as they settle, so the fills of one matching point all settle and the limit blocks the next admission. Read the ledger back with `native_risk_state` native_host.hpp:1311. | +| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:597 | `allowed_open_directions` pineforge.h:541 | `native_risk_limits_strategy.cpp` | The one `strategy.risk.*` rule that is already the kernel's, per ADR-0001's `risk` ruling row. Rejects with `OpeningDirection` native_execution_consumer.cpp:2947. | +| `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:595 | `max_abs_units` pineforge.h:542 | `tests/test_native_run_spec.cpp` | Tests the *resulting* book. TradingView gates the *live* book before the fill, so an entry is refused only once the book already holds the limit — ruled **adapter-policy** in ADR-0001's `max_abs_units` row, with lane N12's measurement (the adapter ends at 4 where the kernel cap ends at 2). | +| `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2321 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:259 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:261 is set. Blocks openings to the end of the run. | +| `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:312 | `risk_max_intraday_loss` native_c_api.h:2324 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | +| `strategy.risk.max_cons_loss_days()` | `NativeRiskLimits::max_consecutive_loss_days` native_run_spec.hpp:313 | `risk_max_consecutive_loss_days` native_c_api.h:2326 | `native_risk_limits_strategy.cpp` | Counts *days* that close with a realized loss; TradingView's streak counts trades. `NativeRiskDay` native_run_spec.hpp:270 chooses the session day or the civil date in the spec timezone. | +| `strategy.risk.max_intraday_filled_orders()` | `NativeRiskLimits::max_fills_per_day` native_run_spec.hpp:314 | `risk_max_fills_per_day` native_c_api.h:2328 | `native_trail_risk_strategy.cpp` | Applied fills counted as they settle, so the fills of one matching point all settle and the limit blocks the next admission. Read the ledger back with `native_risk_state` native_host.hpp:1323. | The whole block is **native-only by ruling**: no adapter run declares it, and lane N12 measured why — TradingView's drawdown latch samples at the close and @@ -429,21 +429,22 @@ liquidation level, and the ticket the forced close is booked under. | concept | C++ | C | Runs in | | --- | --- | --- | --- | -| opening requirement per side | `initial_long` native_run_spec.hpp:241 / `initial_short` native_run_spec.hpp:242 | `margin_initial_long` native_c_api.h:2290 | `native_margin_strategy.cpp` | -| maintenance requirement per side | `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 | `margin_maintenance_long` native_c_api.h:2295 | `native_margin_strategy.cpp` | -| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:140 | `margin_sizing` native_c_api.h:2285 | `native_margin_strategy.cpp` | -| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:158 | `margin_check` native_c_api.h:2286 | `native_margin_strategy.cpp` | -| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2353 | `tests/test_native_margin_model.cpp` | -| which base the level is solved from | `NativeLiquidationLevelBase` native_run_spec.hpp:182 | `margin_level_base` native_c_api.h:2354 | `tests/test_native_margin_model.cpp` | -| the forced close's ticket | `liquidation_label` native_run_spec.hpp:251 | `margin_liquidation_label` native_c_api.h:2355 | `native_margin_strategy.cpp` | -| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:948 | — C hosts gate through `on_precommit` native_c_api.h:2574 | `native_margin_strategy.cpp` | -| the host's own requirement | `resolve_margin_requirement` native_host.hpp:964 | `on_margin_requirement` native_c_api.h:2511 | `native_margin_strategy.cpp` | -| which check points run at all | `margin_check_allowed` native_host.hpp:976 | `on_margin_check` native_c_api.h:2520 | `native_margin_strategy.cpp` | -| a step of the account FX curve, checked though no price moved | `NativeMarginCheckKind::FxRoll` native_host.hpp:542 | `PF_NATIVE_MARGIN_CHECK_FX_ROLL` native_c_api.h:931 | `native_fx_roll_strategy.cpp` | -| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:983 | `on_margin_call_units` native_c_api.h:2527 | `native_margin_strategy.cpp` | -| the liquidation event | `MarginCallEvent` native_order.hpp:1262 delivered to `on_native_margin_call` native_host.hpp:989 | `on_margin_call` native_c_api.h:2475 | `native_margin_strategy.cpp` | - -The one-scalar `initial_margin_fraction` native_run_spec.hpp:597 predates the +| opening requirement per side | `initial_long` native_run_spec.hpp:241 / `initial_short` native_run_spec.hpp:242 | `margin_initial_long` native_c_api.h:2295 | `native_margin_strategy.cpp` | +| maintenance requirement per side | `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 | `margin_maintenance_long` native_c_api.h:2300 | `native_margin_strategy.cpp` | +| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:140 | `margin_sizing` native_c_api.h:2290 | `native_margin_strategy.cpp` | +| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:158 | `margin_check` native_c_api.h:2291 | `native_margin_strategy.cpp` | +| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2358 | `tests/test_native_margin_model.cpp` | +| which base the level is solved from | `NativeLiquidationLevelBase` native_run_spec.hpp:182 | `margin_level_base` native_c_api.h:2359 | `tests/test_native_margin_model.cpp` | +| the forced close's ticket | `liquidation_label` native_run_spec.hpp:251 | `margin_liquidation_label` native_c_api.h:2360 | `native_margin_strategy.cpp` | +| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:960 | — C hosts gate through `on_precommit` native_c_api.h:2580 | `native_margin_strategy.cpp` | +| the host's own requirement | `resolve_margin_requirement` native_host.hpp:976 | `on_margin_requirement` native_c_api.h:2517 | `native_margin_strategy.cpp` | +| which check points run at all | `margin_check_allowed` native_host.hpp:988 | `on_margin_check` native_c_api.h:2526 | `native_margin_strategy.cpp` | +| a step of the account FX curve, checked though no price moved | `NativeMarginCheckKind::FxRoll` native_host.hpp:553 | `PF_NATIVE_MARGIN_CHECK_FX_ROLL` native_c_api.h:931 | `native_fx_roll_strategy.cpp` | +| a magnified bar's later samples, each checked where the path is (TradingView's bar magnifier) | `NativeMarginCheckKind::IntrabarSample` native_host.hpp:554 | `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE` native_c_api.h:939 | `tests/test_native_margin_intrabar_samples.cpp` | +| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:995 | `on_margin_call_units` native_c_api.h:2533 | `native_margin_strategy.cpp` | +| the liquidation event | `MarginCallEvent` native_order.hpp:1262 delivered to `on_native_margin_call` native_host.hpp:1001 | `on_margin_call` native_c_api.h:2481 | `native_margin_strategy.cpp` | + +The one-scalar `initial_margin_fraction` native_run_spec.hpp:598 predates the model and remains the simple spelling; the two are mutually exclusive. It is ruled **adapter-policy** (ADR-0001's `initial_margin_fraction` row): the adapter answers TradingView's ten-significant-digit money admission itself with @@ -456,29 +457,29 @@ part both ways (`tests/test_adapter_margin_schedule_differential.cpp`, M11). | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| script body at bar close | `on_native_bar` native_host.hpp:881 | `on_bar` native_c_api.h:2465 | `hello_kernel.cpp` / `hello_kernel_c.c` | The one pure virtual. Called once per closed script bar — and, with the default cadence, at every calculation there is: the kernel routes all of them through `on_native_recalculate` native_host.hpp:901, whose default forwards here. | -| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:901 with `NativeCalculationReason` native_host.hpp:813 | `on_recalculate` native_c_api.h:2493 | `native_calc_on_fills_strategy.cpp` | **Installing it REPLACES `on_native_bar` for every calculation** — in C++ you override the forwarding away, and in C a non-NULL `on_recalculate` is what the kernel calls instead of `on_bar`. A host that wants both calls the other itself from here; a C host that installs only `on_recalculate` and still expects `on_bar` silently trades nothing. | -| `barstate.isconfirmed` | `NativeCalculationReason::BarClose` native_host.hpp:814 | `on_recalculate` native_c_api.h:2493 | `native_calc_on_fills_strategy.cpp` | The script bar's own calculation is delivered for every run, whatever the cadence is. | -| `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1756 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | -| bar-open pre-pass | `on_native_bar_open` native_host.hpp:877 | `on_bar_open` native_c_api.h:2464 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | -| the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1061 | `strategy_native_partial_bar_v1` native_c_api.h:2972 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | -| a fill, as it happens | `on_native_applied` native_host.hpp:926 | `on_applied` native_c_api.h:2467 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5369. | -| raw input before aggregation | `on_native_input` native_host.hpp:863 | `on_input` native_c_api.h:2462 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | -| a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:916 | `on_sub_bar` native_c_api.h:2503 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | -| `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1454 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. | -| `session.ismarket` | `in_session` market_driver.hpp:161 | `in_session` native_c_api.h:1472 | `tests/test_native_session_day_facts.cpp` | The script bar's label is in session on the run's own calendar (`NativeRunSpec::session` / `timezone`); a D/W/M bar holds whole session days, so it is always true there. The calendar applies a session's day mask to the session day's trading date: Sunday 17:00 CT of a CME-style `1700-1600:23456` is Monday's session, in session. Pine's own `session.ismarket` is still generated as the time-of-day predicate `pine_session_ismarket` session_time.hpp:247, whose mask reads each instant's weekday, so on such a masked overnight session the two differ: on six `lab tv` tapes TradingView flags every bar in market, the calendar's fact matches it on every bar, and the predicate misses every Sunday open under a weekday mask (`1700-1600:23456`) and every bar of `0000-2400`, which it cannot parse as a whole day (`tests/test_session_ismarket_tape.cpp`). Unmasked sessions (`1700-1600`, `1700-1700`, `24x7`, `0930-1600`) agree on all three. In C, read the three session bytes only when `session_facts` native_c_api.h:1471 is 1. | -| `session.isfirstbar` | `opens_session_day` market_driver.hpp:162 | `opens_session_day` native_c_api.h:1473 | `tests/test_native_session_day_facts.cpp` | In session, and the bar before it is not, or is on another session day. The session day rolls at the session's first window start and is keyed to its trading date, so an overnight session is one day across local midnight (Tokyo `2230-0500`: its days open at 22:30). "The bar before" is the one the run holds — the batch input, the stream warmup — else the calendar's previous eligible input slot across declared breaks; a run's first bar opens its day. | -| `session.islastbar` | `closes_session_day` market_driver.hpp:163, `closes_session_day_open_ended` market_driver.hpp:164 | `closes_session_day` native_c_api.h:1474 | `tests/test_native_session_day_facts.cpp` | In session, and the bar after it is not, or is on another session day, "the bar after" read the same way. A batch's final bar closes its day (a batch is complete input); `closes_session_day_open_ended` reads the calendar there instead, for a host recomputing a batch whose last input is still forming (C++ only: a C host's live edge is a stream). A stream's bars read the calendar, so a stream cannot see an early close its session string does not declare: the NYSE half day's 12:45 closes the day in a batch (the next bar held is the next day's) but not in a stream (ADR-0001, ruling "Session-day facts at a bar with nothing held after it"). | -| the price at the current point | `current_execution_point` native_host.hpp:1075 | `price` native_c_api.h:1533 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1087 is an observation, never an apply token; `execute_current` native_host.hpp:1092 is the apply. | +| script body at bar close | `on_native_bar` native_host.hpp:893 | `on_bar` native_c_api.h:2471 | `hello_kernel.cpp` / `hello_kernel_c.c` | The one pure virtual. Called once per closed script bar — and, with the default cadence, at every calculation there is: the kernel routes all of them through `on_native_recalculate` native_host.hpp:913, whose default forwards here. | +| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:913 with `NativeCalculationReason` native_host.hpp:825 | `on_recalculate` native_c_api.h:2499 | `native_calc_on_fills_strategy.cpp` | **Installing it REPLACES `on_native_bar` for every calculation** — in C++ you override the forwarding away, and in C a non-NULL `on_recalculate` is what the kernel calls instead of `on_bar`. A host that wants both calls the other itself from here; a C host that installs only `on_recalculate` and still expects `on_bar` silently trades nothing. | +| `barstate.isconfirmed` | `NativeCalculationReason::BarClose` native_host.hpp:826 | `on_recalculate` native_c_api.h:2499 | `native_calc_on_fills_strategy.cpp` | The script bar's own calculation is delivered for every run, whatever the cadence is. | +| `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1763 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | +| bar-open pre-pass | `on_native_bar_open` native_host.hpp:889 | `on_bar_open` native_c_api.h:2470 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | +| the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1073 | `strategy_native_partial_bar_v1` native_c_api.h:2984 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | +| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2473 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5375. | +| raw input before aggregation | `on_native_input` native_host.hpp:875 | `on_input` native_c_api.h:2468 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | +| a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:928 | `on_sub_bar` native_c_api.h:2509 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | +| `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1461 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. | +| `session.ismarket` | `in_session` market_driver.hpp:161 | `in_session` native_c_api.h:1479 | `tests/test_native_session_day_facts.cpp` | The script bar's label is in session on the run's own calendar (`NativeRunSpec::session` / `timezone`); a D/W/M bar holds whole session days, so it is always true there. The calendar applies a session's day mask to the session day's trading date: Sunday 17:00 CT of a CME-style `1700-1600:23456` is Monday's session, in session. Pine's own `session.ismarket` is still generated as the time-of-day predicate `pine_session_ismarket` session_time.hpp:247, whose mask reads each instant's weekday, so on such a masked overnight session the two differ: on six `lab tv` tapes TradingView flags every bar in market, the calendar's fact matches it on every bar, and the predicate misses every Sunday open under a weekday mask (`1700-1600:23456`) and every bar of `0000-2400`, which it cannot parse as a whole day (`tests/test_session_ismarket_tape.cpp`). Unmasked sessions (`1700-1600`, `1700-1700`, `24x7`, `0930-1600`) agree on all three. In C, read the three session bytes only when `session_facts` native_c_api.h:1478 is 1. | +| `session.isfirstbar` | `opens_session_day` market_driver.hpp:162 | `opens_session_day` native_c_api.h:1480 | `tests/test_native_session_day_facts.cpp` | In session, and the bar before it is not, or is on another session day. The session day rolls at the session's first window start and is keyed to its trading date, so an overnight session is one day across local midnight (Tokyo `2230-0500`: its days open at 22:30). "The bar before" is the one the run holds — the batch input, the stream warmup — else the calendar's previous eligible input slot across declared breaks; a run's first bar opens its day. | +| `session.islastbar` | `closes_session_day` market_driver.hpp:163, `closes_session_day_open_ended` market_driver.hpp:164 | `closes_session_day` native_c_api.h:1481 | `tests/test_native_session_day_facts.cpp` | In session, and the bar after it is not, or is on another session day, "the bar after" read the same way. A batch's final bar closes its day (a batch is complete input); `closes_session_day_open_ended` reads the calendar there instead, for a host recomputing a batch whose last input is still forming (C++ only: a C host's live edge is a stream). A stream's bars read the calendar, so a stream cannot see an early close its session string does not declare: the NYSE half day's 12:45 closes the day in a batch (the next bar held is the next day's) but not in a stream (ADR-0001, ruling "Session-day facts at a bar with nothing held after it"). | +| the price at the current point | `current_execution_point` native_host.hpp:1087 | `price` native_c_api.h:1540 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1099 is an observation, never an apply token; `execute_current` native_host.hpp:1104 is the apply. | ## Position, account and report {#pine_to_native_map_report} Three surfaces answer this namespace, and which one you use is the whole migration decision: -1. **Live, inside a callback** — `physical_position` native_host.hpp:1280, - `native_marked_equity` native_host.hpp:1289, `native_open_lots` - native_host.hpp:1285, `native_liquidation_price` native_host.hpp:1308. +1. **Live, inside a callback** — `physical_position` native_host.hpp:1292, + `native_marked_equity` native_host.hpp:1301, `native_open_lots` + native_host.hpp:1297, `native_liquidation_price` native_host.hpp:1320. 2. **The protected accessors your host inherits.** `NativeStrategyHost` derives from `BacktestEngine`, so the statistics Pine exposes as `strategy.*` are ordinary protected member functions of your own class: `net_profit` @@ -507,7 +508,7 @@ For event readback, C++ `NativeRunSpec::event_retention` defaults to `Window`: the kernel retires events at script-bar boundaries, so a host that inspects `native_events(0)` only after a run must request `Full`. A host that consumes events during the run can keep `Window` and acknowledge its cursor with -`native_acknowledge_events` native_host.hpp:1332. A C caller that omits the +`native_acknowledge_events` native_host.hpp:1344. A C caller that omits the extension's retention word keeps `Full` for ABI compatibility. Pine's `strategy.openprofit` is the **gross** mark-to-market move of open @@ -527,16 +528,16 @@ range-end reporting is off. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.account_currency` | `currency` native_run_spec.hpp:570 | `currency` pineforge.h:537 | `hello_kernel.cpp` / `native_fx_roll_strategy.cpp` | Declared, never inferred. `account_fx` native_run_spec.hpp:581 is the one positive scalar that converts quote to account; a timestamped curve is `configure_native_fx_curve` native_host.hpp:1190. The FX-roll example runs a JPY account on a USD-quoted stock through such a curve, and a step of it is a margin check point of its own. | -| `strategy.initial_capital` | `initial_capital` native_run_spec.hpp:579 | `initial_capital` pineforge.h:539 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | -| `strategy.equity` | `current_equity() + open_profit(mark)` engine.hpp:970-1024; or `native_marked_equity(mark)` native_host.hpp:1289 plus open entry commissions | `strategy_native_marked_equity_v1` native_c_api.h:3017 plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine marks at the current `close`. The native marked call takes the mark explicitly and returns balance plus open lots' **net** `unrealized_pnl` native_host.hpp:341; add their remaining entry commissions to reproduce Pine's gross-open-profit equity. | +| `strategy.account_currency` | `currency` native_run_spec.hpp:571 | `currency` pineforge.h:537 | `hello_kernel.cpp` / `native_fx_roll_strategy.cpp` | Declared, never inferred. `account_fx` native_run_spec.hpp:582 is the one positive scalar that converts quote to account; a timestamped curve is `configure_native_fx_curve` native_host.hpp:1202. The FX-roll example runs a JPY account on a USD-quoted stock through such a curve, and a step of it is a margin check point of its own. | +| `strategy.initial_capital` | `initial_capital` native_run_spec.hpp:580 | `initial_capital` pineforge.h:539 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | +| `strategy.equity` | `current_equity() + open_profit(mark)` engine.hpp:970-1024; or `native_marked_equity(mark)` native_host.hpp:1301 plus open entry commissions | `strategy_native_marked_equity_v1` native_c_api.h:3024 plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine marks at the current `close`. The native marked call takes the mark explicitly and returns balance plus open lots' **net** `unrealized_pnl` native_host.hpp:341; add their remaining entry commissions to reproduce Pine's gross-open-profit equity. | | `strategy.netprofit` | `net_profit` engine.hpp:967 | `net_profit` pineforge.h:412 | `native_open_lots_strategy.cpp` | Realized only. | -| `strategy.netprofit_percent` | derive: `net_profit` engine.hpp:967 over `initial_capital` native_run_spec.hpp:579 | `pf_metrics_t` pineforge.h:358 | `native_sized_report_strategy.cpp` | The kernel keeps the facts, not the ratio. | +| `strategy.netprofit_percent` | derive: `net_profit` engine.hpp:967 over `initial_capital` native_run_spec.hpp:580 | `pf_metrics_t` pineforge.h:358 | `native_sized_report_strategy.cpp` | The kernel keeps the facts, not the ratio. | | `strategy.grossprofit` | `gross_profit` engine.hpp:968 | `pf_trade_stats_t` pineforge.h:291 | `native_open_lots_strategy.cpp` | | | `strategy.grossprofit_percent` | `grossprofit_percent` engine.hpp:975 | `pf_metrics_t` pineforge.h:358 | `native_sized_report_strategy.cpp` | Of initial capital. | | `strategy.grossloss` | `gross_loss` engine.hpp:969 | `pf_trade_stats_t::gross_loss` pineforge.h:246 | `native_open_lots_strategy.cpp` | For booked closed trades the protected C++ accessor is signed (negative for losses), while the C report stores a positive loss magnitude. With range-end reporting off, `-C.gross_loss == gross_loss()` and `netprofit == grossprofit - C.gross_loss`. With it on, C trade statistics include the report-only range-end rows; filter `open_at_end == 0` before comparing to Pine's closed-trade statistics. | | `strategy.grossloss_percent` | `grossloss_percent` engine.hpp:978 | `pf_metrics_t` pineforge.h:358 | `native_sized_report_strategy.cpp` | | -| `strategy.openprofit` | `open_profit` engine.hpp:1025 | `strategy_native_marked_equity_v1` native_c_api.h:3017 less realized balance, plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine's open profit is gross of remaining entry fees. The native marked equity and open-lot `unrealized_pnl` are net of them; use the same explicit mark for every term. | +| `strategy.openprofit` | `open_profit` engine.hpp:1025 | `strategy_native_marked_equity_v1` native_c_api.h:3024 less realized balance, plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine's open profit is gross of remaining entry fees. The native marked equity and open-lot `unrealized_pnl` are net of them; use the same explicit mark for every term. | | `strategy.openprofit_percent` | derive from `open_profit` engine.hpp:1025 | — | `native_open_lots_strategy.cpp` | Pine's denominator is the realized equity. | | `strategy.max_drawdown` | `max_drawdown_` engine.hpp:581 | `max_equity_drawdown` pineforge.h:296 | `native_open_lots_strategy.cpp` | Folded under **every** report policy — see the rule above. The scalar is the run's own; the `pf_equity_stats_t` figure derived from the recorded curve still needs `KernelRecorded` native_run_spec.hpp:63. | | `strategy.max_drawdown_percent` | `max_drawdown_percent` engine.hpp:1507 | `max_equity_drawdown_pct` pineforge.h:296 | `native_sized_report_strategy.cpp` | Same rule. | @@ -554,22 +555,22 @@ range-end reporting is off. | `strategy.max_contracts_held_all` | `max_contracts_held_all` engine.hpp:1836 | — | `native_open_lots_strategy.cpp` | `pf_trade_stats_t` has no max-contracts-held field. A C host must derive the peak from position/open-lot observations; there is no direct C report counterpart. | | `strategy.max_contracts_held_long` | `max_contracts_held_long` engine.hpp:1837 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the long-side peak from position/open-lot observations. | | `strategy.max_contracts_held_short` | `max_contracts_held_short` engine.hpp:1838 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the short-side peak from position/open-lot observations. | -| `strategy.position_size` | `NativePhysicalPosition::signed_units` native_host.hpp:297 | `strategy_native_position_v1` native_c_api.h:2792 | `hello_kernel.cpp` | Signed: no separate direction field. | -| `strategy.position_avg_price` | `NativePhysicalPosition::average_price` native_host.hpp:298 | `strategy_native_position_v1` native_c_api.h:2792 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | -| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1285 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2833 | `native_open_lots_strategy.cpp` | Pine reports the newest entry's id; the native book lets you read any lot's. | -| `strategy.margin_liquidation_price` | `native_liquidation_price` native_host.hpp:1308 | `strategy_native_liquidation_price_v1` native_c_api.h:3020 | `native_margin_strategy.cpp` | Solved from the declared maintenance requirement, so it answers `nullopt` when the model declares none or the slope is degenerate (a long at full maintenance has no finite level). | -| `strategy.convert_to_account()` | none — the kernel converts with the FX you declare | `strategy_configure_native_fx_curve_v1` pineforge.h:584 | `native_fx_roll_strategy.cpp` / `tests/test_native_fx_curve.cpp` | Pine's converter needs TradingView's own FX series. Natively, `account_fx` native_run_spec.hpp:581 is one scalar and `configure_native_fx_curve` native_host.hpp:1190 stages an immutable timestamped curve; codegen emits the Pine call as the identity, because no fixed table reproduces TradingView's series. | +| `strategy.position_size` | `NativePhysicalPosition::signed_units` native_host.hpp:297 | `strategy_native_position_v1` native_c_api.h:2800 | `hello_kernel.cpp` | Signed: no separate direction field. | +| `strategy.position_avg_price` | `NativePhysicalPosition::average_price` native_host.hpp:298 | `strategy_native_position_v1` native_c_api.h:2800 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | +| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1297 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2841 | `native_open_lots_strategy.cpp` | Pine reports the newest entry's id; the native book lets you read any lot's. | +| `strategy.margin_liquidation_price` | `native_liquidation_price` native_host.hpp:1320 | `strategy_native_liquidation_price_v1` native_c_api.h:3032 | `native_margin_strategy.cpp` | Solved from the declared maintenance requirement, so it answers `nullopt` when the model declares none or the slope is degenerate (a long at full maintenance has no finite level). | +| `strategy.convert_to_account()` | none — the kernel converts with the FX you declare | `strategy_configure_native_fx_curve_v1` pineforge.h:584 | `native_fx_roll_strategy.cpp` / `tests/test_native_fx_curve.cpp` | Pine's converter needs TradingView's own FX series. Natively, `account_fx` native_run_spec.hpp:582 is one scalar and `configure_native_fx_curve` native_host.hpp:1202 stages an immutable timestamped curve; codegen emits the Pine call as the identity, because no fixed table reproduces TradingView's series. | | `strategy.convert_to_symbol()` | none — same reason, the other direction | `strategy_configure_native_fx_curve_v1` pineforge.h:584 | `tests/test_native_fx_curve.cpp` | Same ruling. | @anchor pine_to_native_map_trades ## Open trades {#pine_to_native_map_open_trades} -`native_open_lots(mark)` native_host.hpp:1285 answers one `NativeOpenLot` +`native_open_lots(mark)` native_host.hpp:1297 answers one `NativeOpenLot` native_host.hpp:328 per open physical lot, in book order (oldest first, `NativePhysicalPosition::lot_count` native_host.hpp:299 rows). The C twin is -`strategy_native_open_lot_count_v1` native_c_api.h:2825 then -`strategy_native_open_lot_get_v1` native_c_api.h:2833 into a -`pf_native_open_lot_v1` native_c_api.h:1732, whose two strings borrow the +`strategy_native_open_lot_count_v1` native_c_api.h:2833 then +`strategy_native_open_lot_get_v1` native_c_api.h:2841 into a +`pf_native_open_lot_v1` native_c_api.h:1739, whose two strings borrow the snapshot until the next count call. `mark` is the price the three marked fields are computed at; Pine's builtins mark at the current `close`. Reading it moves no fill, no hash and no row. `native_open_lots_strategy.cpp` is the runnable @@ -582,26 +583,26 @@ native_order.hpp:445 names. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.opentrades` | `native_open_lots` native_host.hpp:1285 | `strategy_native_open_lot_count_v1` native_c_api.h:2825 | `native_open_lots_strategy.cpp` | The row count, which equals `lot_count` native_host.hpp:299. | -| `strategy.opentrades.entry_id()` | `entry_label` native_host.hpp:333 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The opening request's `label` native_order.hpp:485. | -| `strategy.opentrades.entry_comment()` | `entry_comment` native_host.hpp:334 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The opening request's `comment` native_order.hpp:486. | -| `strategy.opentrades.entry_bar_index()` | `entry_bar_index` native_host.hpp:336 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Script-bar index of the opening fill. | -| `strategy.opentrades.entry_time()` | `entry_time_ms` native_host.hpp:335 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Effective time of the opening fill, Unix ms. | -| `strategy.opentrades.entry_price()` | `entry_price` native_host.hpp:337 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The booked price: slippage and the price grid are already applied. | -| `strategy.opentrades.size()` | `signed_units` native_host.hpp:338 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Pine's is unsigned with a separate direction; here `> 0` is long, `< 0` short, and `side` native_host.hpp:332 says the same. | -| `strategy.opentrades.commission()` | `entry_commission` native_host.hpp:339 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The entry fee still on the lot, account currency. A partial close takes its share onto the closed row: the example's FIFO reduce leaves 1.5 units carrying 1.5 of a 2.00 ticket. | -| `strategy.opentrades.profit()` | `unrealized_pnl` native_host.hpp:341 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The move from `entry_price` to `mark` in account currency, **net of `entry_commission`** — the lot's own term of the marked equity. Gross is `unrealized_pnl + entry_commission`. | +| `strategy.opentrades` | `native_open_lots` native_host.hpp:1297 | `strategy_native_open_lot_count_v1` native_c_api.h:2833 | `native_open_lots_strategy.cpp` | The row count, which equals `lot_count` native_host.hpp:299. | +| `strategy.opentrades.entry_id()` | `entry_label` native_host.hpp:333 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | The opening request's `label` native_order.hpp:485. | +| `strategy.opentrades.entry_comment()` | `entry_comment` native_host.hpp:334 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | The opening request's `comment` native_order.hpp:486. | +| `strategy.opentrades.entry_bar_index()` | `entry_bar_index` native_host.hpp:336 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | Script-bar index of the opening fill. | +| `strategy.opentrades.entry_time()` | `entry_time_ms` native_host.hpp:335 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | Effective time of the opening fill, Unix ms. | +| `strategy.opentrades.entry_price()` | `entry_price` native_host.hpp:337 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | The booked price: slippage and the price grid are already applied. | +| `strategy.opentrades.size()` | `signed_units` native_host.hpp:338 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | Pine's is unsigned with a separate direction; here `> 0` is long, `< 0` short, and `side` native_host.hpp:332 says the same. | +| `strategy.opentrades.commission()` | `entry_commission` native_host.hpp:339 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | The entry fee still on the lot, account currency. A partial close takes its share onto the closed row: the example's FIFO reduce leaves 1.5 units carrying 1.5 of a 2.00 ticket. | +| `strategy.opentrades.profit()` | `unrealized_pnl` native_host.hpp:341 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | The move from `entry_price` to `mark` in account currency, **net of `entry_commission`** — the lot's own term of the marked equity. Gross is `unrealized_pnl + entry_commission`. | | `strategy.opentrades.profit_percent()` | derive: `unrealized_pnl` native_host.hpp:341 over entry cost | — | `native_open_lots_strategy.cpp` | Entry cost is `entry_price * abs(signed_units) * point_value * account_fx`. | -| `strategy.opentrades.max_runup()` | `favorable_excursion` native_host.hpp:342 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Largest move for the lot the kernel has sampled along the delivered path, account currency, gross of fees, with `mark` folded in. | +| `strategy.opentrades.max_runup()` | `favorable_excursion` native_host.hpp:342 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | Largest move for the lot the kernel has sampled along the delivered path, account currency, gross of fees, with `mark` folded in. | | `strategy.opentrades.max_runup_percent()` | derive from `favorable_excursion` native_host.hpp:342 | — | `native_open_lots_strategy.cpp` | The snapshot carries the facts, not the ratio. | -| `strategy.opentrades.max_drawdown()` | `adverse_excursion` native_host.hpp:343 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Largest move against the lot, likewise; both excursions are magnitudes `>= 0`. | +| `strategy.opentrades.max_drawdown()` | `adverse_excursion` native_host.hpp:343 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | Largest move against the lot, likewise; both excursions are magnitudes `>= 0`. | | `strategy.opentrades.max_drawdown_percent()` | derive from `adverse_excursion` native_host.hpp:343 | — | `native_open_lots_strategy.cpp` | | | `strategy.opentrades.capital_held` | `open_trades_capital_held` engine.hpp:1016 | — | `native_open_lots_strategy.cpp` | `pf_trade_stats_t` carries no capital-held field. Derive it from the open lots and declared price/value/FX terms, or read the protected C++ accessor. | A NaN `mark` keeps every booking fact, leaves `unrealized_pnl` native_host.hpp:341 NaN and folds nothing into the excursions — the example asserts this at every calculation. A host that owns lot excursions -(`owns_lot_excursions` native_host.hpp:1011) keeps its own sampler, so for that +(`owns_lot_excursions` native_host.hpp:1023) keeps its own sampler, so for that run the two excursion fields fold `mark` alone. ## Closed trades {#pine_to_native_map_closed_trades} @@ -610,7 +611,7 @@ run the two excursion fields fold `mark` alone. the `Trade` engine.hpp:172 rows this run booked, in booking order; `report_trade_count` engine.hpp:1815 and `get_report_trade` engine.hpp:1818 span the same rows followed by the range-end rows -`report_open_position_at_end` native_run_spec.hpp:614 adds. In C the report's +`report_open_position_at_end` native_run_spec.hpp:615 adds. In C the report's `pf_report_t::trades` pineforge.h:409 carries the numeric fields, and the strings and the cause come from `strategy_closed_trade_entry_id` pineforge.h:1192, `strategy_closed_trade_exit_id` pineforge.h:1205, @@ -623,7 +624,7 @@ rows of that array. | `strategy.closedtrades` | `closed_trade_count` engine.hpp:1807 | `total_trades` pineforge.h:406 | `native_open_lots_strategy.cpp` | `closed_trade_count` counts booked closed rows. The C report's `total_trades` also includes range-end rows when `report_open_position_at_end` is enabled; filter `pf_trade_t::open_at_end == 0` to match Pine `strategy.closedtrades`. | | `strategy.closedtrades.first_index` | none — the kernel keeps every row | — | `native_open_lots_strategy.cpp` | TradingView drops old rows past a 9000-trade cap and advances `first_index` when it does. The kernel caps nothing, so the first index is always 0 and codegen emits the literal. | | `strategy.closedtrades.entry_id()` | `entry_id` engine.hpp:183 | `strategy_closed_trade_entry_id` pineforge.h:1192 | `native_open_lots_strategy.cpp` | The same string the lot carried as `entry_label` native_host.hpp:333. | -| `strategy.closedtrades.entry_comment()` | `entry_comment` engine.hpp:184 | — | `native_open_lots_strategy.cpp` | No C accessor: `strategy_closed_trade_entry_id` pineforge.h:1192 answers the id, not the comment, and `pf_trade_t` carries no strings. A C host that needs it reads the open lot's `entry_comment` (`pf_native_open_lot_v1` native_c_api.h:1732) before the close. | +| `strategy.closedtrades.entry_comment()` | `entry_comment` engine.hpp:184 | — | `native_open_lots_strategy.cpp` | No C accessor: `strategy_closed_trade_entry_id` pineforge.h:1192 answers the id, not the comment, and `pf_trade_t` carries no strings. A C host that needs it reads the open lot's `entry_comment` (`pf_native_open_lot_v1` native_c_api.h:1739) before the close. | | `strategy.closedtrades.entry_bar_index()` | `entry_bar_index` engine.hpp:181 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | Copied from the lot. | | `strategy.closedtrades.entry_time()` | `entry_time` engine.hpp:173 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | Unix ms. | | `strategy.closedtrades.entry_price()` | `entry_price` engine.hpp:175 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | | @@ -657,12 +658,12 @@ classifies its own rows by order family instead. Which leg closed a row is its | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `request.security()` | `NativeTimeframeSubscription` native_run_spec.hpp:485 in `subscriptions` native_run_spec.hpp:632, or `declare_timeframe_subscriptions` native_host.hpp:1124 inside `on_native_run_begin` native_host.hpp:860 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2897 | `native_htf_strategy.cpp` | A subscription is a series instance: several may share one timeframe. Completed buckets arrive at `on_native_timeframe_bar` native_host.hpp:873 and the latest is pulled with `native_series_bar` native_host.hpp:1107. `authoritative_bars` native_run_spec.hpp:487 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:795 and `pf_native_gaps_e` native_c_api.h:806 (lane E7). The Pine adapter runs its own plain sites through these same subscriptions, ruled **adapter-hook** in ADR-0001's `subscriptions` row. | -| `request.security_lower_tf()` | `NativeAuxiliaryFeed` native_run_spec.hpp:514 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:482 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3168 | `native_auxiliary_feed_strategy.cpp` | Not the same shape: Pine returns an intrabar *array* per bar, the kernel gives you a finer *series* routed by time. A host that wants the raw sub-bars puts them in `IntrabarPath` native_run_spec.hpp:378 and reads `on_native_sub_bar` native_host.hpp:916 instead. | -| `barmerge.gaps_off` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:489 set false | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_HOLD` native_c_api.h:807 | `native_htf_strategy.cpp` | The delivered bucket stands until the next delivery replaces it. | -| `barmerge.gaps_on` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:489 set true | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_CLEAR` native_c_api.h:809 | `native_htf_strategy.cpp` | The series is cleared on every input bar it delivers nothing on, so the pull answers empty — the native spelling of `na`. | -| `barmerge.lookahead_off` | `NativeTimeframeSubscription::lookahead` native_run_spec.hpp:488 set false | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_COMPLETION` native_c_api.h:796 | `native_htf_strategy.cpp` | The default and the honest one: a bucket is delivered when it completes. | -| `barmerge.lookahead_on` | `NativeTimeframeSubscription::lookahead` native_run_spec.hpp:488 set true | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_FIRST_INPUT` native_c_api.h:798 | `tests/test_native_htf_subscriptions.cpp` | Delivers a bucket's final values on the input that held its first bar. It is lookahead: use it to reproduce a chart, never to trade. | +| `request.security()` | `NativeTimeframeSubscription` native_run_spec.hpp:486 in `subscriptions` native_run_spec.hpp:633, or `declare_timeframe_subscriptions` native_host.hpp:1136 inside `on_native_run_begin` native_host.hpp:872 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2905 | `native_htf_strategy.cpp` | A subscription is a series instance: several may share one timeframe. Completed buckets arrive at `on_native_timeframe_bar` native_host.hpp:885 and the latest is pulled with `native_series_bar` native_host.hpp:1119. `authoritative_bars` native_run_spec.hpp:488 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:795 and `pf_native_gaps_e` native_c_api.h:806 (lane E7). The Pine adapter runs its own plain sites through these same subscriptions, ruled **adapter-hook** in ADR-0001's `subscriptions` row. | +| `request.security_lower_tf()` | `NativeAuxiliaryFeed` native_run_spec.hpp:515 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:483 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3182 | `native_auxiliary_feed_strategy.cpp` | Not the same shape: Pine returns an intrabar *array* per bar, the kernel gives you a finer *series* routed by time. A host that wants the raw sub-bars puts them in `IntrabarPath` native_run_spec.hpp:378 and reads `on_native_sub_bar` native_host.hpp:928 instead. | +| `barmerge.gaps_off` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:490 set false | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_HOLD` native_c_api.h:807 | `native_htf_strategy.cpp` | The delivered bucket stands until the next delivery replaces it. | +| `barmerge.gaps_on` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:490 set true | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_CLEAR` native_c_api.h:809 | `native_htf_strategy.cpp` | The series is cleared on every input bar it delivers nothing on, so the pull answers empty — the native spelling of `na`. | +| `barmerge.lookahead_off` | `NativeTimeframeSubscription::lookahead` native_run_spec.hpp:489 set false | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_COMPLETION` native_c_api.h:796 | `native_htf_strategy.cpp` | The default and the honest one: a bucket is delivered when it completes. | +| `barmerge.lookahead_on` | `NativeTimeframeSubscription::lookahead` native_run_spec.hpp:489 set true | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_FIRST_INPUT` native_c_api.h:798 | `tests/test_native_htf_subscriptions.cpp` | Delivers a bucket's final values on the input that held its first bar. It is lookahead: use it to reproduce a chart, never to trade. | | `request.currency_rate()` | none — no FX data feed | `strategy_configure_native_fx_curve_v1` pineforge.h:584 | `native_fx_roll_strategy.cpp` / `tests/test_native_fx_curve.cpp` | The kernel converts with the curve you stage; it fetches nothing. | | `request.dividends()` | none — no fundamentals feed | — | — | Rejected at transpile for a Pine script; a native host fetches and models it itself. | | `request.earnings()` | none — no fundamentals feed | — | — | Same. | @@ -723,9 +724,9 @@ class HelloKernel : public pineforge::NativeStrategyHost { }; ``` -The host is a class; the run is one `NativeRunSpec` native_run_spec.hpp:553 +The host is a class; the run is one `NativeRunSpec` native_run_spec.hpp:554 naming the clock, instrument, account and fees; `configure_native` -native_host.hpp:1184 applies it and `run` engine.hpp:1709 drives the bars. +native_host.hpp:1196 applies it and `run` engine.hpp:1709 drives the bars. **C**, against the same kernel — `examples/native/hello_kernel_c.c:36` is this callback and `examples/native/hello_kernel_c.c:66` the run spec: @@ -743,39 +744,39 @@ static int on_bar(void* user, const pf_bar_t* bar, const pf_native_decision_v1* } ``` -The host is a `pf_native_callbacks_v1` native_c_api.h:2457 table handed to -`strategy_native_host_create_v1` native_c_api.h:2638; the run is +The host is a `pf_native_callbacks_v1` native_c_api.h:2463 table handed to +`strategy_native_host_create_v1` native_c_api.h:2646; the run is `pf_native_run_spec_v1` pineforge.h:548 plus `pf_native_run_spec_ext_v1` -native_c_api.h:2269 through `strategy_configure_native_ext_v1` -native_c_api.h:3108, and `strategy_native_run_v1` native_c_api.h:2651 drives +native_c_api.h:2274 through `strategy_configure_native_ext_v1` +native_c_api.h:3120, and `strategy_native_run_v1` native_c_api.h:2659 drives the bars into a `pf_report_t` pineforge.h:469. Three rules govern the C door: 1. **Every struct is tagged and size-prefixed.** Set `struct_size` - native_c_api.h:2098 and `version` native_c_api.h:2099; a mismatch is refused + native_c_api.h:2103 and `version` native_c_api.h:2104; a mismatch is refused with a documented negative status and mutates nothing. 2. **A callback must not unwind, and must return 0.** A non-zero return latches `NativeFailureCode::CallbackException` native_host.hpp:69 and ends the run - `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2878. + `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2886. 3. **Installing `on_recalculate` replaces `on_bar`.** A C host that installs only the recalculate hook and still expects the bar hook silently trades nothing. Not every C++ capability has a C spelling in 1.0. Five members of the C++ host have none: `prepare_native_begin` (a C host supplies each begin argument itself), -`inspect_current_execution` native_host.hpp:1087 (scheduled for 1.1.0: +`inspect_current_execution` native_host.hpp:1099 (scheduled for 1.1.0: `strategy_native_execute_current_v1` applies the command and is no preview), the two market-only conveniences `submit_market` / `replace_market`, and `native_closed_rows_amended` (a C host cannot write the rows it would name). Every other C++ capability the 1.0 C surface lacks (record fields, hook answers, conveniences, library functions) is a row of the 1.0 C boundary table in @ref native_engine, with its reason and the checker row that pins it. Every policy hook -has a C callback: `validate_execution_precommit` native_host.hpp:948 is -`on_precommit`, `resolve_anchored_level` native_host.hpp:1000 `on_anchored_level`, +has a C callback: `validate_execution_precommit` native_host.hpp:960 is +`on_precommit`, `resolve_anchored_level` native_host.hpp:1012 `on_anchored_level`, the price and opening-shape halves of `resolve_execution_terms` -native_host.hpp:935 `on_execution_terms`, `native_sized_units` -native_host.hpp:1302 `strategy_native_sized_units_v1` and the +native_host.hpp:947 `on_execution_terms`, `native_sized_units` +native_host.hpp:1314 `strategy_native_sized_units_v1` and the `hash_host_extension` seam `on_hash_extension`; only its deprecated spelling `hash_source_extension` has none. What each callback carries is its C struct. The `COVERAGE` comment block at `sha256:71e3e35c1a2409524f511b5ee0e02df496852dc5ff629728a78c5c0401cb8ad0` native_c_api.h:52-55 lists every public member with either its C spelling or that reason, and @@ -821,9 +822,9 @@ spec.fee_value = 0.1; // commission_value spec.max_open_lots = 1; // pyramiding = 1 ``` -Nothing is inferred. `price_tick` native_run_spec.hpp:582 is the ladder the +Nothing is inferred. `price_tick` native_run_spec.hpp:583 is the ladder the next three blocks all measure against, and `fee_value` -native_run_spec.hpp:589 is a **percent**, spelled exactly as Pine's +native_run_spec.hpp:590 is a **percent**, spelled exactly as Pine's `commission_value` is: `0.1` charges 0.1 % of each execution's notional. A fraction there (`0.001`) would charge a hundred times too little. @@ -854,7 +855,7 @@ is set. This strategy never shorts, so its short side simply keeps the ordinary requirement `margin_short = 20` declares, `initial_short = 0.20`. Writing `initial_short = 0.0` there with no `maintenance_short` would state nothing at all, and `configure_native` would refuse the whole spec with -`MarginSideUndeclared` native_run_spec.hpp:760. +`MarginSideUndeclared` native_run_spec.hpp:761. The grid is what makes a bracket level a *ladder* price: `native_price_grid_strategy.cpp` runs one strategy under all four answers and `native_margin_strategy.cpp` drives a real liquidation. @@ -869,10 +870,10 @@ void on_native_run_begin() override { } ``` -`native_series_bar(0)` native_host.hpp:1107 then answers the latest completed +`native_series_bar(0)` native_host.hpp:1119 then answers the latest completed hourly bucket inside any callback — `request.security(…, "60", close)` is `native_series_bar(0)->close`. `native_htf_strategy.cpp` is the runnable -version, including what `gaps` native_run_spec.hpp:489 changes. +version, including what `gaps` native_run_spec.hpp:490 changes. ### 4. The entry: cash, with the fee reserved @@ -971,19 +972,19 @@ closes the stop at `101 - 20 × 0.25 = 96`. 1. **Nothing is implicit.** Pine gets the symbol, session, timezone, tick size and capital from the chart and the `strategy()` call. The spec names all of - them, and `configure_native` native_host.hpp:1184 refuses an incomplete value + them, and `configure_native` native_host.hpp:1196 refuses an incomplete value rather than filling a default. 2. **Submission is not execution.** A request submitted on bar *k* is *accepted* on bar *k* and *filled* at the next eligible point — the open of bar *k+1* under the default `NativeCloseExecution::NextEligiblePoint` native_run_spec.hpp:34. Acceptance and fill are separate rows in - `native_events` native_host.hpp:1317. + `native_events` native_host.hpp:1329. 3. **`strategy.entry` reverses, `Transact` does not.** If the Pine strategy relies on an entry flipping a short into a long, spell it `ReverseTo` native_order.hpp:60. 4. **Most percents become fractions — two stay out of 100.** `margin_long = 20` is `0.20` and `EquityFraction{0.10}` native_order.hpp:97 is ten percent. - The two exceptions keep Pine's own unit: `fee_value` native_run_spec.hpp:589 + The two exceptions keep Pine's own unit: `fee_value` native_run_spec.hpp:590 under `NativeFeeKind::Percent`, so `commission_value = 0.1` is `fee_value = 0.1`, and a `NativeLossLimit` whose `percent` native_run_spec.hpp:261 is set, so `20.0` is twenty percent. @@ -1067,8 +1068,8 @@ the twin harness of `native-feature-parity.md:331`: number shifts the moment one row is added or dropped, which turns a one-trade difference into a whole-list difference. 3. **Then diff the detail.** Event order and kind from `native_events` - native_host.hpp:1317, ownership (which opening a close consumed), and the - continuation hash `native_continuation_hash` native_host.hpp:1358. + native_host.hpp:1329, ownership (which opening a close consumed), and the + continuation hash `native_continuation_hash` native_host.hpp:1370. 4. **Explain every difference against a named row.** §1 of `native-feature-parity.md:42` marks each TradingView-only rule — money rounding, half-tick trigger thresholds, trail conventions, margin-call diff --git a/include/pineforge/native_c_api.h b/include/pineforge/native_c_api.h index 3084ca7e..763fcebf 100644 --- a/include/pineforge/native_c_api.h +++ b/include/pineforge/native_c_api.h @@ -928,7 +928,7 @@ typedef enum pf_native_margin_check_kind_e { PF_NATIVE_MARGIN_CHECK_BAR_OPEN = 0, /**< The script bar's open. */ PF_NATIVE_MARGIN_CHECK_AFTER_APPLIED = 1, /**< The re-arm after a point's fills. */ PF_NATIVE_MARGIN_CHECK_CALCULATION = 2, /**< A CalculationOnly model's calculation. */ - PF_NATIVE_MARGIN_CHECK_FX_ROLL = 3 /**< A step of the run's declared + PF_NATIVE_MARGIN_CHECK_FX_ROLL = 3, /**< A step of the run's declared * #pf_native_fx_curve_v1: the first point * the account converts at a new rate, * offered immediately before that point is @@ -936,6 +936,13 @@ typedef enum pf_native_margin_check_kind_e { * none, and a CalculationOnly model, which * measures at its calculation alone, is not * offered it. */ + PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE = 4 /**< A delivered sample, after the + * script bar's first, of a + * #PF_NATIVE_INTRABAR_LOWER_TF path matched as + * continuous segments, measured at its own + * price immediately before it is matched. A + * one-price distribution path and a + * CalculationOnly model are not offered it. */ } pf_native_margin_check_kind_t; /** What an ANSWERING callback's return value means. diff --git a/include/pineforge/native_host.hpp b/include/pineforge/native_host.hpp index 80073641..d827af54 100644 --- a/include/pineforge/native_host.hpp +++ b/include/pineforge/native_host.hpp @@ -525,8 +525,19 @@ struct NativeMarginCallView { /// Which kernel check point is about to test the maintenance requirement. /// BarOpen is the script bar's open, after on_native_bar_open and before the /// bar's own matching; AfterApplied is the re-arm that follows a point's -/// applied fills, which is the kernel's only mid-path check; Calculation is -/// the script calculation of a CalculationOnly model; FxRoll is a step of the +/// applied fills, the only mid-path check of a bar walked on its modeled +/// waypoints; IntrabarSample is every later delivered sample of a run whose +/// intrabar path is matched as continuous segments (IntrabarPath::lower_tf +/// with SampleEligibility::ContinuousSegments), which has no whole-bar +/// waypoint model: each is measured at its own price immediately before it +/// is matched, as the bar's first sample is at BarOpen, so a breach inside the +/// bar rests its liquidation at the sample that crossed and the path reaches +/// it there. A one-price distribution path (synthesized, or +/// DistributionSamples) is not offered it -- a request armed at a discrete +/// point is matched only at a later one -- and is checked at its first +/// sample and after fills; +/// Calculation is the script calculation of a CalculationOnly model; FxRoll is +/// a step of the /// run's declared NativeFxCurve: the first driver point the account converts /// at a different rate than the point before it, offered immediately before /// that point is matched, with the price where the walk left it -- the @@ -540,6 +551,7 @@ enum class NativeMarginCheckKind : std::uint32_t { AfterApplied = 1, Calculation = 2, FxRoll = 3, + IntrabarSample = 4, }; /// Ephemeral factual view of one kernel check point, offered to the host diff --git a/include/pineforge/native_run_spec.hpp b/include/pineforge/native_run_spec.hpp index 84516b90..a8b276c8 100644 --- a/include/pineforge/native_run_spec.hpp +++ b/include/pineforge/native_run_spec.hpp @@ -387,9 +387,10 @@ struct IntrabarPath { }; /// A RETAINED finer feed: the host's own lower-timeframe bars, the literal they /// are at, and the sampling policy over them. It is the only alternative that has - /// sub-bars of its own, so it is what makes on_native_sub_bar reachable, and it is - /// what gives the margin model a delivered sample to re-evaluate at instead of a - /// whole-bar waypoint. C spelling: PF_NATIVE_INTRABAR_LOWER_TF. + /// sub-bars of its own, so it is what makes on_native_sub_bar reachable, and, + /// matched as ContinuousSegments, it is what gives the margin model a delivered + /// sample to re-evaluate at instead of a whole-bar waypoint + /// (NativeMarginCheckKind::IntrabarSample). C spelling: PF_NATIVE_INTRABAR_LOWER_TF. struct lower_tf { std::vector bars; std::string tf; diff --git a/include/pineforge/source/pine_adapter.hpp b/include/pineforge/source/pine_adapter.hpp index 67522962..551bd7cd 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -1383,6 +1383,7 @@ class PineExecutionAdapter { bool source_margin_rounded_tie_veto() const; bool margin_check_allowed(const NativeMarginCheckPoint&) const; + bool intrabar_sample_checked(const NativeMarginCheckPoint&) const; std::optional resolve_margin_requirement( const NativeMarginRequirementView&) const; std::optional resolve_margin_call_units(const NativeMarginCallView&) const; diff --git a/scripts/test_check_native_c_api_surface.py b/scripts/test_check_native_c_api_surface.py index 6d5a8ae5..aafdf739 100644 --- a/scripts/test_check_native_c_api_surface.py +++ b/scripts/test_check_native_c_api_surface.py @@ -298,11 +298,13 @@ def test_the_copied_tree_passes(self) -> None: self.assertEqual(code, 0, err) def test_an_appended_kernel_enumerator_without_a_c_name_fails(self) -> None: - # The audit's R12 mutation, verbatim: a kind appended after FxRoll. - self.edit("native_host.hpp", " FxRoll = 3,\n};", " FxRoll = 3,\n Unnamed = 4,\n};") + # The audit's R12 mutation: a kind appended after the last one (FxRoll + # then, IntrabarSample since R5 lane PAR-MARGIN). + self.edit("native_host.hpp", " IntrabarSample = 4,\n};", + " IntrabarSample = 4,\n Unnamed = 5,\n};") code, err = self.run_guard() self.assertEqual(code, 1) - self.assertIn("NativeMarginCheckKind::Unnamed (4) has no C name in " + self.assertIn("NativeMarginCheckKind::Unnamed (5) has no C name in " "pf_native_margin_check_kind_e", err) def test_an_implicit_enumerator_appended_without_a_c_name_fails(self) -> None: @@ -312,12 +314,13 @@ def test_an_implicit_enumerator_appended_without_a_c_name_fails(self) -> None: self.assertIn("ActivationKind::Later (4) has no C name in pf_native_activation_e", err) def test_an_inserted_kernel_enumerator_fails(self) -> None: - # Inserted before the last: the C names now spell shifted values. - self.edit("native_host.hpp", " Calculation = 2,\n FxRoll = 3,", - " Calculation = 2,\n Inserted = 3,\n FxRoll = 4,") + # Inserted before the last two: the C names now spell shifted values, + # and the last one's has no C name at all. + self.edit("native_host.hpp", " Calculation = 2,\n FxRoll = 3,\n IntrabarSample = 4,", + " Calculation = 2,\n Inserted = 3,\n FxRoll = 4,\n IntrabarSample = 5,") code, err = self.run_guard() self.assertEqual(code, 1) - self.assertIn("NativeMarginCheckKind::FxRoll (4) has no C name", err) + self.assertIn("NativeMarginCheckKind::IntrabarSample (5) has no C name", err) def test_a_c_name_for_no_kernel_value_fails(self) -> None: self.edit("native_c_api.h", " PF_NATIVE_ORIGIN_KERNEL_RISK = 2 ", diff --git a/src/native_c_host.cpp b/src/native_c_host.cpp index 623a7689..14a82b6b 100644 --- a/src/native_c_host.cpp +++ b/src/native_c_host.cpp @@ -356,6 +356,8 @@ static_assert(static_cast(pineforge::NativeMarginCheckKind::Calculation) * c_word() make each name its kernel value. */ static_assert(static_cast(pineforge::NativeMarginCheckKind::FxRoll) == PF_NATIVE_MARGIN_CHECK_FX_ROLL, "NativeMarginCheckKind drifted"); +static_assert(static_cast(pineforge::NativeMarginCheckKind::IntrabarSample) + == PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE, "NativeMarginCheckKind drifted"); /* The hook tails are append-only: the base layout must still end exactly * where PF_NATIVE_CALLBACKS_V1_BASE_SIZE says, the six-hook tail must be the * seven function pointers below it, and the policy-hook tail the four after @@ -1024,6 +1026,7 @@ constexpr std::uint32_t c_word(pineforge::NativeMarginCheckKind value) noexcept case V::AfterApplied: return PF_NATIVE_MARGIN_CHECK_AFTER_APPLIED; case V::Calculation: return PF_NATIVE_MARGIN_CHECK_CALCULATION; case V::FxRoll: return PF_NATIVE_MARGIN_CHECK_FX_ROLL; + case V::IntrabarSample: return PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE; } return static_cast(value); } diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index 13ab839d..05372f01 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -3121,7 +3121,8 @@ bool NativeExecutionConsumer::margin_check_admitted( // The most adverse price the modeled script path still reaches after `phase`, // including the cursor price itself so a point with no remaining waypoint // still has a finite mark. A run with an intrabar path has no whole-bar -// waypoint model here: it re-evaluates at each delivered sample instead. +// waypoint model here: it re-evaluates at each delivered sample of a +// continuous path instead (NativeMarginCheckKind::IntrabarSample). double NativeExecutionConsumer::margin_sizing_price( bool short_side, NativePathPhase phase, double fallback) const noexcept { if (!has_margin_path_) return fallback; @@ -7553,7 +7554,9 @@ void NativeExecutionConsumer::deliver_intrabar_script( } // A sampled intrabar path re-evaluates the margin model at each delivered - // sample instead of at the containing bar's remaining waypoints. + // sample of a continuous path (IntrabarSample, below) instead of at the + // containing bar's remaining waypoints; a one-price distribution path, at + // its first sample and after fills. has_margin_path_ = false; // The script bar's sub-bars and one sub-bar's samples, in consumer-owned // buffers whose capacity outlives the bar (R5 lane PERF-L1): a lower-path @@ -7714,6 +7717,19 @@ void NativeExecutionConsumer::deliver_intrabar_script( risk_evaluate(engine, point_frame_view(point), CallbackPhase::PreOpen); if (failed()) return; } + } else if (!distribution_samples) { + // No whole-bar waypoint model here, so every later sample of a + // continuous path is a check point of its own, measured at its + // price before it is matched: a mark-check liquidation rests at + // that sample, and the segment into it (or, at a sub-bar's + // open, the segment out of it) reaches it there. A one-price + // distribution sample is not offered one: a request armed at a + // discrete point is matched only at a later point, so a + // liquidation re-armed at every sample would never be. + maintain_margin_liquidation( + engine, make_cursor(point, 0.0), point.coordinate.path_phase, price, + NativeMarginCheckKind::IntrabarSample); + if (failed()) return; } if (distribution_samples || sample_index == 0) { match_discrete(engine, point); diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 162f3317..37f17fa6 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -14194,6 +14194,15 @@ bool PineExecutionAdapter::margin_check_allowed( // Refused by kind, ahead of the ordinal test, because a roll can share // its driver point with an armed BarOpen check. if (point.kind == NativeMarginCheckKind::FxRoll) return false; + // A magnified run's later samples. TradingView's magnified broker checks + // the book at every lower-timeframe bar and books the call at the first + // sub-bar low that crosses, sized there, then again at each later one + // that crosses the reduced book's line (lab tv tapes + // tests/fixtures/intrabar_margin/pm-i3-eth-mag-*, R5 lane PAR-MARGIN); + // the kernel's IntrabarSample point is that check. + if (point.kind == NativeMarginCheckKind::IntrabarSample) { + return intrabar_sample_checked(point) && !source_margin_rounded_tie_veto(); + } // The kernel's BarOpen point and its post-fill AfterApplied re-arm are // both points TradingView checks at -- schedule_margin_call_path is // called from on_bar_open and from on_applied -- so the admitted point is @@ -14216,6 +14225,25 @@ bool PineExecutionAdapter::margin_check_allowed( return !source_margin_rounded_tie_veto(); } +// Where the per-sample check is TradingView's as measured: a plain magnified +// run (the tapes run neither process_orders_on_close nor calc_on_order_fills) +// holding a leveraged book -- a full-margin book keeps its own checkpoints, +// unmeasured under the magnifier -- and no slice of the adapter's own resting +// (the pre-open slice keeps its verdict for its bar, as on the chart path). +// A flat book is admitted: the kernel then withdraws what it had resting. +bool PineExecutionAdapter::intrabar_sample_checked( + const NativeMarginCheckPoint& point) const { + if (!bar_magnifier_ || config_.process_orders_on_close || config_.calc_on_order_fills) + return false; + const double held = point.position.signed_units; + if (held == 0.0) return true; + const double margin = held > 0.0 ? config_.margin_long : config_.margin_short; + if (!(margin < 100.0)) return false; + return !any_live_row(live_handles_, placement_, [](const PlacementSnapshot& row) { + return row.family == PineOrderFamily::Margin; + }); +} + // TradingView's money (MG-A + MG-B), restated for the kernel's breach test at // the mark it measured. nullopt would keep the kernel's own unrounded // requirement against its own marked equity, which is a different broker. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 92dae3df..c0a7f236 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -857,6 +857,10 @@ list(APPEND TEST_SOURCES test_native_session_day_facts) # Wave G KERNEL-EDGE: one source-free row with fail-before witnesses for FX # rolls, session-day neighbours, final calendar-closed buckets and host marks. list(APPEND TEST_SOURCES test_native_kernel_edge) +# R5 lane PAR-MARGIN (item 3): the margin model on an intrabar path is offered +# NativeMarginCheckKind::IntrabarSample at every delivered sample after the +# bar's first. Source-free, so it also runs in the kernel-only profile. +list(APPEND TEST_SOURCES test_native_margin_intrabar_samples) # R5 F5: the Pine adapter selects those facts instead of computing its own: # the same tapes flag for flag through the adapter, its live-probe tail diff --git a/tests/fixtures/intrabar_margin/README.md b/tests/fixtures/intrabar_margin/README.md new file mode 100644 index 00000000..9dc47243 --- /dev/null +++ b/tests/fixtures/intrabar_margin/README.md @@ -0,0 +1,38 @@ +# Intrabar margin tapes (R5 lane PAR-MARGIN, item 3) + +Does TradingView check the margin model at each intrabar sample under the bar +magnifier? Each pair is one `lab tv` export on BINANCE:ETHUSDT.P 15m +(`ws-report-v1`, `rangeProof: covered`), byte-identical, of the same script with +`use_bar_magnifier` on (`pm-i3-eth-mag-*`) and off (`pm-i3-eth-chart-*`): one +leveraged long (margin 20, 4.6-4.7 x equity) placed on a signal bar, filled at +the next bar's open and carried into a bar that first crosses the maintenance +line at an early 1-minute low and reaches a deeper low later in the same 15m +bar; closed 3 bars after its entry bar. Times in `tv_trades.csv` are UTC+8; +TradingView dates a magnified fill at its chart bar's open. + +- Magnifier off: one margin call, at the chart bar's low (4 of 4). +- Magnifier on: the first call at the first 1-minute low that crosses, sized + there (4 of 4), and a further call at each later 1-minute low that crosses the + reduced book's line -- every row on 0622, 0824 and 0406. On 0130 TradingView + books its second call at the 01:43 low (2673.33) on the book the first call + left, where a check at the 01:42 low (2690, which crosses that book's line) + would have booked one a minute earlier; the export repeats over three other + windows. Recorded, open. + +`bars_1m.inc` holds each event's 1-minute bars from the corpus feed; +`pm-i3-eth-ltf-lows` prints TradingView's own 1-minute lows inside the two +multi-call bars through `request.security_lower_tf`, and they equal the feed's. +`tests/test_adapter_margin_schedule_differential.cpp` (section "intrabar margin +on tapes") replays every pair. Campaign notes: `tv-tape--`. + +| probe | window | TradingView | tvTradesCsvHash | +|---|---|---|---| +| `pm-i3-eth-mag-0622` | 2025-06-20 .. 2025-06-24 | margin calls 0.1052 @2223.05, 0.0276 @2198.19, 0.1688 @2180 | `2488da65fe9bd341` | +| `pm-i3-eth-chart-0622` | 2025-06-20 .. 2025-06-24 | margin calls 1.064 @2159.28 | `714f4333cebd50c8` | +| `pm-i3-eth-mag-0130` | 2026-01-28 .. 2026-02-01 | margin calls 0.1088 @2760, 0.4824 @2673.33 | `ac0fb36013b01d07` | +| `pm-i3-eth-chart-0130` | 2026-01-28 .. 2026-02-01 | margin calls 0.988 @2673.33 | `2f0f39543f3d42d9` | +| `pm-i3-eth-mag-0824` | 2025-08-22 .. 2025-08-26 | margin calls 0.1224 @4820 | `f0e1b58462313b44` | +| `pm-i3-eth-chart-0824` | 2025-08-22 .. 2025-08-26 | margin calls 0.3784 @4740.72 | `982df67044dacfa5` | +| `pm-i3-eth-mag-0406` | 2025-04-04 .. 2025-04-08 | margin calls 0.418 @1574.55 | `40208b4aa509bd58` | +| `pm-i3-eth-chart-0406` | 2025-04-04 .. 2025-04-08 | margin calls 1.0776 @1552.5 | `ea61c13201510514` | +| `pm-i3-eth-ltf-lows` | 2025-06-20 .. 2026-02-01 | the 1m lows inside 2025-06-22 13:15 and 2026-01-30 01:30 (UTC), printed as entry comments | `ad312f3e1202becf` | diff --git a/tests/fixtures/intrabar_margin/bars_1m.inc b/tests/fixtures/intrabar_margin/bars_1m.inc new file mode 100644 index 00000000..c3023f2c --- /dev/null +++ b/tests/fixtures/intrabar_margin/bars_1m.inc @@ -0,0 +1,373 @@ +// ETH 1-minute bars per item-3 event (R5 lane PAR-MARGIN), from the corpus feed +// corpus/data/ohlcv_ETH-USDT-USDT_1m.csv (the magnifier's lower timeframe on a 15m +// BINANCE:ETHUSDT.P chart): the signal bar through the entry bar + 4, 90 bars each. +// TradingView's own 1m lows inside the two multi-call bars (pm-i3-eth-ltf-lows) +// equal these. {timestamp_ms, open, high, low, close} +constexpr FeedBar kI3_0622[90] = { // signal bar 2025-06-22 12:45 UTC + {1750596300000LL, 2270.01, 2270.01, 2268.09, 2268.82}, + {1750596360000LL, 2268.82, 2269.4, 2266.79, 2269.4}, + {1750596420000LL, 2269.4, 2270, 2268.41, 2269.98}, + {1750596480000LL, 2269.98, 2272.48, 2269.98, 2272.48}, + {1750596540000LL, 2272.48, 2274.45, 2271.99, 2272.48}, + {1750596600000LL, 2272.48, 2272.87, 2270.8, 2271}, + {1750596660000LL, 2271, 2273.33, 2270.5, 2272.44}, + {1750596720000LL, 2272.42, 2275.49, 2271.37, 2274.82}, + {1750596780000LL, 2274.81, 2277.44, 2274.67, 2276.88}, + {1750596840000LL, 2276.88, 2278.41, 2276.02, 2276.29}, + {1750596900000LL, 2276.28, 2279.07, 2276.28, 2279.06}, + {1750596960000LL, 2279.06, 2279.38, 2278.1, 2278.95}, + {1750597020000LL, 2278.95, 2279.15, 2277.47, 2278.11}, + {1750597080000LL, 2278.11, 2279.86, 2277.74, 2278.48}, + {1750597140000LL, 2278.47, 2279.71, 2278.1, 2278.81}, + {1750597200000LL, 2278.8, 2280.84, 2278.28, 2279.85}, + {1750597260000LL, 2279.86, 2282.97, 2279.25, 2282.69}, + {1750597320000LL, 2282.68, 2284.17, 2281.62, 2282.09}, + {1750597380000LL, 2282.1, 2283.07, 2278.37, 2278.65}, + {1750597440000LL, 2278.64, 2279.03, 2276.06, 2278.82}, + {1750597500000LL, 2278.83, 2281.65, 2278.34, 2280.5}, + {1750597560000LL, 2280.5, 2281.16, 2276.61, 2276.85}, + {1750597620000LL, 2276.85, 2278, 2274.56, 2275.13}, + {1750597680000LL, 2275.13, 2276, 2274.05, 2275.98}, + {1750597740000LL, 2275.98, 2276.67, 2274.35, 2274.73}, + {1750597800000LL, 2274.73, 2274.74, 2267.26, 2268.67}, + {1750597860000LL, 2268.67, 2269.73, 2264.38, 2266.65}, + {1750597920000LL, 2266.65, 2266.65, 2257.71, 2261.01}, + {1750597980000LL, 2261.01, 2264.72, 2258, 2263.52}, + {1750598040000LL, 2263.53, 2263.9, 2260.49, 2260.57}, + {1750598100000LL, 2260.57, 2261.44, 2254.45, 2256.65}, + {1750598160000LL, 2256.66, 2257.8, 2252, 2253.48}, + {1750598220000LL, 2253.49, 2254.41, 2248.15, 2253.05}, + {1750598280000LL, 2253.06, 2253.46, 2247.25, 2248.89}, + {1750598340000LL, 2248.88, 2253.87, 2246.14, 2246.7}, + {1750598400000LL, 2246.7, 2248.48, 2237.77, 2241.33}, + {1750598460000LL, 2241.33, 2242.81, 2223.05, 2227.97}, + {1750598520000LL, 2227.96, 2231.66, 2225.39, 2229.12}, + {1750598580000LL, 2229.13, 2236.16, 2223.6, 2229.39}, + {1750598640000LL, 2229.4, 2233.4, 2225, 2228.5}, + {1750598700000LL, 2228.5, 2229.54, 2198.19, 2214.3}, + {1750598760000LL, 2214.3, 2214.41, 2201.5, 2208.11}, + {1750598820000LL, 2208.12, 2211.87, 2180, 2190.13}, + {1750598880000LL, 2190.13, 2193.74, 2159.28, 2190.87}, + {1750598940000LL, 2190.8, 2199.16, 2182.22, 2194.81}, + {1750599000000LL, 2194.8, 2197.34, 2181.16, 2184.22}, + {1750599060000LL, 2184.22, 2202.28, 2183.11, 2196.22}, + {1750599120000LL, 2196.22, 2206, 2193.48, 2200.61}, + {1750599180000LL, 2200.58, 2206.87, 2198.61, 2198.87}, + {1750599240000LL, 2198.86, 2202.75, 2196.14, 2197.93}, + {1750599300000LL, 2197.93, 2205.4, 2195.2, 2195.2}, + {1750599360000LL, 2195.2, 2201.09, 2190.67, 2196.75}, + {1750599420000LL, 2196.75, 2201.19, 2192.77, 2196.79}, + {1750599480000LL, 2196.79, 2202.2, 2193.01, 2194.44}, + {1750599540000LL, 2194.44, 2199.8, 2193.35, 2193.79}, + {1750599600000LL, 2193.8, 2197.05, 2186, 2196.69}, + {1750599660000LL, 2196.7, 2197.81, 2188.8, 2191.99}, + {1750599720000LL, 2191.98, 2194.58, 2186.15, 2186.93}, + {1750599780000LL, 2187.01, 2193.36, 2175.58, 2186.77}, + {1750599840000LL, 2186.74, 2188.12, 2177.25, 2181.82}, + {1750599900000LL, 2181.83, 2188.78, 2179.5, 2183.72}, + {1750599960000LL, 2183.62, 2194.18, 2183.28, 2194.18}, + {1750600020000LL, 2194.18, 2194.18, 2186.24, 2186.63}, + {1750600080000LL, 2186.64, 2198.99, 2186.17, 2192.9}, + {1750600140000LL, 2192.91, 2204.64, 2192.17, 2201.89}, + {1750600200000LL, 2201.89, 2205.06, 2198.41, 2205}, + {1750600260000LL, 2205, 2205.55, 2199.27, 2200.28}, + {1750600320000LL, 2200.28, 2202.8, 2194.54, 2194.6}, + {1750600380000LL, 2194.59, 2199.26, 2192.73, 2195.1}, + {1750600440000LL, 2195.11, 2199.66, 2193.26, 2197.22}, + {1750600500000LL, 2197.21, 2202.16, 2196.22, 2200.57}, + {1750600560000LL, 2200.57, 2201.01, 2196.89, 2199.04}, + {1750600620000LL, 2199.04, 2201.59, 2198.67, 2199.97}, + {1750600680000LL, 2199.96, 2205, 2197.61, 2201.41}, + {1750600740000LL, 2201.4, 2201.79, 2198.26, 2198.9}, + {1750600800000LL, 2198.9, 2200.35, 2196.55, 2199.15}, + {1750600860000LL, 2199.15, 2201.44, 2197.29, 2201.44}, + {1750600920000LL, 2201.45, 2207.9, 2199.26, 2205.81}, + {1750600980000LL, 2205.82, 2207.55, 2202.9, 2205.82}, + {1750601040000LL, 2205.82, 2212, 2202, 2202.47}, + {1750601100000LL, 2202.47, 2205.08, 2196.34, 2196.43}, + {1750601160000LL, 2196.43, 2201.74, 2195.35, 2201.62}, + {1750601220000LL, 2201.61, 2201.86, 2195.39, 2197.26}, + {1750601280000LL, 2197.26, 2200.58, 2196.01, 2199.91}, + {1750601340000LL, 2199.94, 2203.2, 2198.6, 2203.04}, + {1750601400000LL, 2203.06, 2205.17, 2200.08, 2201.61}, + {1750601460000LL, 2201.6, 2201.65, 2194.22, 2194.6}, + {1750601520000LL, 2194.59, 2198.74, 2193.05, 2194.7}, + {1750601580000LL, 2194.7, 2195.6, 2190.43, 2192.08}, + {1750601640000LL, 2192.07, 2193.93, 2188.64, 2189.86}, +}; +constexpr FeedBar kI3_0130[90] = { // signal bar 2026-01-30 01:00 UTC + {1769734800000LL, 2803.79, 2804.04, 2803.13, 2803.48}, + {1769734860000LL, 2803.48, 2803.59, 2801.38, 2801.85}, + {1769734920000LL, 2801.85, 2803.14, 2801.23, 2801.41}, + {1769734980000LL, 2801.41, 2803.31, 2801.41, 2802.9}, + {1769735040000LL, 2802.91, 2803.46, 2800.24, 2803.46}, + {1769735100000LL, 2803.46, 2807.43, 2803.46, 2806.68}, + {1769735160000LL, 2806.69, 2808, 2806.68, 2807.32}, + {1769735220000LL, 2807.32, 2809.9, 2807.26, 2809.44}, + {1769735280000LL, 2809.44, 2810.2, 2806.94, 2810.06}, + {1769735340000LL, 2810.07, 2811.5, 2809.62, 2809.75}, + {1769735400000LL, 2809.74, 2810, 2809.11, 2809.77}, + {1769735460000LL, 2809.77, 2810.73, 2806.35, 2807.39}, + {1769735520000LL, 2807.38, 2808.96, 2806.93, 2807.91}, + {1769735580000LL, 2807.92, 2810.14, 2807.84, 2809.81}, + {1769735640000LL, 2809.81, 2810.28, 2809.33, 2809.34}, + {1769735700000LL, 2809.34, 2812.13, 2809.08, 2811.49}, + {1769735760000LL, 2811.49, 2812, 2810.75, 2811.38}, + {1769735820000LL, 2811.37, 2811.38, 2809.39, 2809.4}, + {1769735880000LL, 2809.39, 2809.39, 2804.74, 2808.02}, + {1769735940000LL, 2808.01, 2808.02, 2806.55, 2807.34}, + {1769736000000LL, 2807.33, 2809.6, 2804.84, 2804.85}, + {1769736060000LL, 2804.84, 2805.19, 2796.9, 2797.74}, + {1769736120000LL, 2797.73, 2799.9, 2792.09, 2793.03}, + {1769736180000LL, 2793.04, 2795.33, 2788.38, 2792.63}, + {1769736240000LL, 2792.63, 2801.48, 2792.63, 2801.46}, + {1769736300000LL, 2801.46, 2808, 2800.4, 2804.32}, + {1769736360000LL, 2804.33, 2805.94, 2801.23, 2802.06}, + {1769736420000LL, 2802.06, 2803, 2801.23, 2802.35}, + {1769736480000LL, 2802.36, 2802.6, 2800.95, 2801.55}, + {1769736540000LL, 2801.55, 2802.18, 2798.94, 2799.17}, + {1769736600000LL, 2799.17, 2799.18, 2793.76, 2797.28}, + {1769736660000LL, 2797.28, 2804.77, 2796.67, 2804.4}, + {1769736720000LL, 2804.4, 2804.41, 2801.12, 2802.47}, + {1769736780000LL, 2802.47, 2805, 2802.47, 2805}, + {1769736840000LL, 2805, 2807.46, 2802.72, 2802.8}, + {1769736900000LL, 2802.81, 2803.4, 2796.95, 2798.84}, + {1769736960000LL, 2798.84, 2801.68, 2796.78, 2797.31}, + {1769737020000LL, 2797.31, 2797.31, 2782.69, 2783.58}, + {1769737080000LL, 2783.58, 2789.76, 2782.63, 2789.75}, + {1769737140000LL, 2789.76, 2790.61, 2760, 2774.34}, + {1769737200000LL, 2774.33, 2779.63, 2763.35, 2772.36}, + {1769737260000LL, 2772.36, 2773.13, 2727.51, 2742.74}, + {1769737320000LL, 2742.75, 2751.87, 2690, 2691.35}, + {1769737380000LL, 2691.35, 2716.01, 2673.33, 2715.36}, + {1769737440000LL, 2715.36, 2732.44, 2707.28, 2723.71}, + {1769737500000LL, 2723.7, 2733.59, 2715, 2723.41}, + {1769737560000LL, 2723.41, 2734.29, 2719.85, 2722.2}, + {1769737620000LL, 2722.21, 2732.02, 2716.02, 2725.23}, + {1769737680000LL, 2725.24, 2730.98, 2721.74, 2723.28}, + {1769737740000LL, 2723.29, 2730.23, 2721.74, 2727.9}, + {1769737800000LL, 2727.91, 2738.56, 2727.9, 2734}, + {1769737860000LL, 2734.01, 2744.71, 2730.99, 2733.95}, + {1769737920000LL, 2733.92, 2735.17, 2730.13, 2731.69}, + {1769737980000LL, 2731.68, 2745.35, 2729.3, 2740.36}, + {1769738040000LL, 2740.36, 2747.9, 2740.35, 2744.27}, + {1769738100000LL, 2744.28, 2747.95, 2740.15, 2746.94}, + {1769738160000LL, 2746.95, 2751, 2744.96, 2746.6}, + {1769738220000LL, 2746.63, 2746.81, 2741.45, 2742.33}, + {1769738280000LL, 2742.33, 2749.13, 2741.02, 2747.5}, + {1769738340000LL, 2747.5, 2748.89, 2745.1, 2745.65}, + {1769738400000LL, 2745.65, 2748.91, 2743.19, 2743.56}, + {1769738460000LL, 2743.57, 2744.83, 2741.64, 2742.57}, + {1769738520000LL, 2742.57, 2745.34, 2739.19, 2744.94}, + {1769738580000LL, 2744.94, 2749.9, 2741, 2742.4}, + {1769738640000LL, 2742.39, 2746.61, 2739.01, 2739.11}, + {1769738700000LL, 2739.11, 2741.87, 2732.16, 2737.87}, + {1769738760000LL, 2737.87, 2739.55, 2732.23, 2732.4}, + {1769738820000LL, 2732.4, 2735.53, 2728.03, 2732.39}, + {1769738880000LL, 2732.4, 2735.65, 2732, 2732.01}, + {1769738940000LL, 2732.01, 2734.76, 2729.06, 2729.34}, + {1769739000000LL, 2729.35, 2732.62, 2726.85, 2729.35}, + {1769739060000LL, 2729.36, 2734.62, 2728.84, 2733.82}, + {1769739120000LL, 2733.82, 2734.58, 2731, 2731.01}, + {1769739180000LL, 2731.01, 2737.06, 2730.62, 2736.66}, + {1769739240000LL, 2736.66, 2736.68, 2728.07, 2728.59}, + {1769739300000LL, 2728.59, 2734.98, 2728.42, 2733.08}, + {1769739360000LL, 2733.09, 2733.63, 2722, 2725.96}, + {1769739420000LL, 2725.96, 2727.1, 2717.99, 2725.81}, + {1769739480000LL, 2725.81, 2733.76, 2724.84, 2726.44}, + {1769739540000LL, 2726.44, 2727.22, 2722.43, 2723.11}, + {1769739600000LL, 2723.12, 2724.87, 2718.76, 2719.29}, + {1769739660000LL, 2719.3, 2723.28, 2713.63, 2719.26}, + {1769739720000LL, 2719.26, 2721.98, 2716.3, 2717.2}, + {1769739780000LL, 2717.21, 2720.69, 2713.53, 2719.4}, + {1769739840000LL, 2719.39, 2719.39, 2704.02, 2706.48}, + {1769739900000LL, 2706.49, 2712.92, 2704.36, 2710.96}, + {1769739960000LL, 2710.98, 2712.24, 2704.02, 2706.67}, + {1769740020000LL, 2706.67, 2709.46, 2699, 2699.15}, + {1769740080000LL, 2699.2, 2709.69, 2696.61, 2709.1}, + {1769740140000LL, 2709.11, 2714.75, 2704.34, 2714.43}, +}; +constexpr FeedBar kI3_0824[90] = { // signal bar 2025-08-24 19:00 UTC + {1756062000000LL, 4932.85, 4935.8, 4931.37, 4934.78}, + {1756062060000LL, 4934.79, 4941.82, 4934.78, 4940.63}, + {1756062120000LL, 4940.63, 4940.64, 4933.99, 4934.05}, + {1756062180000LL, 4934.05, 4937.84, 4934.05, 4936.16}, + {1756062240000LL, 4936.15, 4939, 4936.15, 4938.05}, + {1756062300000LL, 4938.04, 4940, 4933.55, 4939.38}, + {1756062360000LL, 4939.38, 4939.82, 4933.87, 4937.3}, + {1756062420000LL, 4937.3, 4946.58, 4937.3, 4944.25}, + {1756062480000LL, 4944.24, 4950.88, 4943.56, 4945.63}, + {1756062540000LL, 4945.66, 4953, 4943.03, 4950}, + {1756062600000LL, 4950.01, 4953.2, 4948.55, 4950.98}, + {1756062660000LL, 4950.98, 4952, 4945.23, 4945.52}, + {1756062720000LL, 4945.51, 4956.72, 4944.74, 4951.34}, + {1756062780000LL, 4951.34, 4951.69, 4933.81, 4934.57}, + {1756062840000LL, 4934.57, 4940, 4931.9, 4939.7}, + {1756062900000LL, 4939.71, 4942.52, 4938.81, 4942.52}, + {1756062960000LL, 4942.51, 4949.43, 4942.51, 4948.09}, + {1756063020000LL, 4948.1, 4948.91, 4943.97, 4948.72}, + {1756063080000LL, 4948.72, 4951.47, 4948.29, 4949.32}, + {1756063140000LL, 4949.31, 4951.41, 4942.26, 4945.54}, + {1756063200000LL, 4945.54, 4949.65, 4942.12, 4949.64}, + {1756063260000LL, 4949.64, 4954.54, 4948.26, 4951.22}, + {1756063320000LL, 4951.23, 4954.78, 4950.23, 4953}, + {1756063380000LL, 4953, 4954.97, 4950.81, 4951.26}, + {1756063440000LL, 4951.25, 4951.25, 4942.48, 4943.2}, + {1756063500000LL, 4943.19, 4945.56, 4936.47, 4939.18}, + {1756063560000LL, 4939.17, 4939.43, 4933.89, 4935.01}, + {1756063620000LL, 4935, 4938.91, 4932.4, 4934.31}, + {1756063680000LL, 4934.3, 4938.03, 4933.4, 4936.35}, + {1756063740000LL, 4936.35, 4936.41, 4932.48, 4936.07}, + {1756063800000LL, 4936.07, 4937.61, 4929.17, 4929.65}, + {1756063860000LL, 4929.64, 4930.25, 4917.93, 4920.74}, + {1756063920000LL, 4920.74, 4921.15, 4906, 4908.24}, + {1756063980000LL, 4908.23, 4909, 4898.48, 4899.35}, + {1756064040000LL, 4899.35, 4899.36, 4820, 4863.65}, + {1756064100000LL, 4863.64, 4873.35, 4846.85, 4849.05}, + {1756064160000LL, 4849.06, 4850.99, 4800, 4803.99}, + {1756064220000LL, 4804.23, 4808.35, 4740.72, 4779.79}, + {1756064280000LL, 4779.79, 4822.15, 4774.12, 4816.51}, + {1756064340000LL, 4816.5, 4857.76, 4815.67, 4834.89}, + {1756064400000LL, 4834.9, 4840, 4811, 4811.67}, + {1756064460000LL, 4811.67, 4823.18, 4794.33, 4804.8}, + {1756064520000LL, 4804.79, 4818.78, 4802.38, 4814.21}, + {1756064580000LL, 4814.2, 4831.47, 4813.43, 4823.81}, + {1756064640000LL, 4823.91, 4849.5, 4823.91, 4849.06}, + {1756064700000LL, 4849.07, 4850.4, 4814.81, 4820.32}, + {1756064760000LL, 4820.33, 4829, 4805, 4808.45}, + {1756064820000LL, 4808.45, 4819.52, 4792.8, 4817.03}, + {1756064880000LL, 4817.02, 4825.44, 4810.8, 4819.82}, + {1756064940000LL, 4819.77, 4833, 4814.99, 4817.33}, + {1756065000000LL, 4817.2, 4818.6, 4806.23, 4808.83}, + {1756065060000LL, 4808.83, 4812.42, 4771.37, 4779.9}, + {1756065120000LL, 4779.89, 4783.97, 4750.01, 4772}, + {1756065180000LL, 4771.92, 4800, 4768.71, 4799.52}, + {1756065240000LL, 4799.52, 4801.15, 4774.11, 4776.71}, + {1756065300000LL, 4776.71, 4780.58, 4761, 4775.19}, + {1756065360000LL, 4775.2, 4784.54, 4767.53, 4780.43}, + {1756065420000LL, 4780.45, 4806.15, 4780.45, 4801.04}, + {1756065480000LL, 4801.04, 4805.96, 4792.3, 4805.28}, + {1756065540000LL, 4805.28, 4811.81, 4802.88, 4811.25}, + {1756065600000LL, 4811.25, 4814.33, 4785.21, 4789.9}, + {1756065660000LL, 4789.9, 4794.18, 4776.85, 4778.75}, + {1756065720000LL, 4778.75, 4780.54, 4761.71, 4770.43}, + {1756065780000LL, 4770.44, 4783.33, 4770.43, 4779.3}, + {1756065840000LL, 4779.3, 4789, 4776.04, 4777.68}, + {1756065900000LL, 4777.69, 4778.49, 4761.5, 4772.04}, + {1756065960000LL, 4772.04, 4784.99, 4766.63, 4767.22}, + {1756066020000LL, 4767.21, 4767.47, 4720, 4744.09}, + {1756066080000LL, 4744.08, 4744.8, 4723.04, 4725.12}, + {1756066140000LL, 4725.12, 4739.19, 4702.47, 4737.24}, + {1756066200000LL, 4737.24, 4765.55, 4737.2, 4750.6}, + {1756066260000LL, 4750.61, 4752.65, 4742.12, 4749.38}, + {1756066320000LL, 4749.37, 4749.57, 4727.34, 4727.8}, + {1756066380000LL, 4727.8, 4734.62, 4722.11, 4731.05}, + {1756066440000LL, 4731.04, 4744.14, 4731.04, 4737.82}, + {1756066500000LL, 4737.81, 4755, 4730.23, 4752.7}, + {1756066560000LL, 4752.71, 4764.74, 4747.42, 4764.74}, + {1756066620000LL, 4764.73, 4775.7, 4763.99, 4772.11}, + {1756066680000LL, 4772.75, 4778.31, 4761.98, 4769.17}, + {1756066740000LL, 4769.17, 4775.91, 4766.6, 4774.89}, + {1756066800000LL, 4774.89, 4777.01, 4765.05, 4770.35}, + {1756066860000LL, 4770.35, 4793.38, 4770.09, 4784.46}, + {1756066920000LL, 4784.4, 4796, 4777.67, 4794.49}, + {1756066980000LL, 4794.48, 4811.8, 4794.07, 4809.02}, + {1756067040000LL, 4809.02, 4812.65, 4798, 4800.7}, + {1756067100000LL, 4800.7, 4803.43, 4786.32, 4788.29}, + {1756067160000LL, 4788.28, 4795.91, 4781.15, 4792.07}, + {1756067220000LL, 4792.07, 4796.88, 4778.35, 4779.64}, + {1756067280000LL, 4779.63, 4790, 4774.07, 4786.99}, + {1756067340000LL, 4786.98, 4792.88, 4784.33, 4792.88}, +}; +constexpr FeedBar kI3_0406[90] = { // signal bar 2025-04-06 20:00 UTC + {1743969600000LL, 1625.21, 1625.22, 1622.62, 1623.05}, + {1743969660000LL, 1623.05, 1629.98, 1622.64, 1629.98}, + {1743969720000LL, 1629.98, 1629.99, 1627.84, 1628.69}, + {1743969780000LL, 1628.7, 1630.45, 1627.52, 1628.59}, + {1743969840000LL, 1628.59, 1628.76, 1623.19, 1623.84}, + {1743969900000LL, 1623.83, 1629.19, 1622.81, 1628.23}, + {1743969960000LL, 1628.24, 1628.48, 1624.36, 1624.98}, + {1743970020000LL, 1625.01, 1625.55, 1623.9, 1624.27}, + {1743970080000LL, 1624.28, 1624.44, 1620.85, 1621.34}, + {1743970140000LL, 1621.3, 1622.12, 1620, 1620.01}, + {1743970200000LL, 1620.01, 1624.48, 1617.79, 1622.8}, + {1743970260000LL, 1622.81, 1623.16, 1619.42, 1620.96}, + {1743970320000LL, 1620.96, 1622.83, 1619.11, 1619.4}, + {1743970380000LL, 1619.41, 1620.77, 1619, 1619.87}, + {1743970440000LL, 1619.86, 1620.07, 1618.61, 1618.66}, + {1743970500000LL, 1618.67, 1620.07, 1617.03, 1618.96}, + {1743970560000LL, 1618.95, 1619.87, 1615.42, 1616.05}, + {1743970620000LL, 1616.06, 1619.48, 1616.05, 1617.86}, + {1743970680000LL, 1617.87, 1623, 1617.36, 1622.46}, + {1743970740000LL, 1622.46, 1623.84, 1622, 1622.98}, + {1743970800000LL, 1622.98, 1622.98, 1620.08, 1620.1}, + {1743970860000LL, 1620.09, 1621, 1617.92, 1620.26}, + {1743970920000LL, 1620.26, 1621.18, 1618, 1621.18}, + {1743970980000LL, 1621.18, 1621.67, 1617, 1617.21}, + {1743971040000LL, 1617.2, 1618.81, 1617.2, 1618.01}, + {1743971100000LL, 1618, 1618.42, 1615.7, 1615.7}, + {1743971160000LL, 1615.7, 1617.76, 1615.55, 1616.78}, + {1743971220000LL, 1616.78, 1620.05, 1616.78, 1619.52}, + {1743971280000LL, 1619.51, 1619.76, 1618.22, 1619}, + {1743971340000LL, 1619.01, 1619.01, 1615.32, 1615.76}, + {1743971400000LL, 1615.77, 1617.84, 1613.2, 1617.3}, + {1743971460000LL, 1617.28, 1617.7, 1613.4, 1613.54}, + {1743971520000LL, 1613.54, 1614.65, 1611.3, 1612.19}, + {1743971580000LL, 1612.17, 1612.19, 1603.22, 1607.55}, + {1743971640000LL, 1607.55, 1608.66, 1574.55, 1586.88}, + {1743971700000LL, 1586.88, 1594, 1586.86, 1593.63}, + {1743971760000LL, 1593.63, 1593.63, 1555.38, 1555.48}, + {1743971820000LL, 1555.38, 1585, 1552.5, 1581.69}, + {1743971880000LL, 1581.69, 1585.4, 1577.78, 1581.11}, + {1743971940000LL, 1581.09, 1594, 1581.09, 1593.19}, + {1743972000000LL, 1593.1, 1593.1, 1585.36, 1586.98}, + {1743972060000LL, 1586.98, 1592.22, 1585.77, 1591.71}, + {1743972120000LL, 1591.74, 1596.5, 1590.67, 1595.95}, + {1743972180000LL, 1595.95, 1597, 1589.89, 1590.8}, + {1743972240000LL, 1590.8, 1591.6, 1587.34, 1589.4}, + {1743972300000LL, 1589.41, 1590, 1583.3, 1584.22}, + {1743972360000LL, 1584.22, 1588.16, 1584.21, 1586.42}, + {1743972420000LL, 1586.41, 1586.41, 1577.13, 1579.65}, + {1743972480000LL, 1579.66, 1582.37, 1576.02, 1582.31}, + {1743972540000LL, 1582.27, 1582.28, 1577.26, 1578.3}, + {1743972600000LL, 1578.3, 1586.64, 1576.16, 1585.99}, + {1743972660000LL, 1585.98, 1585.98, 1580.08, 1581.94}, + {1743972720000LL, 1581.94, 1582.08, 1573.77, 1578.24}, + {1743972780000LL, 1578.24, 1578.24, 1569.28, 1571.39}, + {1743972840000LL, 1571.4, 1575.92, 1571.39, 1572.72}, + {1743972900000LL, 1572.72, 1578.29, 1572.54, 1577.83}, + {1743972960000LL, 1577.84, 1578.88, 1573.36, 1575.46}, + {1743973020000LL, 1575.47, 1576, 1571.42, 1572.61}, + {1743973080000LL, 1572.6, 1577.59, 1570.6, 1576.63}, + {1743973140000LL, 1576.63, 1576.64, 1573.44, 1574.04}, + {1743973200000LL, 1574.05, 1578.5, 1570.08, 1577.7}, + {1743973260000LL, 1577.7, 1583.56, 1575.04, 1581.97}, + {1743973320000LL, 1581.98, 1585.32, 1580.86, 1582.73}, + {1743973380000LL, 1582.72, 1586.37, 1581.01, 1585.89}, + {1743973440000LL, 1585.88, 1585.96, 1581.49, 1584.11}, + {1743973500000LL, 1584.1, 1584.1, 1579.16, 1581.69}, + {1743973560000LL, 1581.69, 1588.88, 1581.69, 1588.88}, + {1743973620000LL, 1588.88, 1589.36, 1583.8, 1584.09}, + {1743973680000LL, 1584.1, 1584.14, 1581, 1581.5}, + {1743973740000LL, 1581.5, 1581.51, 1578.19, 1579.8}, + {1743973800000LL, 1579.8, 1584.14, 1579.7, 1583.79}, + {1743973860000LL, 1583.78, 1586, 1581.84, 1585.87}, + {1743973920000LL, 1585.85, 1589.65, 1585.1, 1588.81}, + {1743973980000LL, 1588.81, 1589.89, 1586.62, 1586.92}, + {1743974040000LL, 1586.92, 1587.99, 1584.87, 1586.79}, + {1743974100000LL, 1586.78, 1593.99, 1585.99, 1593.4}, + {1743974160000LL, 1593.4, 1594.92, 1591.45, 1592.48}, + {1743974220000LL, 1592.49, 1595.64, 1592.34, 1594.99}, + {1743974280000LL, 1594.98, 1595.69, 1591.91, 1592.15}, + {1743974340000LL, 1592.15, 1592.56, 1590.63, 1592.38}, + {1743974400000LL, 1592.38, 1595.51, 1591.72, 1594.81}, + {1743974460000LL, 1594.81, 1596.8, 1594.33, 1594.81}, + {1743974520000LL, 1594.81, 1594.81, 1591, 1594.6}, + {1743974580000LL, 1594.6, 1595.55, 1593.73, 1595.4}, + {1743974640000LL, 1595.41, 1595.42, 1591.6, 1593.21}, + {1743974700000LL, 1593.21, 1593.7, 1590, 1590.34}, + {1743974760000LL, 1590.33, 1590.81, 1588.35, 1589.99}, + {1743974820000LL, 1589.98, 1589.99, 1583.78, 1586.61}, + {1743974880000LL, 1586.61, 1586.61, 1581.25, 1585.44}, + {1743974940000LL, 1585.43, 1588.16, 1585.43, 1588.15}, +}; diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/meta.json b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/meta.json new file mode 100644 index 00000000..d04de73c --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-i3-eth-chart-0130", + "slug": "pm-i3-eth-chart-0130", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:14:48.079Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-28", + "to": "2026-02-01" + }, + "pine_sha256": "bd1ffdc0846c78556f8ab7439bdff550854fe716c9a50774febdad31cfdc26a9", + "tv_trades_sha256": "2f0f39543f3d42d9daade384a3c70319d088f7ffc74fd5331e363d64c611dfb6", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/metrics.json b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/metrics.json new file mode 100644 index 00000000..155ef3a6 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-i3-eth-chart-0130", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-28", + "to": "2026-02-01", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "bd1ffdc0846c78556f8ab7439bdff550854fe716c9a50774febdad31cfdc26a9", + "tvTradesCsvHash": "2f0f39543f3d42d9daade384a3c70319d088f7ffc74fd5331e363d64c611dfb6", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74602", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1769558400000, + "to": 1769904000000 + }, + "returnedRange": { + "from": 1769558400000, + "to": 1769904000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "d6d77944d3c5317f1715e6752f3e5b3306551ac8b06352251ebe0637a32fcc52", + "decodedReportSha256": "302518b8720b040b7dd98dfecf3a0294504eeecc2d723d4edbd00b9ddb3e9cdb", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:14:48.079Z" +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/strategy.pine b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/strategy.pine new file mode 100644 index 00000000..4caf5340 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN item 3: does TradingView check the margin model at each +// intrabar sample under the bar magnifier? One leveraged long (margin 20, 4.743 x equity) +// placed on the signal bar 2026-01-30 01:00 UTC, filled at the next bar's open and +// carried: the bar after the entry bar first crosses the maintenance line at an early +// sub-bar low and reaches a deeper low later in the same 15m bar. +// Magnifier OFF (control). Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-i3-eth-chart-0130", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=false) +if time == timestamp("UTC", 2026, 1, 30, 1, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.743 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/tv_trades.csv b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/tv_trades.csv new file mode 100644 index 00000000..e30fe7cc --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0130/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2026-01-30 09:30,Margin call,2673.33,0.988,2775.62792,-134.37788,-4.84,0,2.75652,0.10,-134.37788,-4.84,-134.37788,-13.44,1 +1,Entry long,2026-01-30 09:15,L,2809.34,0.988,2775.62792,-134.37788,-4.84,0,2.75652,0.10,-134.37788,-4.84,-134.37788,-13.44,1 +2,Exit long,2026-01-30 10:15,timeout,2728.59,0.7,1966.538,-56.525,-2.87,0,1.953,0.10,-95.207,-4.84,-190.90288,-19.09,4 +2,Entry long,2026-01-30 09:15,L,2809.34,0.7,1966.538,-56.525,-2.87,0,1.953,0.10,-95.207,-4.84,-190.90288,-19.09,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/meta.json b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/meta.json new file mode 100644 index 00000000..6d791746 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-i3-eth-chart-0406", + "slug": "pm-i3-eth-chart-0406", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:15:02.621Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-04", + "to": "2025-04-08" + }, + "pine_sha256": "062f7dd77499546a4804b49a4213a2bc3d1b90726f5c388291e63df074b79248", + "tv_trades_sha256": "ea61c132015105143ab94db8e558edca09578b4708617e39dad80d0446b64f42", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/metrics.json b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/metrics.json new file mode 100644 index 00000000..e86f869b --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-i3-eth-chart-0406", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-04", + "to": "2025-04-08", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "062f7dd77499546a4804b49a4213a2bc3d1b90726f5c388291e63df074b79248", + "tvTradesCsvHash": "ea61c132015105143ab94db8e558edca09578b4708617e39dad80d0446b64f42", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74606", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743724800000, + "to": 1744070400000 + }, + "returnedRange": { + "from": 1743724800000, + "to": 1744070400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "5ccfcd710a4c73770b5a93212afb1b85dca3d953cfd0b818d3cb4682af18fee3", + "decodedReportSha256": "0f5d27836433ff65df9b05847bc63bcc1df6340851d5d92ab0a3f15a12e3d415", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:15:02.620Z" +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/strategy.pine b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/strategy.pine new file mode 100644 index 00000000..99806603 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN item 3: does TradingView check the margin model at each +// intrabar sample under the bar magnifier? One leveraged long (margin 20, 4.657 x equity) +// placed on the signal bar 2025-04-06 20:00 UTC, filled at the next bar's open and +// carried: the bar after the entry bar first crosses the maintenance line at an early +// sub-bar low and reaches a deeper low later in the same 15m bar. +// Magnifier OFF (control). Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-i3-eth-chart-0406", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=false) +if time == timestamp("UTC", 2025, 4, 6, 20, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.657 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/tv_trades.csv b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/tv_trades.csv new file mode 100644 index 00000000..0bf0657d --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0406/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-07 04:30,Margin call,1552.5,1.0776,1744.278792,-71.304794,-4.09,0,5.571192,0.32,-71.304794,-4.09,-71.304794,-7.13,1 +1,Entry long,2025-04-07 04:15,L,1618.67,1.0776,1744.278792,-71.304794,-4.09,0,5.571192,0.32,-71.304794,-4.09,-71.304794,-7.13,1 +2,Exit long,2025-04-07 05:15,timeout,1586.78,1.7994,2912.634798,-57.382866,-1.97,0,9.302898,0.32,-119.0663,-4.09,-128.68765,-12.87,4 +2,Entry long,2025-04-07 04:15,L,1618.67,1.7994,2912.634798,-57.382866,-1.97,0,9.302898,0.32,-119.0663,-4.09,-128.68765,-12.87,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/meta.json b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/meta.json new file mode 100644 index 00000000..20e81133 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-i3-eth-chart-0622", + "slug": "pm-i3-eth-chart-0622", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:14:39.778Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-20", + "to": "2025-06-24" + }, + "pine_sha256": "46e609767ace7de7b645cec34becb2af83384080d143391d750704a15d64a78d", + "tv_trades_sha256": "714f4333cebd50c8c3f699b45648ce78495ef3e50ef888359135be5525edaf2f", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/metrics.json b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/metrics.json new file mode 100644 index 00000000..08e9ac6a --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-i3-eth-chart-0622", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-20", + "to": "2025-06-24", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "46e609767ace7de7b645cec34becb2af83384080d143391d750704a15d64a78d", + "tvTradesCsvHash": "714f4333cebd50c8c3f699b45648ce78495ef3e50ef888359135be5525edaf2f", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74600", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1750377600000, + "to": 1750723200000 + }, + "returnedRange": { + "from": 1750377600000, + "to": 1750723200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "d24631393afdbe56247c206739d81ce84933c5b8d743835c9df6751b4c05acce", + "decodedReportSha256": "a41a1cb9ae5bf669d07706ffb9048d6f719647012bb91b38bcd7294a03acef44", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:14:39.777Z" +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/strategy.pine b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/strategy.pine new file mode 100644 index 00000000..423d73ea --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN item 3: does TradingView check the margin model at each +// intrabar sample under the bar magnifier? One leveraged long (margin 20, 4.608 x equity) +// placed on the signal bar 2025-06-22 12:45 UTC, filled at the next bar's open and +// carried: the bar after the entry bar first crosses the maintenance line at an early +// sub-bar low and reaches a deeper low later in the same 15m bar. +// Magnifier OFF (control). Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-i3-eth-chart-0622", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=false) +if time == timestamp("UTC", 2025, 6, 22, 12, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.608 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/tv_trades.csv b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/tv_trades.csv new file mode 100644 index 00000000..429974a6 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0622/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-22 21:15,Margin call,2159.28,1.064,2424.6432,-127.16928,-5.24,0,5.71368,0.24,-127.16928,-5.24,-127.16928,-12.72,1 +1,Entry long,2025-06-22 21:00,L,2278.8,1.064,2424.6432,-127.16928,-5.24,0,5.71368,0.24,-127.16928,-5.24,-127.16928,-12.72,1 +2,Exit long,2025-06-22 22:00,timeout,2198.9,0.958,2183.0904,-76.5442,-3.51,0,5.14446,0.24,-114.50016,-5.24,-203.71349,-20.37,4 +2,Entry long,2025-06-22 21:00,L,2278.8,0.958,2183.0904,-76.5442,-3.51,0,5.14446,0.24,-114.50016,-5.24,-203.71349,-20.37,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/meta.json b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/meta.json new file mode 100644 index 00000000..f64ef1ee --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-i3-eth-chart-0824", + "slug": "pm-i3-eth-chart-0824", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:14:55.537Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-08-22", + "to": "2025-08-26" + }, + "pine_sha256": "5e9e23dd96a120eb35882edd30587c7506ef999bf09b7ad0fc1b72a8462bcc0d", + "tv_trades_sha256": "982df67044dacfa5705f41677229b29d693c3bd2d1825ef39fcceaf0e4b3af61", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/metrics.json b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/metrics.json new file mode 100644 index 00000000..e1ec1012 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-i3-eth-chart-0824", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-08-22", + "to": "2025-08-26", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "5e9e23dd96a120eb35882edd30587c7506ef999bf09b7ad0fc1b72a8462bcc0d", + "tvTradesCsvHash": "982df67044dacfa5705f41677229b29d693c3bd2d1825ef39fcceaf0e4b3af61", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74604", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1755820800000, + "to": 1756166400000 + }, + "returnedRange": { + "from": 1755820800000, + "to": 1756166400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "c9be8c0ec89ea3ee10273f579889fbc3b0da359d29f17ca3d8d41b197d319df4", + "decodedReportSha256": "5a5fd560189ee4aa5c9cf9f0fb9be2b13e483c8c12d7638ed8e29ee0f556f558", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:14:55.536Z" +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/strategy.pine b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/strategy.pine new file mode 100644 index 00000000..9cf832fd --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN item 3: does TradingView check the margin model at each +// intrabar sample under the bar magnifier? One leveraged long (margin 20, 4.694 x equity) +// placed on the signal bar 2025-08-24 19:00 UTC, filled at the next bar's open and +// carried: the bar after the entry bar first crosses the maintenance line at an early +// sub-bar low and reaches a deeper low later in the same 15m bar. +// Magnifier OFF (control). Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-i3-eth-chart-0824", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=false) +if time == timestamp("UTC", 2025, 8, 24, 19, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.694 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/tv_trades.csv b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/tv_trades.csv new file mode 100644 index 00000000..9ce40f4d --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-chart-0824/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-08-25 03:30,Margin call,4740.72,0.3784,1869.186264,-75.29781,-4.03,0,5.774384,0.31,-75.29781,-4.03,-75.29781,-7.53,1 +1,Entry long,2025-08-25 03:15,L,4939.71,0.3784,1869.186264,-75.29781,-4.03,0,5.774384,0.31,-75.29781,-4.03,-75.29781,-7.53,1 +2,Exit long,2025-08-25 04:15,timeout,4737.81,0.5716,2823.538236,-115.40604,-4.09,0,8.722616,0.31,-135.60638,-4.80,-190.70386,-19.07,4 +2,Entry long,2025-08-25 03:15,L,4939.71,0.5716,2823.538236,-115.40604,-4.09,0,8.722616,0.31,-135.60638,-4.80,-190.70386,-19.07,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/meta.json b/tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/meta.json new file mode 100644 index 00000000..2559cabf --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-i3-eth-ltf-lows", + "slug": "pm-i3-eth-ltf-lows", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:17:08.403Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-20", + "to": "2026-02-01" + }, + "pine_sha256": "a4a4906f620d6d7f45339021217ba192aaa23fb7fb6232860acf700e2c265304", + "tv_trades_sha256": "ad312f3e1202becf14efea0f2f1c00dbcda4b0c54e3eb80a51b8aee93a4ccc9a", + "tv_trades_rows": 8 +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/metrics.json b/tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/metrics.json new file mode 100644 index 00000000..b9599816 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-i3-eth-ltf-lows", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-20", + "to": "2026-02-01", + "deepBacktesting": true, + "trades": 4, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "a4a4906f620d6d7f45339021217ba192aaa23fb7fb6232860acf700e2c265304", + "tvTradesCsvHash": "ad312f3e1202becf14efea0f2f1c00dbcda4b0c54e3eb80a51b8aee93a4ccc9a", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74607", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1750377600000, + "to": 1769904000000 + }, + "returnedRange": { + "from": 1750377600000, + "to": 1769904000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "b362d64fe02c43c20b0796fa029616ba45f1ba9edbc760326662c77b211ee257", + "decodedReportSha256": "d562d643a4717314b486133170065ce193f9684091941510f091e58056cde356", + "closedTrades": 4, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:17:08.402Z" +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/strategy.pine b/tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/strategy.pine new file mode 100644 index 00000000..3d3a217d --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN item 3: TradingView's own 1-minute lows inside two 15m bars, read +// through request.security_lower_tf and printed as order comments (Signal column), to +// compare with the corpus 1m feed the magnified replay uses. +//@version=6 +strategy("pm-i3-eth-ltf-lows", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=0.001, pyramiding=4) +lows = request.security_lower_tf(syminfo.tickerid, "1", low) +if time == timestamp("UTC", 2026, 1, 30, 1, 30) and array.size(lows) >= 15 + strategy.entry("A", strategy.long, comment = str.tostring(array.slice(lows, 0, 8))) + strategy.entry("B", strategy.long, comment = str.tostring(array.slice(lows, 8, 15))) +if time == timestamp("UTC", 2025, 6, 22, 13, 15) and array.size(lows) >= 15 + strategy.entry("C", strategy.long, comment = str.tostring(array.slice(lows, 0, 8))) + strategy.entry("D", strategy.long, comment = str.tostring(array.slice(lows, 8, 15))) +if time == timestamp("UTC", 2026, 1, 30, 2, 0) or time == timestamp("UTC", 2025, 6, 22, 14, 0) + strategy.close_all(comment = "n=" + str.tostring(array.size(lows))) diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/tv_trades.csv b/tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/tv_trades.csv new file mode 100644 index 00000000..41dd6f97 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-ltf-lows/tv_trades.csv @@ -0,0 +1,9 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-22 22:15,n=15,2189.86,0.001,2.1948,-0.00494,-0.23,0,0.0172,0.78,-0.01922,-0.88,-0.00494,0.00,3 +1,Entry long,2025-06-22 21:30,"[2254.45, 2252, 2248.15, 2247.25, 2246.14, 2237.77, 2223.05, 2225.39]",2194.8,0.001,2.1948,-0.00494,-0.23,0,0.0172,0.78,-0.01922,-0.88,-0.00494,0.00,3 +2,Exit long,2025-06-22 22:15,n=15,2189.86,0.001,2.1948,-0.00494,-0.23,0,0.0172,0.78,-0.01922,-0.88,-0.00988,0.00,3 +2,Entry long,2025-06-22 21:30,"[2223.6, 2225, 2198.19, 2201.5, 2180, 2159.28, 2182.22]",2194.8,0.001,2.1948,-0.00494,-0.23,0,0.0172,0.78,-0.01922,-0.88,-0.00988,0.00,3 +3,Exit long,2026-01-30 10:15,n=15,2728.59,0.001,2.7237,0.00489,0.18,0,0.0273,1.00,-0.0087,-0.32,-0.00499,0.00,2 +3,Entry long,2026-01-30 09:45,"[2793.76, 2796.67, 2801.12, 2802.47, 2802.72, 2796.95, 2796.78, 2782.69]",2723.7,0.001,2.7237,0.00489,0.18,0,0.0273,1.00,-0.0087,-0.32,-0.00499,0.00,2 +4,Exit long,2026-01-30 10:15,n=15,2728.59,0.001,2.7237,0.00489,0.18,0,0.0273,1.00,-0.0087,-0.32,-0.0001,0.00,2 +4,Entry long,2026-01-30 09:45,"[2782.63, 2760, 2763.35, 2727.51, 2690, 2673.33, 2707.28]",2723.7,0.001,2.7237,0.00489,0.18,0,0.0273,1.00,-0.0087,-0.32,-0.0001,0.00,2 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/meta.json b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/meta.json new file mode 100644 index 00000000..c43bccd1 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-i3-eth-mag-0130", + "slug": "pm-i3-eth-mag-0130", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:14:43.708Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-28", + "to": "2026-02-01" + }, + "pine_sha256": "f86b94ff1abbb068905c2c4ea4abdfa74d98ad84500e1de5c488e8817f13507f", + "tv_trades_sha256": "ac0fb36013b01d07933abe1523bbc6fff5bb2ee008821cee24cd5102e056a2d4", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/metrics.json b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/metrics.json new file mode 100644 index 00000000..2958a94f --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-i3-eth-mag-0130", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-28", + "to": "2026-02-01", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "f86b94ff1abbb068905c2c4ea4abdfa74d98ad84500e1de5c488e8817f13507f", + "tvTradesCsvHash": "ac0fb36013b01d07933abe1523bbc6fff5bb2ee008821cee24cd5102e056a2d4", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74601", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1769558400000, + "to": 1769904000000 + }, + "returnedRange": { + "from": 1769558400000, + "to": 1769904000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "77a28a9668207fe7d8f563ea69013b07655880111fb667adc520c99d736c71bb", + "decodedReportSha256": "8ffdeb8c4040c8fc17f8a283db4176060a66716e4827823f8b038d84ac50c09f", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:14:43.708Z" +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/strategy.pine b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/strategy.pine new file mode 100644 index 00000000..d8fcc5e8 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN item 3: does TradingView check the margin model at each +// intrabar sample under the bar magnifier? One leveraged long (margin 20, 4.743 x equity) +// placed on the signal bar 2026-01-30 01:00 UTC, filled at the next bar's open and +// carried: the bar after the entry bar first crosses the maintenance line at an early +// sub-bar low and reaches a deeper low later in the same 15m bar. +// Magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-i3-eth-mag-0130", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=true) +if time == timestamp("UTC", 2026, 1, 30, 1, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.743 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/tv_trades.csv b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/tv_trades.csv new file mode 100644 index 00000000..89160d08 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0130/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2026-01-30 09:30,Margin call,2760,0.1088,305.656192,-5.368192,-1.76,0,0.303552,0.10,-5.368192,-1.76,-5.368192,-0.54,1 +1,Entry long,2026-01-30 09:15,L,2809.34,0.1088,305.656192,-5.368192,-1.76,0,0.303552,0.10,-5.368192,-1.76,-5.368192,-0.54,1 +2,Exit long,2026-01-30 09:30,Margin call,2673.33,0.4824,1355.225616,-65.61122,-4.84,0,1.345896,0.10,-65.61122,-4.84,-70.979416,-7.10,1 +2,Entry long,2026-01-30 09:15,L,2809.34,0.4824,1355.225616,-65.61122,-4.84,0,1.345896,0.10,-65.61122,-4.84,-70.979416,-7.10,1 +3,Exit long,2026-01-30 10:15,timeout,2728.59,1.0968,3081.284112,-88.5666,-2.87,0,3.060072,0.10,-149.17577,-4.84,-159.54602,-15.95,4 +3,Entry long,2026-01-30 09:15,L,2809.34,1.0968,3081.284112,-88.5666,-2.87,0,3.060072,0.10,-149.17577,-4.84,-159.54602,-15.95,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/meta.json b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/meta.json new file mode 100644 index 00000000..a129a44d --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-i3-eth-mag-0406", + "slug": "pm-i3-eth-mag-0406", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:14:58.992Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-04", + "to": "2025-04-08" + }, + "pine_sha256": "a0eea5ebca679698dc5953160bd50bdc68dc9c01430c0aa204ae94255b8b9be2", + "tv_trades_sha256": "40208b4aa509bd587fee990e0f8490956242c0cc3e9cfc6d8dc387e606a38e8a", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/metrics.json b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/metrics.json new file mode 100644 index 00000000..21547ad7 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-i3-eth-mag-0406", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-04", + "to": "2025-04-08", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "a0eea5ebca679698dc5953160bd50bdc68dc9c01430c0aa204ae94255b8b9be2", + "tvTradesCsvHash": "40208b4aa509bd587fee990e0f8490956242c0cc3e9cfc6d8dc387e606a38e8a", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74605", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743724800000, + "to": 1744070400000 + }, + "returnedRange": { + "from": 1743724800000, + "to": 1744070400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f7b3efe10b63fe6751a9d0e341e504c5d2b930cde29e1fa78b2fe4973c6c6eb9", + "decodedReportSha256": "5807b949257bc2eecc28b4834ddd4d52d66b80a45e944dee8f02b9cd7e7eab57", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:14:58.991Z" +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/strategy.pine b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/strategy.pine new file mode 100644 index 00000000..407dd16e --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN item 3: does TradingView check the margin model at each +// intrabar sample under the bar magnifier? One leveraged long (margin 20, 4.657 x equity) +// placed on the signal bar 2025-04-06 20:00 UTC, filled at the next bar's open and +// carried: the bar after the entry bar first crosses the maintenance line at an early +// sub-bar low and reaches a deeper low later in the same 15m bar. +// Magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-i3-eth-mag-0406", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 4, 6, 20, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.657 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/tv_trades.csv b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/tv_trades.csv new file mode 100644 index 00000000..97db1d90 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0406/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-07 04:30,Margin call,1574.55,0.418,676.60406,-18.44216,-2.73,0,2.16106,0.32,-18.44216,-2.73,-18.44216,-1.84,1 +1,Entry long,2025-04-07 04:15,L,1618.67,0.418,676.60406,-18.44216,-2.73,0,2.16106,0.32,-18.44216,-2.73,-18.44216,-1.84,1 +2,Exit long,2025-04-07 05:15,timeout,1586.78,2.459,3980.30953,-78.41751,-1.97,0,12.71303,0.32,-162.71204,-4.09,-96.85967,-9.69,4 +2,Entry long,2025-04-07 04:15,L,1618.67,2.459,3980.30953,-78.41751,-1.97,0,12.71303,0.32,-162.71204,-4.09,-96.85967,-9.69,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/meta.json b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/meta.json new file mode 100644 index 00000000..487f8e88 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-i3-eth-mag-0622", + "slug": "pm-i3-eth-mag-0622", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:14:35.784Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-20", + "to": "2025-06-24" + }, + "pine_sha256": "f0e8a2f42bca5a6d39db15b9b03d0c4a14eefc5f37e29569fa4e336db80c88c0", + "tv_trades_sha256": "2488da65fe9bd3412b35cb19d3fc59f63ae07c3de39b6e1645598af94dd59d41", + "tv_trades_rows": 8 +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/metrics.json b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/metrics.json new file mode 100644 index 00000000..75a95866 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-i3-eth-mag-0622", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-20", + "to": "2025-06-24", + "deepBacktesting": true, + "trades": 4, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "f0e8a2f42bca5a6d39db15b9b03d0c4a14eefc5f37e29569fa4e336db80c88c0", + "tvTradesCsvHash": "2488da65fe9bd3412b35cb19d3fc59f63ae07c3de39b6e1645598af94dd59d41", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74599", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1750377600000, + "to": 1750723200000 + }, + "returnedRange": { + "from": 1750377600000, + "to": 1750723200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "cdb8ff08ff39978498e62e1819c69ad04f9a01443f14360bacb5706d35f7d67f", + "decodedReportSha256": "e8383f7d8bbeb0bbcb1bd575f9b5cd6a6b2b10c0d5432b307964f3ddb0eca121", + "closedTrades": 4, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:14:35.783Z" +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/strategy.pine b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/strategy.pine new file mode 100644 index 00000000..0bc8d429 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN item 3: does TradingView check the margin model at each +// intrabar sample under the bar magnifier? One leveraged long (margin 20, 4.608 x equity) +// placed on the signal bar 2025-06-22 12:45 UTC, filled at the next bar's open and +// carried: the bar after the entry bar first crosses the maintenance line at an early +// sub-bar low and reaches a deeper low later in the same 15m bar. +// Magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-i3-eth-mag-0622", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 6, 22, 12, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.608 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/tv_trades.csv b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/tv_trades.csv new file mode 100644 index 00000000..82a812e3 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0622/tv_trades.csv @@ -0,0 +1,9 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-22 21:15,Margin call,2223.05,0.1052,239.72976,-5.8649,-2.45,0,0.564924,0.24,-5.8649,-2.45,-5.8649,-0.59,1 +1,Entry long,2025-06-22 21:00,L,2278.8,0.1052,239.72976,-5.8649,-2.45,0,0.564924,0.24,-5.8649,-2.45,-5.8649,-0.59,1 +2,Exit long,2025-06-22 21:15,Margin call,2198.19,0.0276,62.89488,-2.224836,-3.54,0,0.148212,0.24,-2.224836,-3.54,-8.089736,-0.81,1 +2,Entry long,2025-06-22 21:00,L,2278.8,0.0276,62.89488,-2.224836,-3.54,0,0.148212,0.24,-2.224836,-3.54,-8.089736,-0.81,1 +3,Exit long,2025-06-22 21:15,Margin call,2180,0.1688,384.66144,-16.67744,-4.34,0,0.906456,0.24,-16.67744,-4.34,-24.767176,-2.48,1 +3,Entry long,2025-06-22 21:00,L,2278.8,0.1688,384.66144,-16.67744,-4.34,0,0.906456,0.24,-16.67744,-4.34,-24.767176,-2.48,1 +4,Exit long,2025-06-22 22:00,timeout,2198.9,1.7204,3920.44752,-137.45996,-3.51,0,9.238548,0.24,-205.62221,-5.24,-162.22714,-16.22,4 +4,Entry long,2025-06-22 21:00,L,2278.8,1.7204,3920.44752,-137.45996,-3.51,0,9.238548,0.24,-205.62221,-5.24,-162.22714,-16.22,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/meta.json b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/meta.json new file mode 100644 index 00000000..cfcbc070 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm-i3-eth-mag-0824", + "slug": "pm-i3-eth-mag-0824", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T22:14:51.826Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-08-22", + "to": "2025-08-26" + }, + "pine_sha256": "a0380dfc38314faef2de873033780865b42d066527ac3e2b7a130ef65af1d4e4", + "tv_trades_sha256": "f0e1b58462313b44a5727e0d9ecc952238de677cf6db3ad3ecdc0192d04d8d4a", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/metrics.json b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/metrics.json new file mode 100644 index 00000000..12642893 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm-i3-eth-mag-0824", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-08-22", + "to": "2025-08-26", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "a0380dfc38314faef2de873033780865b42d066527ac3e2b7a130ef65af1d4e4", + "tvTradesCsvHash": "f0e1b58462313b44a5727e0d9ecc952238de677cf6db3ad3ecdc0192d04d8d4a", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.74603", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1755820800000, + "to": 1756166400000 + }, + "returnedRange": { + "from": 1755820800000, + "to": 1756166400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "fb28d8f8c5b83425195ba808254dc20cefdf32d26c98db3e28bff35293566721", + "decodedReportSha256": "1f6b35a0b6fbbbd1f0cb4932f4b1d77cd0628e37b578c1f4bbed6670e1ab0958", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T22:14:51.824Z" +} diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/strategy.pine b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/strategy.pine new file mode 100644 index 00000000..3e920d8b --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN item 3: does TradingView check the margin model at each +// intrabar sample under the bar magnifier? One leveraged long (margin 20, 4.694 x equity) +// placed on the signal bar 2025-08-24 19:00 UTC, filled at the next bar's open and +// carried: the bar after the entry bar first crosses the maintenance line at an early +// sub-bar low and reaches a deeper low later in the same 15m bar. +// Magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm-i3-eth-mag-0824", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 8, 24, 19, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.694 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/tv_trades.csv b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/tv_trades.csv new file mode 100644 index 00000000..71bdde8e --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm-i3-eth-mag-0824/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-08-25 03:30,Margin call,4820,0.1224,604.620504,-14.652504,-2.42,0,1.867824,0.31,-14.652504,-2.42,-14.652504,-1.47,1 +1,Entry long,2025-08-25 03:15,L,4939.71,0.1224,604.620504,-14.652504,-2.42,0,1.867824,0.31,-14.652504,-2.42,-14.652504,-1.47,1 +2,Exit long,2025-08-25 04:15,timeout,4737.81,0.8276,4088.103996,-167.09244,-4.09,0,12.629176,0.31,-196.33983,-4.80,-181.74495,-18.17,4 +2,Entry long,2025-08-25 03:15,L,4939.71,0.8276,4088.103996,-167.09244,-4.09,0,12.629176,0.31,-196.33983,-4.80,-181.74495,-18.17,4 \ No newline at end of file diff --git a/tests/test_adapter_margin_schedule_differential.cpp b/tests/test_adapter_margin_schedule_differential.cpp index 78134886..cdca01a3 100644 --- a/tests/test_adapter_margin_schedule_differential.cpp +++ b/tests/test_adapter_margin_schedule_differential.cpp @@ -42,6 +42,14 @@ * E20 f1: under set_probe_suppress_tail_logic the post-script close * checkpoint still runs on the forming bar (ruling of 2026-09-22, * docs/pages/live-surface.md §3.2). + * Intrabar margin on TradingView's tapes (tests/fixtures/intrabar_margin): + * a carried leveraged long under the bar magnifier crosses its line at + * a 1-minute low inside a 15m bar. TradingView books the call there, + * sized there, and again at each later 1-minute low that crosses the + * reduced book's line; with the magnifier off, once at the chart bar's + * low. Since R5 lane PAR-MARGIN the kernel offers an IntrabarSample + * point at every delivered sample and the adapter admits it on a + * magnified run's leveraged book. * M7 on TradingView's tapes (tests/fixtures/margin_entry_bar): a leveraged * opening whose entry bar breaches. TradingView books the margin call * on the entry bar at its low (4 of 4 tapes), and so does the kernel's @@ -84,6 +92,7 @@ struct FeedBar { }; #include "fixtures/margin_entry_bar/bars.inc" #include "fixtures/margin_entry_bar/m10_bars.inc" +#include "fixtures/intrabar_margin/bars_1m.inc" // ------------------------------------------------------------ TV policy // The twin's TradingView money and slice (test_native_margin_hooks_twin.cpp, @@ -873,8 +882,9 @@ std::int64_t tape_ms(const std::string& text) { // The tape's exits, each as {exit bar, price, qty, signal}; bars counted // from the signal bar. -std::vector tape_exits(const char* slug, const FeedBar* bars) { - std::ifstream in(std::string(PINEFORGE_HM_M7A_FIXTURE_DIR) + "/" + slug + "/tv_trades.csv"); +std::vector tape_exits(const char* slug, const FeedBar* bars, + const std::string& dir = PINEFORGE_HM_M7A_FIXTURE_DIR) { + std::ifstream in(dir + "/" + slug + "/tv_trades.csv"); std::vector out; std::string line; bool header = true; @@ -1107,6 +1117,114 @@ void m10_tradingview_tapes() { } } +// ================================================================= intrabar margin on tapes +// Item 3 of R5 lane PAR-MARGIN: does TradingView check the margin model at each +// intrabar sample under the bar magnifier? Four pairs of lab tv tapes +// (tests/fixtures/intrabar_margin, BINANCE:ETHUSDT.P 15m, ws-report-v1, +// rangeProof covered): one leveraged long (margin 20, K x equity) placed on +// the signal bar, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute low and reaches a +// deeper low later in the same bar; closed 3 bars after its entry bar. The +// same script with use_bar_magnifier on and off. +// magnifier off: TradingView books one call at the chart bar's low, and so +// does the adapter on the chart path (4 of 4); +// magnifier on: TradingView books the call at the first 1-minute low that +// crosses, sized there, and again at each later one that crosses the +// reduced book's line. The kernel checked a magnified bar at its first +// sample and after fills only, so the adapter booked none of those rows +// inside the bar; since the lane the kernel offers an IntrabarSample point +// at every later sample and the adapter admits it on a leveraged book, and +// it books every row on 0622 (three calls), 0824 and 0406. +// Recorded, open: on 0130 TradingView books its second call at the 01:43 +// low (0.4824 @2673.33) on the book the first call left, though the 01:42 +// low (2690) already crosses that book's line; the adapter books it there +// (0.3228 @2690), and nothing at 01:43. The first call and the chart-path +// control agree; the tape repeats over three other windows, and +// TradingView's own 1-minute lows (pm-i3-eth-ltf-lows) are the feed's. +struct IntrabarTape { + const char* tag; + const FeedBar* one; // 90 one-minute bars: the signal bar .. the entry bar + 4 + double leverage; + bool recorded; // 0130: pinned rows instead of the tape's +}; + +void intrabar_margin_tapes() { + const std::string dir = std::string(PINEFORGE_HM_M7A_FIXTURE_DIR) + "/../intrabar_margin"; + const IntrabarTape tapes[] = { + {"0622", kI3_0622, 4.608, false}, + {"0130", kI3_0130, 4.743, true}, + {"0824", kI3_0824, 4.694, false}, + {"0406", kI3_0406, 4.657, false}, + }; + for (const IntrabarTape& tape : tapes) { + std::vector one; + for (int i = 0; i < 90; ++i) { + const FeedBar& r = tape.one[i]; + one.push_back({r.open, r.high, r.low, r.close, 1.0, r.ts}); + } + FeedBar fifteen_rows[6]; + std::vector fifteen; + for (int b = 0; b < 6; ++b) { + const FeedBar* sub = tape.one + 15 * b; + FeedBar agg{sub[0].ts, sub[0].open, sub[0].high, sub[0].low, sub[14].close}; + for (int i = 1; i < 15; ++i) { + agg.high = std::max(agg.high, sub[i].high); + agg.low = std::min(agg.low, sub[i].low); + } + fifteen_rows[b] = agg; + fifteen.push_back({agg.open, agg.high, agg.low, agg.close, 1.0, agg.ts}); + } + const double qty = std::floor(tape.leverage * 1000.0 / fifteen_rows[0].close * 1000.0) + / 1000.0; + Config c; + c.capital = 1000.0; c.margin_long = 20.0; c.margin_short = 20.0; + c.qty_step = 0.0001; // TradingView's margin-call floor on this symbol (M7) + const auto script = [qty](PineSide& h, int bar) { + if (bar == 0) h.entry("L", true, kNaN, kNaN, qty); + if (h.position() > 0.0 && bar - h.entry_bar_index() >= 3) h.close("L"); + }; + for (const bool magnified : {false, true}) { + const std::string slug = std::string(magnified ? "pm-i3-eth-mag-" : "pm-i3-eth-chart-") + + tape.tag; + std::printf("-- intrabar margin tape %s\n", slug.c_str()); + PineSide pine(c); + pine.script = script; + if (magnified) { + pine.run(one.data(), static_cast(one.size()), "1", "15", true); + } else { + pine.run(fifteen.data(), static_cast(fifteen.size()), "15", "15", false); + } + CHECK(pine.last_error().empty()); + const auto adapter = engine_exits(pine); + const auto tv = tape_exits(slug.c_str(), fifteen_rows, dir); + print_rows("tape", tv); + print_rows("adapter", adapter); + // Every call lands on the bar after the entry bar (bar 2). + REQUIRE(!tv.empty()); + CHECK(tv[0].bar == 2); + CHECK(tv[0].signal == "Margin call"); + if (!magnified) { + CHECK(std::abs(tv[0].price - fifteen_rows[2].low) <= 5e-3); + CHECK(same_rows(adapter, tv)); + continue; + } + if (!tape.recorded) { + CHECK(same_rows(adapter, tv)); + continue; + } + // 0130, recorded: the first call agrees; the second parts. + REQUIRE(adapter.size() == 3); + REQUIRE(tv.size() == 3); + CHECK(same_rows({adapter[0]}, {tv[0]})); + CHECK(adapter[1].bar == 2 && same_value(adapter[1].price, 2690.0) + && std::abs(adapter[1].qty - 0.3228) <= 5e-5); + CHECK(tv[1].bar == 2 && std::abs(tv[1].price - 2673.33) <= 5e-3 + && std::abs(tv[1].qty - 0.4824) <= 5e-5); + CHECK(adapter[2].bar == tv[2].bar && std::abs(adapter[2].price - tv[2].price) <= 5e-3); + } + } +} + } // namespace int main() { @@ -1124,6 +1242,7 @@ int main() { test("E20 f1 probe tail", e20_probe_tail_margin_call); test("M7 TradingView tapes", m7_tradingview_tapes); test("M10 TradingView tapes", m10_tradingview_tapes); + test("intrabar margin tapes", intrabar_margin_tapes); std::printf("%d checks, %d failures\n", checks, failures); return failures == 0 ? 0 : 1; } diff --git a/tests/test_native_margin_intrabar_samples.cpp b/tests/test_native_margin_intrabar_samples.cpp new file mode 100644 index 00000000..05d3fcda --- /dev/null +++ b/tests/test_native_margin_intrabar_samples.cpp @@ -0,0 +1,266 @@ +// R5 lane PAR-MARGIN, item 3: the margin model on an intrabar path. +// +// A run with an IntrabarPath has no whole-bar waypoint model, and the kernel +// documented that it "re-evaluates at each delivered sample" -- but it checked +// only the script bar's first sample (BarOpen) and the re-arm after fills, so +// a breach inside a magnified bar that recovered by the next open was never +// booked (lane H-MEASURE's acid finding A). NativeMarginCheckKind:: +// IntrabarSample is that per-sample check: every delivered sample after the +// bar's first is offered to the host, measured at its own price, immediately +// before it is matched. TradingView's magnified broker checks the same way +// (tests/fixtures/intrabar_margin; the adapter half is +// test_adapter_margin_schedule_differential's "intrabar margin on tapes"). +// +// One short on a maintenance-only side, the run fed as 15m bars and, for the +// intrabar runs, the same bars as three 5m sub-bars each. Bar 2 first crosses +// the liquidation level at its first sub-bar's high (106) and reaches 110 in +// its second: +// 1. the points: a lower_tf run matched as continuous segments offers +// IntrabarSample at every sample but each bar's first (11 of 12 here), +// each at that sample's own price, and BarOpen at the first; a run +// without an intrabar path offers none, nor does a CalculationOnly model; +// 2. the mark check books the call AT the crossing sample (106), sized +// there, where the whole-bar model books it at the bar's extreme (110); +// 3. a host that refuses IntrabarSample keeps the earlier behaviour: the +// bar recovers by the next open and nothing is booked; +// 4. a one-price distribution path (synthesized) is not offered the point: +// a request armed at a discrete point is matched only at a later one, so +// it keeps its first sample and the re-arm after fills; +// 5. the C spelling is the kernel's value. +// +// Source-free: runs in the kernel-only profile too. +#include +#include + +#include +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; + +namespace { + +int checks = 0; +int failures = 0; +#define CHECK(expr) do { \ + ++checks; \ + if (!(expr)) { ++failures; std::printf("FAIL %d %s\n", __LINE__, #expr); } \ +} while (0) + +static_assert(static_cast(NativeMarginCheckKind::IntrabarSample) + == PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE, + "the C spelling names the kernel's value"); + +struct Point { + NativeMarginCheckKind kind; + int bar; + double mark; + std::uint64_t ordinal; +}; + +struct Probe final : NativeStrategyHost { + int bars = 0; + bool refuse_samples = false; + mutable std::vector points; + std::vector calls; + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + if (bars++ == 0) submit({no::Transact{-174.0}, "short", ""}); + } + bool margin_check_allowed(const NativeMarginCheckPoint& p) const override { + points.push_back({p.kind, p.cursor.point.interval_index, p.mark, p.cursor.point.ordinal}); + return !(refuse_samples && p.kind == NativeMarginCheckKind::IntrabarSample); + } + void on_native_margin_call(const no::MarginCallEvent& e) override { calls.push_back(e); } + int count(NativeMarginCheckKind kind) const { + int n = 0; + for (const auto& p : points) n += p.kind == kind; + return n; + } +}; + +const int64_t T0 = 1791190800000LL; // Mon 5 Oct 2026 09:00 UTC +const int64_t M15 = 15 * 60000LL; +const int64_t M5 = 5 * 60000LL; + +std::vector input() { + return {{100.0, 100.2, 99.8, 100.0, 10, T0}, + {100.0, 100.2, 99.8, 100.0, 10, T0 + M15}, // the short fills at 100 + {100.0, 110.0, 99.9, 100.0, 10, T0 + 2 * M15}, // crosses at 106, then 110 + {100.0, 100.2, 99.8, 100.0, 10, T0 + 3 * M15}, // opens under the level again + {100.0, 100.2, 99.8, 100.0, 10, T0 + 4 * M15}}; +} + +// Three 5m sub-bars per 15m bar, each O H L C with a high-first leg order. +std::vector finer() { + std::vector out; + for (const Bar& b : input()) { + if (b.high == 110.0) { + out.push_back({100.0, 106.0, 99.9, 105.0, 3, b.timestamp}); + out.push_back({105.0, 110.0, 104.0, 109.0, 4, b.timestamp + M5}); + out.push_back({109.0, 109.5, 100.0, 100.0, 3, b.timestamp + 2 * M5}); + } else { + out.push_back({b.open, b.high, b.low, 100.1, 3, b.timestamp}); + out.push_back({100.1, 100.1, 100.0, 100.0, 4, b.timestamp + M5}); + out.push_back({100.0, 100.0, 100.0, b.close, 3, b.timestamp + 2 * M5}); + } + } + return out; +} + +enum class Path { Chart, LowerTf, Synthesized }; + +NativeRunSpec spec(Path path, NativeLiquidationCheck check) { + NativeRunSpec s; + s.identity = {"margin-intrabar-samples", 1}; + s.input_tf = "15"; + s.script_tf = "15"; + s.ticker = "P"; + s.tickerid = "PROBE:P"; + s.type = "cfd"; + s.currency = "USD"; + s.timezone = "UTC"; + s.session = "24x7"; + s.initial_capital = 10000.0; + s.point_value = 1.0; + s.account_fx = 1.0; + s.price_tick = 0.01; + s.event_retention = NativeEventRetention::Full; + NativeMarginModel m; + m.initial_long = 0.5; + m.initial_short = 0.0; // maintenance-only short side + m.maintenance_short = 0.5; + m.sizing = NativeLiquidationSizing::RestoreMinimum; + m.check = check; + s.margin = m; + if (path == Path::LowerTf) { + IntrabarPath::lower_tf lower; + lower.bars = finer(); + lower.tf = "5"; + s.intrabar.value = lower; + } else if (path == Path::Synthesized) { + IntrabarPath::synthesized sampled; + sampled.samples = 8; + sampled.distribution = MagnifierDistribution::ENDPOINTS; + s.intrabar.value = sampled; + } + return s; +} + +void run(Probe& h, Path path, NativeLiquidationCheck check) { + const auto setup = h.configure_native(spec(path, check)); + CHECK(setup.status == NativeSetupStatus::Applied); + if (setup.status != NativeSetupStatus::Applied) return; + const auto bars = input(); + h.run(bars.data(), static_cast(bars.size())); + CHECK(h.last_error().empty()); +} + +void print(const char* label, const Probe& h) { + std::printf("%-30s BarOpen %d AfterApplied %d IntrabarSample %d calls %zu", label, + h.count(NativeMarginCheckKind::BarOpen), + h.count(NativeMarginCheckKind::AfterApplied), + h.count(NativeMarginCheckKind::IntrabarSample), h.calls.size()); + for (const auto& c : h.calls) + std::printf(" [bar %d units %.6g at %.4f]", c.cursor.point.interval_index, c.units, c.mark); + std::printf(" position %.6g\n", h.physical_position().signed_units); +} + +// 1. The points. +void offered_points() { + Probe lower; + run(lower, Path::LowerTf, NativeLiquidationCheck::PathAdverseExtremeMark); + print("lower_tf, mark check", lower); + CHECK(lower.count(NativeMarginCheckKind::BarOpen) == 5); + // Three retained sub-bars of four turning points each: every sample but + // each bar's first, at least on the bars no liquidation re-armed. + CHECK(lower.count(NativeMarginCheckKind::IntrabarSample) >= 5 * 11 - 1); + // Each point follows the last, and each sample is measured at its own + // price: inside its bar's range, bar 2's crossing at 106 before its 110. + const auto bars = input(); + std::uint64_t last = 0; + bool increasing = true; + bool in_range = true; + int at_106 = -1; + int at_110 = -1; + for (std::size_t i = 0; i < lower.points.size(); ++i) { + const auto& p = lower.points[i]; + if (p.ordinal < last) increasing = false; + last = p.ordinal; + if (p.kind != NativeMarginCheckKind::IntrabarSample) continue; + const Bar& b = bars[static_cast(p.bar)]; + if (p.mark < b.low || p.mark > b.high) in_range = false; + if (p.bar == 2 && p.mark == 106.0 && at_106 < 0) at_106 = static_cast(i); + if (p.bar == 2 && p.mark == 110.0 && at_110 < 0) at_110 = static_cast(i); + } + CHECK(increasing); + CHECK(in_range); + CHECK(at_106 >= 0 && at_110 > at_106); + + Probe chart; + run(chart, Path::Chart, NativeLiquidationCheck::PathAdverseExtremeMark); + print("chart, mark check", chart); + CHECK(chart.count(NativeMarginCheckKind::IntrabarSample) == 0); + + Probe calc; + run(calc, Path::LowerTf, NativeLiquidationCheck::CalculationOnly); + print("lower_tf, CalculationOnly", calc); + CHECK(calc.count(NativeMarginCheckKind::IntrabarSample) == 0); + CHECK(calc.count(NativeMarginCheckKind::BarOpen) == 0); +} + +// 2. Where the call lands. +void call_at_the_crossing_sample() { + Probe chart; + run(chart, Path::Chart, NativeLiquidationCheck::PathAdverseExtremeMark); + Probe lower; + run(lower, Path::LowerTf, NativeLiquidationCheck::PathAdverseExtremeMark); + // The whole-bar model: one call at bar 2's extreme. + CHECK(!chart.calls.empty()); + if (!chart.calls.empty()) { + CHECK(chart.calls[0].cursor.point.interval_index == 2); + CHECK(chart.calls[0].mark == 110.0); + } + // Per sample: the first call at the sample that crossed, 106, sized there + // -- a smaller slice than the whole-bar one, then whatever the deeper + // samples still find on the reduced book. + CHECK(!lower.calls.empty()); + if (!lower.calls.empty() && !chart.calls.empty()) { + CHECK(lower.calls[0].cursor.point.interval_index == 2); + CHECK(lower.calls[0].mark == 106.0); + CHECK(lower.calls[0].units < chart.calls[0].units); + for (const auto& c : lower.calls) CHECK(c.cursor.point.interval_index == 2); + } +} + +// 3. A host that refuses the new points keeps the old behaviour. +void refused_samples() { + Probe h; + h.refuse_samples = true; + run(h, Path::LowerTf, NativeLiquidationCheck::PathAdverseExtremeMark); + print("lower_tf, samples refused", h); + CHECK(h.count(NativeMarginCheckKind::IntrabarSample) > 0); + CHECK(h.calls.empty()); + CHECK(h.physical_position().signed_units == -174.0); +} + +// 4. A one-price synthesized path. +void synthesized_samples() { + Probe h; + run(h, Path::Synthesized, NativeLiquidationCheck::PathAdverseExtremeMark); + print("synthesized, mark check", h); + CHECK(h.count(NativeMarginCheckKind::BarOpen) == 5); + CHECK(h.count(NativeMarginCheckKind::IntrabarSample) == 0); +} + +} // namespace + +int main() { + offered_points(); + call_at_the_crossing_sample(); + refused_samples(); + synthesized_samples(); + std::printf("test_native_margin_intrabar_samples: %d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} From 23e5a1749968bee4d395658cb722a2fb5eea4670 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 12:18:45 +0800 Subject: [PATCH 41/83] PAR-MARGIN: the ctest floors count the lane's rows -- release 678 -> 680, kernel 277 -> 278 - kernel +1: test_native_margin_intrabar_samples (item 3, source-free, so it registers in release too); - release +1 more: test_adapter_margin_schedule_differential, the row the lane cherry-picked from H-MEASURE's bafefba0 for its pins (H-MEASURE's own floor commit counts it again when it lands whole). Counted with ctest -N on the lane's tree (661 registered in a local build without examples). The PR-only registration floors (EXCLUDED_REGISTERED_MIN, pinned by test_ci_verify) stay minimums below the new populations. docs/ci.md's full-run floors follow; the six citations of the two constants (CONTRIBUTING.md, contributing-llm.md) point at their new lines. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 38442cecec6637655ade7868cc27eff5b7425fcc) INT26 pick note: the floors add this lane's one row to the merged values (KERNEL 283 -> 284, RELEASE 698 -> 699, after PAR-ORDERS-2's picks). The lane's release count of 680 included H-MEASURE's test_adapter_margin_schedule_differential, which the lane had cherry-picked; H-MEASURE's own floor commit (f6a86083) counts it here, so it is not counted twice. The four floor citations name the merged lines (ci_verify.py:259 and :401). INT26 item (iv) recounts every floor with ctest -N per profile. --- CONTRIBUTING.md | 4 ++-- docs/ci.md | 2 +- docs/pages/contributing-llm.md | 6 +++--- scripts/ci_verify.py | 12 ++++++++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2991fb5..b30ea331 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -152,8 +152,8 @@ What each gate refuses: ### The floors `ci_verify.py` counts the CTest rows that actually **ran** and fails below a -floor — `KERNEL_MIN_TESTS` ci_verify.py:255 and `RELEASE_MIN_TESTS` -ci_verify.py:393. +floor — `KERNEL_MIN_TESTS` ci_verify.py:259 and `RELEASE_MIN_TESTS` +ci_verify.py:401. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/docs/ci.md b/docs/ci.md index 7e857c0b..09acf22c 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -102,7 +102,7 @@ requires the rows that actually ran to equal the second count, and records registration below the PR registration floor, unreadable enumeration, or missing label fails. The PR registration floors at INT25 are 659 for Debug and sanitizers and 668 for native (653 and 662 at the INT24 base, plus wave G's -six rows). The full-run release and kernel floors are 678 and 277 rows that +six rows). The full-run release and kernel floors are 680 and 278 rows that ran; full runs do not exclude a label. Preflight also runs the detached-comment census of the kernel compile closure diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 0c48804c..b1cfdcf2 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -117,8 +117,8 @@ finding to report, not a step to take. `scripts/check_doc_reverts.py`. 10. **A test row never silently disappears.** Each profile counts the rows that - *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:255 and - `RELEASE_MIN_TESTS` ci_verify.py:393. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:259 and + `RELEASE_MIN_TESTS` ci_verify.py:401. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -264,7 +264,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:255, `RELEASE_MIN_TESTS` ci_verify.py:393). It +(`KERNEL_MIN_TESTS` ci_verify.py:259, `RELEASE_MIN_TESTS` ci_verify.py:401). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index a6462ad8..6e592eba 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -252,7 +252,11 @@ # 283 = those 282 plus R5 lane PAR-ORDERS-2's source-free # test_native_current_cohort_refusal (it registers in release too; INT26 # pick, summed). -KERNEL_MIN_TESTS = 283 +# 284 = those 283 plus R5 lane PAR-MARGIN's source-free row (INT26 pick, +# summed): +# +1 PAR-MARGIN test_native_margin_intrabar_samples (IntrabarSample, item 3) +# It registers in release too. +KERNEL_MIN_TESTS = 284 # Release-row floor, the same gate for the default profile. Before lane P7 # only the kernel profile had one, so a row that left release alone (a # source-bound TU dropped from TEST_SOURCES, a deleted twin or ABI row) left a @@ -390,7 +394,11 @@ # (source-bound; INT26 pick, summed). # 698 = those 697 plus R5 lane PAR-ORDERS-2's test_pooc_reversing_stop_tapes # (source-bound; INT26 pick, summed). -RELEASE_MIN_TESTS = 698 +# 699 = those 698 plus R5 lane PAR-MARGIN's kernel row above (INT26 pick, +# summed). The lane's own count of 680 also counted H-MEASURE's +# test_adapter_margin_schedule_differential, which it had cherry-picked for its +# pins; H-MEASURE's twelve above already count that row. +RELEASE_MIN_TESTS = 699 # R5 lane H-DOCGATES (AUDIT4-opus perf N-7) raises both floors by one more: # test_native_runtime_ambient_lifo, the runtime blocks' LIFO death row, is # source-free, so it registers and runs in the kernel profile (279 -> 280) and From 3fd07f70fe4caa9529267bd00164e460c59d1301 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:32:26 +0800 Subject: [PATCH 42/83] Kernel: a post-fill margin check faces the waypoint its fill was reached on the way to; v19 re-pin: test_publication_witness Config03 (R5 lane PAR-MARGIN-2) A resting request is matched at the driver point its segment reaches: a buy limit at 95 on a bar O100 H101 L80 C81 fills on the segment from the high to the low and is presented at the low's point. The AfterApplied re-arm measured only the waypoints AFTER that point's phase, so the book the fill left was measured at the close (81) and never at the low it was still falling toward; on a low-first bar, or for a short filled on its way up to the high, the breach was not measured at all. The re-arm now measures from the segment's origin (NativeExecutionConsumer::margin_segment_origin), with the fill price as the point's own mark, the idiom fx_roll_margin_check_at already uses for a continuous roll. The adapter's schedule_margin_call_path answers on the same path. ADR-0001 rule 2: a mechanism correction, no knob, nothing that knows a venue (the new rule-2 row). TradingView (12 lab tv exports, BINANCE:ETHUSDT.P 15m, ws-report-v1, rangeProof covered; tests/fixtures/margin_entry_bar): a limit opening filled on its way down is called on its own bar at the LOW (pm2-m7-lim-*, 4 of 4, a low-first bar among them), a short limit opening filled on its way up at the HIGH, leveraged and 1x (pm2-m7-slim-*, pm2-m7-s1lim-*, 8 of 8). The adapter booked 0 of 12 (the call a bar late, or none); it books 12 of 12. ab9714be's entry-bar suffix books the same (the differential's M7-B pin, kernel side: 8.888.. @81 -> 10 @80; the adapter still refuses an add's point until the next commit). v19 re-pin (the lane's only hash-witness move), harvested with -DPINEFORGE_D2D_PUBLICATION_HARVEST: tests/test_publication_witness_pinned.inc Config03: 0x461a919ebdcd41e9 (144 folds, 50 trades) -> 0x4593ef08ab33e045 (144, 48) a short opened by a sell limit on its way up a low-first bar is now called on that bar at its high (4 @112.5, where it booked 2 @112.25 there and 6 @114.75 a bar later); the other 155 runs keep their digest and counts. Test data (not a hash value; the transcript folds none), harvested with -DPINEFORGE_V19E_HARVEST: tests/test_adapter_live_state_equivalence_data.hpp chains seed 1570937 10446321169177622098 -> 14248979107259646644 (a limit long called on its own bar at the low, a bar earlier); 107 unchanged. New kernel-only row tests/test_native_margin_post_fill_path.cpp (27 checks): fail-before against the frozen v18 header closure (host-fc7aad6): "test_native_margin_post_fill_path.cpp:96:7: error: no member named 'event_retention' in 'pineforge::NativeRunSpec'"; against 38442cec's library 6 failures (marks 81 / 95 / 105, no call on the low-first bar or for the short). Floors (scripts/ci_verify.py), for that row: kernel 278 -> 279, release 680 -> 681. tests/test_adapter_margin_schedule_differential.cpp: a data-driven "M7 shapes" section replays each tape from its OWN strategy.pine (bars: tests/fixtures/margin_entry_bar/pm2_bars.inc, the corpus feed). Docs: native-engine.md (arming), ADR-0001 (rule-2 row), the fixture README; anchors carried by an exact line map, the two floor anchors by hand. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 8876c1087457357ea225b4bdf37e0fffa50d1867) INT26 pick note: picked right after PAR-MARGIN (the late-picks list; its base is PAR-MARGIN's 38442cec). The lane's hash re-pin folds into the single INT26 v19 hash re-pin, so Config03's digest keeps its pinned value here and moves there; the behaviour this commit moves is pinned here, harvested on this tree with each TU's own switch: Config03 closes 48 trades (was 50), and live-state chains seed 1570937's transcript moves (15204985627708005050 -> 16038855060654239956; the integrated tree's own value, since the lanes before it had already moved that digest; the label's counts are unchanged, as on the lane's tree). No other pinned behaviour moves: quiet-bar and publication trades and counts, and every other witness, harvest as pinned but for the hash values PAR-ORDERS-2's fold already moves. ADR-0001's new rule-2 row follows PAR-MARGIN's IntrabarSample row, above H-DOCGATES' inventory rows; the docs merged on anchor digits otherwise; the floors are summed (kernel 284 -> 285, release 699 -> 700) and their four citations name the assignment lines. --- CONTRIBUTING.md | 4 +- docs/adr/0001-kernel-adapter-boundary.md | 21 +- docs/design/native-feature-parity.md | 110 ++-- docs/pages/contributing-llm.md | 6 +- docs/pages/native-engine.md | 24 +- docs/pages/pine-to-native.md | 6 +- scripts/ci_verify.py | 11 +- src/native_execution_consumer.cpp | 27 +- src/native_execution_consumer.hpp | 6 + src/source/pine_adapter.cpp | 9 +- tests/CMakeLists.txt | 6 + tests/fixtures/margin_entry_bar/README.md | 83 ++- .../pm2-m7-lim-0402-1945/meta.json | 16 + .../pm2-m7-lim-0402-1945/metrics.json | 40 ++ .../pm2-m7-lim-0402-1945/strategy.pine | 13 + .../pm2-m7-lim-0402-1945/tv_trades.csv | 5 + .../pm2-m7-lim-0402-2000/meta.json | 16 + .../pm2-m7-lim-0402-2000/metrics.json | 40 ++ .../pm2-m7-lim-0402-2000/strategy.pine | 13 + .../pm2-m7-lim-0402-2000/tv_trades.csv | 5 + .../pm2-m7-lim-0801-0030/meta.json | 16 + .../pm2-m7-lim-0801-0030/metrics.json | 40 ++ .../pm2-m7-lim-0801-0030/strategy.pine | 13 + .../pm2-m7-lim-0801-0030/tv_trades.csv | 5 + .../pm2-m7-lim-1201-0215/meta.json | 16 + .../pm2-m7-lim-1201-0215/metrics.json | 40 ++ .../pm2-m7-lim-1201-0215/strategy.pine | 13 + .../pm2-m7-lim-1201-0215/tv_trades.csv | 5 + .../pm2-m7-s1lim-0402-1330/meta.json | 16 + .../pm2-m7-s1lim-0402-1330/metrics.json | 40 ++ .../pm2-m7-s1lim-0402-1330/strategy.pine | 12 + .../pm2-m7-s1lim-0402-1330/tv_trades.csv | 5 + .../pm2-m7-s1lim-0509-1100/meta.json | 16 + .../pm2-m7-s1lim-0509-1100/metrics.json | 40 ++ .../pm2-m7-s1lim-0509-1100/strategy.pine | 12 + .../pm2-m7-s1lim-0509-1100/tv_trades.csv | 5 + .../pm2-m7-s1lim-0709-1930/meta.json | 16 + .../pm2-m7-s1lim-0709-1930/metrics.json | 40 ++ .../pm2-m7-s1lim-0709-1930/strategy.pine | 12 + .../pm2-m7-s1lim-0709-1930/tv_trades.csv | 5 + .../pm2-m7-s1lim-1001-0830/meta.json | 16 + .../pm2-m7-s1lim-1001-0830/metrics.json | 40 ++ .../pm2-m7-s1lim-1001-0830/strategy.pine | 12 + .../pm2-m7-s1lim-1001-0830/tv_trades.csv | 5 + .../pm2-m7-slim-0402-1330/meta.json | 16 + .../pm2-m7-slim-0402-1330/metrics.json | 40 ++ .../pm2-m7-slim-0402-1330/strategy.pine | 12 + .../pm2-m7-slim-0402-1330/tv_trades.csv | 5 + .../pm2-m7-slim-0509-1100/meta.json | 16 + .../pm2-m7-slim-0509-1100/metrics.json | 40 ++ .../pm2-m7-slim-0509-1100/strategy.pine | 12 + .../pm2-m7-slim-0509-1100/tv_trades.csv | 5 + .../pm2-m7-slim-0709-1930/meta.json | 16 + .../pm2-m7-slim-0709-1930/metrics.json | 40 ++ .../pm2-m7-slim-0709-1930/strategy.pine | 12 + .../pm2-m7-slim-0709-1930/tv_trades.csv | 5 + .../pm2-m7-slim-1001-0830/meta.json | 16 + .../pm2-m7-slim-1001-0830/metrics.json | 40 ++ .../pm2-m7-slim-1001-0830/strategy.pine | 12 + .../pm2-m7-slim-1001-0830/tv_trades.csv | 5 + tests/fixtures/margin_entry_bar/pm2_bars.inc | 537 ++++++++++++++++++ ...st_adapter_live_state_equivalence_data.hpp | 9 +- ...t_adapter_margin_schedule_differential.cpp | 351 +++++++++++- tests/test_native_margin_post_fill_path.cpp | 222 ++++++++ tests/test_publication_witness.cpp | 17 +- tests/test_publication_witness_pinned.inc | 7 +- 66 files changed, 2180 insertions(+), 156 deletions(-) create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2_bars.inc create mode 100644 tests/test_native_margin_post_fill_path.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b30ea331..9e0d80f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -152,8 +152,8 @@ What each gate refuses: ### The floors `ci_verify.py` counts the CTest rows that actually **ran** and fails below a -floor — `KERNEL_MIN_TESTS` ci_verify.py:259 and `RELEASE_MIN_TESTS` -ci_verify.py:401. +floor — `KERNEL_MIN_TESTS` ci_verify.py:264 and `RELEASE_MIN_TESTS` +ci_verify.py:408. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 13c0df50..fefc5ecc 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -77,7 +77,7 @@ summary of it. (`native_host.hpp:893`); also `on_native_input` (`native_host.hpp:875`), `on_native_tick` (`native_host.hpp:878`), `on_native_timeframe_bar` (`native_host.hpp:885`), `on_native_bar_open` — at the modeled opening, before its matching pass - (`native_host.hpp:889`, `on_native_bar_open` `native_execution_consumer.cpp:7063-7065`) — + (`native_host.hpp:889`, `on_native_bar_open` `native_execution_consumer.cpp:7084-7086`) — `on_native_recalculate` for every calculation of the run (`native_host.hpp:913`), `on_native_sub_bar` (`native_host.hpp:928`) and post-fill `on_native_applied` (`native_host.hpp:938`). `on_bar` is `final` (`native_host.hpp:857`). @@ -111,7 +111,7 @@ summary of it. bare host needs no override at all, while `HostSized` hands the units to the host — and an unresolved `HostSized` whose `resolve_execution_terms` (`native_host.hpp:947`) returns no units is a `TermsUnresolved` rejection - (`TermsUnresolved` `native_execution_consumer.cpp:4964`). + (`TermsUnresolved` `native_execution_consumer.cpp:4976`). - **Observing executions.** Implement `on_native_applied` (`native_host.hpp:938`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` (`native_host.hpp:1329`). Do *not* implement @@ -122,7 +122,7 @@ summary of it. | What | Surface | |---|---| | Run state | `native_state()` → `NativeStateView` — kind, phase, `NativeCompletion` BatchComplete / StreamEnded, failure, high water (`NativeCompletion` `native_host.hpp:50`, `NativeStateView` `:282`, `NativeRunPhase` `:41`) | - | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:4937`) | + | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:4949`) | | Cohorts | `cohort_open` (`native_host.hpp:1282`) / `cohort_add` (`:1285`) / `cohort_remove` (`:1288`) — the handle a `BindCohort` owner names | | Mid-callback execution | `current_execution_point` (`native_host.hpp:1087`), `inspect_current_execution` (`native_host.hpp:1099`) / `execute_current` (`native_host.hpp:1104`) | | Margin seams | four, not one: the run spec's own `margin` model (`NativeMarginModel` `native_run_spec.hpp:240`) with its three hooks — `resolve_margin_requirement` (`native_host.hpp:976`), `margin_check_allowed` (`native_host.hpp:988`), `resolve_margin_call_units` (`native_host.hpp:995`) — and `validate_execution_precommit` (`native_host.hpp:960`), whose `AdmitWithHostMargin` verdict hands the opening check to the host | @@ -261,7 +261,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:16863`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16868`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -551,6 +551,7 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | the order of one cause's group-effect receipts -- the queue order `WorkingRequestCore::group_recipients` walks, which a `ReplaceOptions::keep_handle` re-price parts from incarnation order: `receipt_lookup` bisects the receipts by cause and walks that cause's, and `prepare_group_effect` / `apply_group_effect` refuse only a cause older than the newest receipt's (`src/native_order.cpp`; `WorkingRequestCore::group_effect_receipt`, `include/pineforge/native_order.hpp`) | any C++ host that re-prices a group member with `keep_handle` and then fills a sibling (`NativeStrategyHost::replace(handle, request, options)`; no C spelling, the C replace takes no options); the Pine adapter never sets `keep_handle` | **generic: a drain applies a group's effect in queue order, whatever the handles' numbers** (R5 lane K-OCA-KEEP, K-ULP5's finding 1). Rule 2 (a), mechanism: which live siblings a member's fill reaches, and in what order, is the book's own queue for a host of any venue, and the receipts are that drain's idempotence record, in the order it applied them; a request re-priced with its handle kept is reached where a plain replace's successor is, so the two runs record one timeline, as V19-D's contract already said. Rule 2 (b), knob: none -- it replaces a whole-run `Contract` failure (code 2, discriminator 1, `CoreFailure::InvalidCause`, after the member's fill was booked) that no host could want: the receipt check held one cause's receipts to incarnation order, and the re-priced member, drained behind a younger sibling, was refused; a cause older than the newest receipt's is still refused. What moved: runs that stopped on such a drain now complete -- K-ULP5's `probe_keep_handle_oca`, staged and direct, under `Cancel` and `Reduce`, and all 180 runs of the seeded battery under a keep_handle option set (of its 360), which stopped on 2a03c658 -- each recording the plain replace's run event for event. No completed run moves and no hash value moved: a completed run's receipts were already in incarnation order within each cause, and the lookup answers such a store as before (the K-ULP4 and K-ULP5 batteries' per-run hashes unchanged; corpus parity unchanged by construction, the Pine adapter never setting `keep_handle`). Pinned by `tests/test_native_group_keep_handle.cpp` (kernel-only). | | a request on a quantity grid whose pending group deduction its units absorb -- it settles them all in one fill (`settles_in_one_fill`, `src/native_execution_consumer.cpp`, which asks `WorkingRequestCore::effective_host_units` whether the pending total takes any), so a whole-scope `ScopeFraction` resolves to its scope's held total unfloored and a host-sized close answered with that total is its own quantity, at the match and at the current execution alike | any host, by the OCA-Reduce groups and the quantity grid it declares (C: a fraction, and a close sized by `on_close_units`); the Pine adapter does not reach it: it emits no `ScopeFraction`, and it answers a close of its scope's whole total with `ExecutionGridPolicy::ExplicitUnits`, whose grid check never asks whether the request settles in one fill | **one definition of settling in one fill** (R5 lane K-OCA-KEEP, K-ULP5's finding 2). Rule 2 (a), mechanism: K-ULP4's grid rule admits the book's own quantity for a request that settles it in one fill, and K-ULP5's absorption makes a sub-ulp pending total take nothing, for a host of any venue; the two now agree. Rule 2 (b), knob: none. What moved: a run in which such a request closes its scope's total now closes it whole -- a whole-scope fraction was floored onto the grid and closed short, leaving a dust lot (before K-ULP5 such a run stopped wherever the floored units absorbed the total too, code 6, discriminator 7), and a host-sized close answered with its scope's total was refused `InvalidTerms` and left the lot open, before K-ULP5 as after: the `grid-absorbed-*` rows of `tests/test_native_group_keep_handle.cpp`, whose fraction closed 1.1000000000000001 of a 1.1000000000000227 lot on 2a03c658. K-ULP4's own battery answers every scenario's hash as before, K-ULP5's absorption and C-host batteries are unchanged (no grid), `grid-fraction-oca-pending` of `tests/test_native_unrepresentable_refusal.cpp` (a pending total that takes 0.3) is still floored, and no pinned hash moved. Pinned by `tests/test_native_group_keep_handle.cpp` (kernel-only). | | the margin model's check points on an intrabar path -- `NativeMarginCheckKind::IntrabarSample` (`include/pineforge/native_host.hpp`; C `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE`), offered at every delivered sample after the script bar's first of a `lower_tf` path matched as `ContinuousSegments`, at that sample's price, immediately before it is matched (`NativeExecutionConsumer::deliver_intrabar_script`, `src/native_execution_consumer.cpp`) | every host with a margin model and such a path, through `margin_check_allowed`; the Pine adapter admits it on a magnified run's leveraged book (`PineExecutionAdapter::intrabar_sample_checked`, `src/source/pine_adapter.cpp`) | **a check point of the kernel's own** (R5 lane PAR-MARGIN, item 3; H-MEASURE's acid finding A). Rule 2 (a), mechanism: a run with no whole-bar waypoint model measures its maintenance requirement where its path is -- at each delivered sample -- the rule the docs and `native_run_spec.hpp` stated and the kernel had not kept (it checked the first sample and after fills only); nothing in it knows a venue, and a host that checks somewhere else suppresses the points it does not share, as for every other kind. Rule 2 (b), knob: none -- the path is the run's own declaration, and a one-price distribution path (`synthesized`, `DistributionSamples`) is not offered the point, since a request armed at a discrete point is matched only at a later one. What moved: a margin run on a continuous intrabar path whose book crosses its line inside a bar now books that call at the crossing sample (H-MEASURE's intrabar probe: 55 points offered, a slice at 105 where none was booked). TradingView's magnified broker does the same (`tests/fixtures/intrabar_margin`: the first call at the first lower-timeframe low that crosses on 4 of 4 tapes, every row on 3). No pinned value moved. Pinned by `tests/test_native_margin_intrabar_samples.cpp` (kernel-only) and the "intrabar margin on tapes" section of `tests/test_adapter_margin_schedule_differential.cpp`. | +| the path a post-fill margin check measures -- `NativeExecutionConsumer::margin_segment_origin` (`src/native_execution_consumer.cpp`), the phase the `AfterApplied` re-arm hands `margin_sizing_price`: the waypoint the segment into the fill's driver point starts from, with the fill price as the point's own mark | every host with a margin model on a path walked by its modeled waypoints (no intrabar path), through the `AfterApplied` point `margin_check_allowed` admits | **a mechanism correction** (R5 lane PAR-MARGIN-2). Rule 2 (a), mechanism: a request matched at a driver point was reached on the segment into it, so the book its fill leaves still faces that point's waypoint -- a limit filled on its way down to the bar's low faces the low, a short filled on its way up faces the high -- and the scan that started strictly after the point's phase measured a path the book had not finished; the waypoints the path already passed stay out, as `fx_roll_margin_check_at`'s continuous roll measures from its segment's origin. Nothing in it knows a venue. Rule 2 (b), knob: none. What moved: a post-fill point after a fill matched on a segment now measures that segment's destination waypoint -- on a high-first bar O100 H101 L80 C81 a buy limit at 95 measures 80 where it measured the close 81, and a low-first bar or a short filled on its way up is called where nothing was (`tests/test_native_margin_post_fill_path.cpp`, kernel-only); a fill at a waypoint's own price, the open's included, measures what it did. TradingView books that call on the fill's bar (lab tv tapes `tests/fixtures/margin_entry_bar/pm2-m7-lim-*` at the low, `pm2-m7-slim-*` and `pm2-m7-s1lim-*` at the high, 12 of 12), as `ab9714be`'s entry-bar suffix did; the differential's M7-B pin moved to that answer, 10 @80. One v19 witness value moved with it: `tests/test_publication_witness.cpp` Config03, a short filled on its way up a bar, now called at that bar's high (461a919ebdcd41e9 -> 4593ef08ab33e045); the corpus is 312/312, moved 0. | | the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:1825` and `:1934`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:295`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | | the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1478`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | | the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1616`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | @@ -713,7 +714,7 @@ number names its symbol on this tree. | `strategy_configure_native_v1` (`c_abi.cpp:896`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:3982`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | | `strategy_configure_native_fx_curve_v1` (`pineforge.h:584`) | **typed additive route** | `strategy_configure_native_fx_curve_ext_v1` writes the C twin of `NativeFxCurveValidation` and its offending index. The old `-1` route remains unchanged, while a C caller can now distinguish a decreasing timestamp, non-positive rate, allocation failure or wrong phase. | | `on_native_timeframe_bar` (`native_host.hpp:885`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:809`) | **query routed** | The callback signature stays frozen. `strategy_native_timeframe_bar_interval_v1` is valid only during that callback and copies the kernel calendar interval without deriving it from the delivered bar or delivery timestamp. | -| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7629-7641`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1085-1090`) sees the same values, so no hash contract moves. | +| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7665-7677`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1085-1090`) sees the same values, so no hash contract moves. | | `pf_native_lot_excursion_v1` (`native_c_api.h:1876`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | ## TradingView-calibrated kernel mechanisms (rule 2's own account) @@ -771,7 +772,7 @@ The table records, at the tree the doc wave read, what each block described and | `ta::ATR` `include/pineforge/ta.hpp:321` (the `TradingView` comment `sha256:4f6f6939aec2773684b114a13dd002eb58990d6d96c3056ad87361753faf7a0a` :333-340) | average true range over Wilder's `RMA` | Wilder's ATR is the textbook definition; the comment records which convention and the tape that pinned the recompute behaviour | a seeding/averaging field on an indicator whose definition is the convention | `tests/test_ta_rma_warmup.cpp`, `tests/test_recompute.cpp` | | `ta::StdDev` `include/pineforge/ta.hpp:430` (the `TradingView` comment `sha256:7cc1bbcd32dfe673285d51a8ca7080890ac551858099b523480d998f46408666` :420-429) | the population (biased) standard deviation | biased vs sample is a textbook choice, and the kernel states which | a per-instance switch is the *right* answer where two conventions genuinely compete, which is why `ta::EmaSeeding` exists and this one does not need it | `tests/test_ta_indicators_extras.cpp` | | `float_band_eq` `include/pineforge/ta_compare_band.hpp:38` | an absolute 1e-10 float comparison for indicator equality | an epsilon band is needed by any float comparison; the width is the calibration | a band-width field on a comparison the indicators share | `tests/test_dmi_parity.cpp`, `tests/test_ta_osc_edge.cpp` | -| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8587-8590` (lookahead) and `clear_if_gapped` `:8581-8584` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | +| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8639-8642` (lookahead) and `clear_if_gapped` `:8633-8636` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | | `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2235`, `::gaps` `include/pineforge/native_c_api.h:2239` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:795`, `pf_native_gaps_e` `:806`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | | the authoritative-feed partition `src/engine_aux_security.cpp` | a feed is the venue's own bars of one timeframe; its stamps are the period partition | already ruled: design §2.ii row s, and the residual table's own row | the policy knob is installing the feed or not | `tests/test_native_htf_subscriptions.cpp`, `tests/test_native_security_feed.cpp` | | `session_template_knows_early_close` `src/engine_security.cpp` | the instrument-class classification | already ruled: design §2.ii row t; `SymInfo::type`'s vocabulary is fixed by the frozen C ABI | — | `tests/test_native_wm_buckets.cpp` | @@ -785,10 +786,10 @@ The table records, at the tree the doc wave read, what each block described and | the indicator library -- `include/pineforge/ta.hpp`, `src/ta_moving_averages.cpp`, `src/ta_oscillators.cpp`, `src/ta_extremes_volume.cpp`, `src/ta_volatility_trend.cpp`, `src/ta_misc.cpp`, `include/pineforge/window_sum.hpp`, `include/pineforge/math.hpp` | each indicator implements its Pine `ta.*` / `math.*` definition -- warm-up and na rules, the window call-site ring, the compensated window sum, tie and epsilon rules -- and its comments record the TradingView tape or export that calibrated each number (the `ta::ATR` and standard-deviation rows above are two of them) | an indicator is a function of its inputs, the same for every venue; generated strategies call exactly these classes, and where two textbook conventions genuinely compete the choice is a per-instance option (`ta::EmaSeeding`), never a branch on the platform | a convention switch on every indicator, for a choice the script already made by calling that Pine function | the kernel-only TA suites `tests/test_ta_*.cpp` and the corpus parity gate | | the formatting library -- `include/pineforge/str_utils.hpp`, `src/str_utils.cpp` (`str_tostring`, `str_format_values`, `str_format`) | the number text of Pine's `str.tostring` / `str.format` built-ins: a number's shortest round-trip decimal spelling, scaled and rounded half-up on its digits, the default, `percent`, `volume` (K / M / B / T) and pattern modes, MessageFormat placeholders and quoting, `NaN` and `Infinity` -- the published rendering TradingView's exported tapes pin (R5 lane C6), ported from the codegen formatter (lane C6b) by R5 lane B-ENGINE; the `mintick` mode, which generated code delegates to it, is kept as it was | a formatting library is a function of its inputs, the same for every venue, as the indicator library is: it spells a number the way the source language's own `str.*` library defines, and no kernel order, fill, settlement, margin or hash code calls it -- of the thirty-five kernel translation units only `src/str_utils.cpp` names these functions | a rendering switch on functions whose definition the calling script chose by calling them | `tests/test_str_number_format.cpp` (lane C6b's 42-case edge battery, every numeric string of lane C6's TradingView exports, byte-identical output on macOS arm64 and Linux x86-64) and `tests/test_str_utils.cpp` | | the language calendar functions -- `include/pineforge/session_time.hpp`, `src/session_time.cpp` (`timeframe_time`, `local_hour` and its siblings, `session_in_market`, `session_trading_day_open_ms`, ...) | the definitions of Pine's `time()` / `time_close()` / bare-time / `session.is*` built-ins for generated code: session and timezone parsing, the invalid- and 24-hour-session spellings, `time("60")` day keying, `session.is*` on daily charts, each comment naming the tape that pinned it | the inputs are data -- a session string, a timezone, a timeframe -- so another venue is driven by its own session template; nothing branches on a platform, and a native host's calendar is the separate generic `native_calendar` | a rules switch on functions whose definition the generated script chose by calling them | `tests/test_session_time.cpp`, `tests/test_pine_time_day_stamp_grid.cpp` and the corpus parity gate | -| `present_session_day` `src/native_execution_consumer.cpp:7269`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (`tests/fixtures/session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | `tests/test_native_session_day_facts.cpp` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), the C scenario of `tests/test_native_c_api.c`, and `tests/test_session_day_facts_adapter.cpp` (the adapter selects them flag for flag) | -| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7893-7909` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | -| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7870-7891` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | -| `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6606` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | +| `present_session_day` `src/native_execution_consumer.cpp:7286`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (`tests/fixtures/session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | `tests/test_native_session_day_facts.cpp` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), the C scenario of `tests/test_native_c_api.c`, and `tests/test_session_day_facts_adapter.cpp` (the adapter selects them flag for flag) | +| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7910-7926` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | +| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7887-7908` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | +| `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6618` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | | `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:6342` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | | the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:334`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1541`, `:1556`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:1854`). A bare host never calls the builder, so its calendar stays nominal | `tests/test_o_close_pct_day_anchor.cpp` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | | the bar-time accessors -- `_decompose_bar_time` and `_bar_hour` through `_bar_weekofyear` `include/pineforge/engine.hpp:1066-1093` | the presented bar's stamp broken into calendar fields in UTC, memoized per stamp | a function of the stamp alone, and UTC is the engine's storage timezone | none: they read no spec field and fold into no digest. They are protected `BacktestEngine` members, part of the codegen contract. Their comment claimed that generated code reads them for Pine's bare `hour` / `minute` / `dayofweek`. It does not: the codegen transpiler emits `pine_hour(current_bar_.timestamp, syminfo_.timezone)` and its siblings on the exchange timezone (`pineforge_codegen/codegen/tables.py` at the corpus's pinned commit `f3285d79`), which are the language calendar functions above. No generated strategy in `corpus/` names a `_bar_*` accessor. The comment now says so (R5 lane H-DOCGATES) | `tests/test_chart_timezone.cpp` (`_bar_hour()` is the UTC hour) and `tests/test_intraday_rollover_chart_tz_l4a.cpp` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index a2eac908..52b4f63e 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,28 +47,28 @@ 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:2738-2749; `entry()` pine_adapter.cpp:5311, `order()` pine_adapter.cpp:10512) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5686-5733; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2738-2749; `entry()` pine_adapter.cpp:5311, `order()` pine_adapter.cpp:10512) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5698-5745; `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:8417 → `Flatten{}` pine_adapter.cpp:8446-8447; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2708-3915 → `host.replace` pine_adapter.cpp:2724-2804) | **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:4723-4769, `cancel_all` pine_adapter.cpp:18091-18106; `live_by_source_key_` pine_adapter.hpp:1999; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **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. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4723-4769, `cancel_all` pine_adapter.cpp:18096-18111; `live_by_source_key_` pine_adapter.hpp:1999; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5535-5570, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7505-7522) | **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:2747-2753) | **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:13035; lowering `submit_one` pine_adapter.cpp:9072-9084, `BindCohort` pine_adapter.cpp:9573-9579, `submit_leg` pine_adapter.cpp:9597-9621; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8677-8680; `materialize_relative_exits` pine_adapter.cpp:17381-17422, pine_adapter.hpp:1824-1836) | **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:5612-5646. `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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19162-19191) | **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:2001-2019); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10912-10936), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1585-1608, `CloseCallsiteState` pine_adapter.hpp:1679-1694; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15690, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15924) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8677-8680; `materialize_relative_exits` pine_adapter.cpp:17386-17427, pine_adapter.hpp:1824-1836) | **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:5624-5658. `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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19167-19196) | **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:2001-2019); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10912-10936), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1585-1608, `CloseCallsiteState` pine_adapter.hpp:1679-1694; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15695, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15929) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). What `tests/test_adapter_margin_revival_cancel.cpp` replays on the NYSE:F 15m chart of seven lab tv tapes: badapter-v19dp1-control (no cancel: the margin call closes 928 through X) and v19fix-gapped-stop-control (X's stop at 10.15, gapped by the 05-01 open and parked: the same close), and badapter-v19dp1-cancel, v19fix-cancel-before-pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair (a cancel on 05-01 09:30 while X is dormant, on 04-30 15:45 before the reversal pair and on 04-30 10:00 while X is live, and on 05-01 09:30 and after the pair while X is parked; one tape file: margin calls 12 @10.39 and 92 @10.57, 836 open). Untaped, the same row holds the tapes' trades for `strategy.cancel_all` on 05-01 and before the pair and for `strategy.cancel` after the pair, and on the same bars with no quantity grid — where the pair holds X in the cancel's own callback — every spelling books what the cancel on the dormant exit books | ### 1.2 Trailing | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:5698-5720; `NativeTrailState` native_host.hpp:669-674, `trail_state` native_host.hpp:1092 | K | — | F:B1 O:B1 S:T1 | — | +| TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:5710-5732; `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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11811-11823) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4452-4457) **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:11724-11861) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11811-11823) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4464-4469) **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:11724-11861) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4022-4068) | **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:287-289) is A. | | TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9623-9628, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11724-11861, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8647-8873, `trail_one_shot` pine_adapter.cpp:9712-9841 | n/a | A | — | F:B3 S:T2 | R5-1 | @@ -77,16 +77,16 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5924-5928, pine_adapter.cpp:5924) | **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:10989-12193) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4923-4957 | 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:2596-2651) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4964-4966 **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: rounded equity `source_money_round` pine_adapter.cpp:2548, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | +| SZ2 | Host override of the resolved price / units (`resolve_terms` pine_adapter.cpp:10989-12193) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4935-4969 | 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:2596-2651) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4976-4978 **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: rounded equity `source_money_round` pine_adapter.cpp:2548, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2557-2559) | **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:2609, `typed_entry_units` pine_adapter.cpp:2627; `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:2588; `percent_commission_live_equity` pine_adapter.cpp:4329-4369) | **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. | | SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2514-2529, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6672-6698) | **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:592, 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:4980-4983; the book's own quantities are on the grid -- a whole-scope `ScopeFraction` and a `Reduce` of a FIFO boundary of its scope (R5 lane K-ULP4) | K | — | F:C4 O:C5 S:S1 | **Disagree:** F, O partial / S yes. R5-3: the quantity grid is native already. L3's `Sized` intent reuses the snap policy. | +| SZ7 | Quantity step | **yes** — `quantity_grid` admission native_run_spec.hpp:592, 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:4992-4995; 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:12510-12538, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:8999-9309) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4444-4459) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6733), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7945), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12766), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13362, pine_adapter.cpp:13415) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15738) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18758) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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). | +| 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:6733), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7945), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12766), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13362, pine_adapter.cpp:13415) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15743) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18763) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7207-7558; ledger `close_batch_bar_` pine_adapter.hpp:2042-2053; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | @@ -99,30 +99,30 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6230-6279, cap left unset `max_open_lots` pine_adapter.cpp:2361-2363) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | | MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13311-13376 and leaves the kernel gate unset pine_adapter.cpp:13310-13312) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13966-13967, `margin_pct` pine_adapter.cpp:13374) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:14066-14272, `submit_margin_call_units` pine_adapter.cpp:14233-14279, `schedule_margin_call_path` pine_adapter.cpp:18244-18342) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **Closed:** `maintenance_long` / `maintenance_short` with `NativeLiquidationCheck::{PathAdverseExtreme, PathAdverseExtremeMark, CalculationOnly}`; the kernel solves the level, rests its own `Reduce`/`Flatten` bound to the live book, re-prices it with `CancelReason::Superseded` and books a `MarginCallEvent` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | F:D2 O:D3 S:M3 | TV parts → MG15 | +| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:14066-14272, `submit_margin_call_units` pine_adapter.cpp:14233-14279, `schedule_margin_call_path` pine_adapter.cpp:18249-18347) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13997-14033) | **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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16342-16358, `apply_fx_opening_margin_slice` pine_adapter.cpp:18343-18371) | **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:16833-16872) | **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 | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16347-16363, `apply_fx_opening_margin_slice` pine_adapter.cpp:18348-18376) | **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:16838-16877) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13346-13349) | **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:13927-13941, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14981-15011) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15024-15258) | **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 | +| MG12 | Max intraday loss → flatten + block (`intraday_loss_breached` pine_adapter.cpp:13927-13941, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14986-15016) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15029-15263) | **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:13737-13816) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1323 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:13955, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14014, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14750-14790, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14274-14401, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14274) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged book, where TradingView books its call at the first lower-timeframe low that crosses (`tests/fixtures/intrabar_margin`, every row on 3 of 4 tapes, the fourth a recorded divergence). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15262-15320) | n/a | A | — | S:R2 | (single-source) | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15267-15325) | 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:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7506-7510 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16177) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7063-7067; `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:7010 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7061 (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. | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7542-7546 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16182) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `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:7027 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7078 (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:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5632, `flush_coof_tail` pine_adapter.cpp:16320, `end_coof_recalc` pine_adapter.cpp:5653) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6917, native_execution_consumer.cpp:6896-6926; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5612-5646, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | -| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7145-7149; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | -| 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:7500-7619 **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 | — | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5632, `flush_coof_tail` pine_adapter.cpp:16325, `end_coof_recalc` pine_adapter.cpp:5653) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7166-7170; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | +| 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:7536-7655 **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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:234-320) 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:5779), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1660-1667 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | @@ -132,23 +132,23 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1484-1487, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1392-1548; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1485 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:486), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | -| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI `strategy_set_native_security_feed` c_abi.cpp:839) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1429-1445 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1517; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1447-1452 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:488 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8292 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:46-56). | +| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI `strategy_set_native_security_feed` c_abi.cpp:839) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1429-1445 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1517; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1447-1452 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:488 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8309 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:46-56). | | HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:637) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:928 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | | HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:21-220, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:241-413; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:515) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:255-336) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | -| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:875; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7872-7889, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7879, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | +| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:875; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7910-7927, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7917, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | ### 1.7 Fill price, slippage, commission, FX | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:5686-5733, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6269-6277; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:584 applied once native_execution_consumer.cpp:4599-4600, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12032-12041) is A | +| FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:5698-5745, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6281-6289; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:584 applied once native_execution_consumer.cpp:4611-4612, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12032-12041) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:472-479) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11994-12003, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4061; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6351-6352; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11994-12003, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4073; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6363-6364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:8687). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | +| FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1202 in batch; the stream then refused a non-empty curve (today only tick-driven input is refused under one, `refuse_fx_curve_tick_input` native_execution_consumer.cpp:8739). **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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9106-9111, `exit_limit_trigger` pine_adapter.cpp:9640-9661), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11757) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -161,8 +161,8 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:219; `fill_metrics_section` engine_report.cpp:119-139 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | | RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1684) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1409) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | -| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9252, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | -| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1370, native_execution_consumer.cpp:10638; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | +| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9313, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | +| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1370, native_execution_consumer.cpp:10715; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | | RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:538; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1931 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:411 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:405 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:411 is its deprecated spelling, which the generic default forwards to (`tests/test_native_host_hash_extension.cpp`). | K | L2 (the virtual rides the shared bump) | F:I3 O:J1 S:X3 | — | @@ -176,7 +176,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OT4 | Cooperative abort | **yes** — `request_abort` engine.hpp:2158, `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:896, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:940, probe `strategy_execution_contract` c_abi.cpp:873, stream `strategy_stream_begin` c_abi.cpp:624 **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:548 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:907 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 58 runtime exports (and eight generated per-strategy exports) (`tests/test_native_c_api.c`, `tests/test_native_c_api_frozen_header.cpp`). | K | L13 | F:J6 O:J5 | R5-7: post-v1. S covers it in its §2.3 without a row. | -| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1461 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7267), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1765 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:857; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | +| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1461 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7286), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1765 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:857; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | | OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:288-290) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | --- @@ -204,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:82-91) and of the rich `BacktestEngine::run` overload (engine.hpp:1780-1785, 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:292-295 | 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) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:29 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:356-359 (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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8748`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2309-2310. 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:260-261. **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:741) | 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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8800`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2309-2310. 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:260-261. **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:741) | 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:454-460) | 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:6422 | NEUTRAL | Reuse as the reference arithmetic of the price-grid and sizing lanes, delete the rest | O (single-source) | +| e | Dead helpers in the public kernel header | **Done.** All four are gone from `engine.hpp`: `round_to_mintick_directional` and `apply_slippage` were the pre-R5 spelling of the directional snap and the slippage step and the kernel now fills on `native_matching::grid_round_directional` / `native_matching::apply_slippage` src/native_matching.hpp:259 (the engine.hpp comment that named them went with R5 lane E10's residue sweep); `apply_limit_fill` and `reserve_percent_commission` are deleted outright, and only the adapter still spells the first two names, in its own comments. `round_to_mintick` engine.hpp:753 and `bar_fill_price` engine.hpp:771 stay, live through the call `bar_fill_price` native_execution_consumer.cpp:6434 | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19162-19191 | 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:19167-19196 | 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:523, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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:604. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1274 | 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) | @@ -281,8 +281,8 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the | | F (its §2.iv A) | O (its G10) | S (its P7) | Chosen | |---|---|---|---|---| | API shape | spec `subscriptions` + push callback `on_native_timeframe_bar` | `register_native_series` + pull `native_series_bar(id)` / `native_series_slot_is_new(id)` | spec `security_requests` + `configure_native_security_feed` + `native_security(id)` view | F's subscription in the spec + push callback, plus O's pull accessor | -| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:656-671, `contribute_input` native_execution_consumer.cpp:7996, `seal_script` native_execution_consumer.cpp:7973) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | -| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7095), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | +| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:656-671, `contribute_input` native_execution_consumer.cpp:8046, `seal_script` native_execution_consumer.cpp:8011) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | +| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7116), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | | Authoritative bars | `authoritative_bars` on the subscription | `Source::ExchangeFeed` through `set_native_security_feed` | `configure_native_security_feed(id, bars, tf)` | F's field, installed through the existing store | | Lookahead | none, by construction | `lookahead = false` default, available | `lookahead_on = false`, `gaps_on = true` | open → §4 U1 | @@ -298,7 +298,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3025-3093) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3033-3101) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:45, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -341,7 +341,7 @@ the campaign's own anchor audit found that the first thing to rot: | the request-epoch sentence | `native_order_v4` | `native_order_v7` native_order.hpp:25 | | the two host-epoch sentences | host epoch v16 | `engine_script_run_v19` native_host.hpp:20 | | "What still requires Pine compatibility to build" | `BacktestEngine` still holds `CapAttachment` / `OrderPriority` / `IntradayCap` | `engine.hpp` has zero references to them; the cap type is `CapAttachment` intraday_cap.hpp:18 and the constructor is `PineStrategyHost`'s pine_strategy_host.hpp:244 | -| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9122-9162 shows no such refusal (still unpinned by a test; the page says so) | +| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9183-9223 shows no such refusal (still unpinned by a test; the page says so) | | added | — | the self-aggregation recipe beside the subscriptions it is an alternative to; the open-bar lookahead warning with `current_partial_bar` native_host.hpp:1073; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -350,7 +350,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:2205-2338) 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:1641-1645). 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 Cloud Run campaign sweep on the final tree (fixed population, 4190 probes, zero tolerance on the hard band — native-refactor-progress.md:41-47; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:139-140). 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:2205-2338) 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:1647-1651). 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 Cloud Run campaign sweep on the final tree (fixed population, 4190 probes, zero tolerance on the hard band — native-refactor-progress.md:41-47; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:139-140). 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 drive the adapter and 14 the native host (O), 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. @@ -411,7 +411,7 @@ closure markers and `scripts/check_design_inventory.py`). | L2 | `NativeReportPolicy { HostRecorded, KernelRecorded }` (R1 appended a third value, `KernelRecordedAtHostMarks` native_run_spec.hpp:64), `report_open_position_at_end`, `hash_host_extension`, `native_metrics()` → A.1 | at L2 the adapter stayed `HostRecorded`; since R1 it declares `KernelRecordedAtHostMarks` pine_adapter.cpp:2358, 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:5617-5621) and `drain_applied_notifications` (native_execution_consumer.cpp:6278-6291) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | +| L5 | `NativeCalculationTrigger`, `on_native_recalculate(…, reason, cause)`, `on_native_sub_bar`, `current_partial_bar()`, `NativeOpenBarView` → A.4 | the adapter keeps `BarClose` + its own cascade; `born_on_remaining_path` (native_execution_consumer.cpp:5629-5633) and `drain_applied_notifications` (native_execution_consumer.cpp:6290-6303) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | | L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1305-1389 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | | L7 | `native_working_requests()`, `cancel_all()`, `cancel_where(label)`, `TriggerAnchor`, `TrailTicks`, zero offset, `ReplaceOptions`; toolkit `submit_bracket`, `OrderBook` → A.6 | new kinds unused by the adapter; anchor defaults to `Absolute`; the adapter's `tick * 0.5` sentinel keeps working | bracket + trailing probe through the builder: exact where TR5 / FP7 are inactive (on-grid levels) (F); the zero-offset trail test asserts the run no longer fails (O); parent-limit + stop sibling, parent rejection, replacement, parent-cycle revival (S) | | L8 | `NativePriceGrid { None, QuantizeFills, QuantizeFillsAndTriggers }`, `NativeGridRounding` → A.7 | `None` for the adapter, permanently — **native-only by ruling, audit lane P6: §3.6.2** | a host with `QuantizeFills` books on-grid prices; a second test pins that `QuantizeFillsAndTriggers` still differs from TV's half-tick threshold (FP5) (O) | @@ -444,7 +444,7 @@ Flagged: | Tier | Lanes | Why | |---|---|---| -| Day-one blockers (O) | L1, L2, L3, L10 | Without L2 `fill_report` silently returns an empty curve and degenerate equity metrics and drops an open final position. Without L3 every non-unit order needs a hand-written `resolve_execution_terms`, and a mistake is a run-terminating `TermsUnresolved` (native_execution_consumer.cpp:4964-4966). Without L1 "kernel" is a claim, not an artifact. L10 is the minimum buildable proof. | +| Day-one blockers (O) | L1, L2, L3, L10 | Without L2 `fill_report` silently returns an empty curve and degenerate equity metrics and drops an open final position. Without L3 every non-unit order needs a hand-written `resolve_execution_terms`, and a mistake is a run-terminating `TermsUnresolved` (native_execution_consumer.cpp:4976-4978). Without L1 "kernel" is a claim, not an artifact. L10 is the minimum buildable proof. | | Required by the promise (R5-11) | L0, L4, L5, L6, L7, L8 | A kernel that cannot liquidate is not a broker model (O); bracket / trailing corner cases are hit immediately (O); calc-timing and HTF are Pine features the user wants natively (R5-5, R5-6); the price grid is generic and missing (R5-3). | | Wanted with v1, not blocking | L11 a, d | removes the last `source::` name from kernel signatures and the orphaned TV commentary (F) | | In scope, 1.x | L9 | R5-9; a toolkit stand-in is acceptable meanwhile (F) | @@ -523,9 +523,9 @@ line numbers are this tree's (683a82f); kernel semantics are |---|---|---|---|---|---| | `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2362); 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:13257-13258; 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:16833, `update_risk_state` pine_adapter.cpp:13898-13925): 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:13250), 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_drawdown` | `update_risk_state(bar.close)` once per script bar at the close mark (`update_risk_state(bar.close)` pine_adapter.cpp:16838, `update_risk_state` pine_adapter.cpp:13898-13925): 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:13250), 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:1200-1208): +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:17724-17733); 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:16336); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16448), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15011-15020, `submit_intraday_loss_close` pine_adapter.cpp:14981-15009), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17190); 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:5983, `:7652`, `:8376`, `:8504`, `:11481`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18089-18092) | `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_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16341); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16453), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15016-15025, `submit_intraday_loss_close` pine_adapter.cpp:14986-15014), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17195); 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:5983, `:7652`, `:8376`, `:8504`, `:11481`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18094-18097) | `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:13889-13891); 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:13795-13816, `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 @@ -1299,8 +1299,8 @@ unchanged after it. | E1 | **Spec hashing.** `hash_spec` folds every field unconditionally (native_execution_consumer.cpp:234-256) | 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:1620); 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5779) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6278-6291); 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:16177-16206) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5779) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6290-6303); 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:16182-16211) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | @@ -1345,7 +1345,7 @@ unchanged after it. | **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:16342-16358) 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:2646) 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:16347-16363) 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:2646) 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. | @@ -1367,11 +1367,11 @@ virtual void hash_host_extension(BrokerStateHashSink&) const {} // F (S: hash_ NativeMetricsView native_metrics() const; // S: net_profit, equity, closed_trades, open_lots; non-virtual ``` -- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` + `record_equity_point(script_open_ms)` (engine.hpp:1380, engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7189, native_execution_consumer.cpp:7189, native_execution_consumer.cpp:7189) and after the `AfterCalculation` close; appends the per-bar broker hash when recording is on; synthesizes the open-position row at run end / `stream_end` with the existing `build_close_trade_with_costs` (engine_orders.cpp:70). +- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` + `record_equity_point(script_open_ms)` (engine.hpp:1380, engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7210, native_execution_consumer.cpp:7210, native_execution_consumer.cpp:7210) 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:2358), with `report_open_position_at_end == false`. - Publish read-only `closed_trade_*` accessors (RP6). - Alternative: F's virtual `owns_equity_recording()` (the A48 ownership pattern of `owns_lot_excursions` native_host.hpp:1023). Same semantics, but it costs a host-epoch bump, so the spec form was chosen; the virtual was never added, and `NativeReportPolicy` is what landed (§3.4 row 3). -- TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5061-5103), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. +- TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5073-5115), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. ### A.2 L3 — sizing, fractional reduce, sibling reservation (F P1, P7 + O G2 + S P2, P3) @@ -1423,7 +1423,7 @@ virtual void on_native_margin_call(const native_order::MarginCallEvent&) {} - 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:13223-13244). 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:2575), 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:4663-4676 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). +- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:4675-4688 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). ### A.4 L5 — calculation timing (F P4 + O G8, G9 + S P6) @@ -1443,9 +1443,9 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1692-1693) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6979-6991) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). -2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5617-5621) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1660-1667) is calibrated against it. -3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6836-6843). `on_native_tick` stays the observation hook (S). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1700-1701) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6995-7007) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5629-5633) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1660-1667) is calibrated against it. +3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6848-6855). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2553); valid in bar-open, applied and tick callbacks. 5. The kernel never attempts language-state rollback; that stays Pine's (`snapshot / restore / commit_coof_script_state`, pine_scheduler_native.cpp:52-104) (O). @@ -1512,7 +1512,7 @@ NativeGridRounding grid_rounding = NativeGridRounding::HalfUp; ``` - `QuantizeFills`: the kernel rounds `resolved_price` onto `price_tick` **before** slippage (`HalfUp`) or in the direction that favours the resting order (`Directional`). `QuantizeFillsAndTriggers`: the matcher additionally compares the quantized path against the raw level. -- TUs: `native_run_spec.cpp` (validation), `native_execution_consumer.cpp` (resolve path native_execution_consumer.cpp:3950-3972, `execute_current` path native_execution_consumer.cpp:3770-3780), `native_matching.hpp` (threshold form). The dead helpers were the reference arithmetic and are deleted (§2.ii e); the kernel's own step is `native_matching::apply_slippage` native_matching.hpp:259. +- TUs: `native_run_spec.cpp` (validation), `native_execution_consumer.cpp` (resolve path native_execution_consumer.cpp:3962-3984, `execute_current` path native_execution_consumer.cpp:3782-3792), `native_matching.hpp` (threshold form). The dead helpers were the reference arithmetic and are deleted (§2.ii e); the kernel's own step is `native_matching::apply_slippage` native_matching.hpp:259. ### A.8 L9 — risk limits (F P3 + O G7 + S P5) @@ -1532,7 +1532,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:1329) (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:373), 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:13721-13819); 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:4663-4676 and the per-point walk), `native_order.hpp`, the state hash. +- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:4675-4688 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index b1cfdcf2..45f948cf 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -117,8 +117,8 @@ finding to report, not a step to take. `scripts/check_doc_reverts.py`. 10. **A test row never silently disappears.** Each profile counts the rows that - *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:259 and - `RELEASE_MIN_TESTS` ci_verify.py:401. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:264 and + `RELEASE_MIN_TESTS` ci_verify.py:408. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -264,7 +264,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:259, `RELEASE_MIN_TESTS` ci_verify.py:401). It +(`KERNEL_MIN_TESTS` ci_verify.py:264, `RELEASE_MIN_TESTS` ci_verify.py:408). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index b06d9fe6..64526fb2 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -463,7 +463,7 @@ spec fields. The rich `run(bars, n, input_tf, script_tf, inputs, syminfo, overrides, …)` overload (`engine.hpp:1623-1634`) is **not** refused as a source mutation: it reaches `NativeExecutionConsumer::run_rich` -(`native_execution_consumer.cpp:9122-9162`), which admits the begin, checks the +(`native_execution_consumer.cpp:9183-9223`), which admits the begin, checks the timeframe arguments against the spec, preflights and pumps the batch exactly like the plain overload. `inputs` / `syminfo` / `overrides` are carried only as `NativeBeginArgs` fields to `prepare_native_begin` — the overrides as the @@ -631,13 +631,13 @@ a host reacts to its own execution and may submit again. A request born there, mid-bar on a continuous segment, is eligible on the **remaining path suffix** of that segment — the birth is admitted at the current cursor and the geometric search then sees only the unconsumed suffix (`born_on_remaining_path`, -`native_execution_consumer.cpp:5617-5621`). Requests accepted before the +`native_execution_consumer.cpp:5629-5633`). Requests accepted before the segment, and discrete points, keep the ordinary birth gate above. `on_native_bar_open` fires at the modeled opening, before that point's matching -pass (`native_execution_consumer.cpp:7063-7065`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:7084-7086`). **Lookahead warning:** the `Bar` it receives is the *complete* script bar — the consumer has already set -`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6955`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6971`), the complete bar unless the spec asks for `NativeOpenBarView::OpenOnly` — so its high, low and close are the finished bar's, not what is known at the open. A host that must decide on open-only information reads @@ -1286,8 +1286,16 @@ position has already paid. The two agree whenever no open entry paid one. **Arming (`PathAdverseExtreme`, the default).** At every script-bar open and after every applied fill, the kernel measures the requirement against the most -adverse price the modeled script path still reaches after the current -waypoint — the same sizing mark a whole-bar broker check would use. If that +adverse price the modeled script path still reaches — the same sizing mark a +whole-bar broker check would use. At the open that is the rest of the bar. +After a fill it is the rest of the segment the fill was matched on and the +waypoints after it: a request matched at a driver point was reached on the +segment into it, so a limit filled on its way down to the bar's low still +faces that low (a short filled on its way up, the high), while the waypoints +the path already passed do not count (R5 lane PAR-MARGIN-2; +`tests/test_native_margin_post_fill_path.cpp`; TradingView books those calls +on the fill's bar, `tests/fixtures/margin_entry_bar/pm2-m7-lim-*`, `-slim-*`, +`-s1lim-*`). If that mark breaches, the kernel rests its own `Reduce` (or `Flatten`) with `Stop{L}`, bound to the live book. The reduction therefore *fills at the liquidation level*, where the account actually runs out of margin, while it is @@ -2074,7 +2082,7 @@ default, set while no run is active — because each row is a full the live state, not the run's length: the closed rows enter through a running digest). With the switch on, one row follows each point, after the extremes that point just folded -(`record_script_report_point`, `native_execution_consumer.cpp:7799`), so +(`record_script_report_point`, `native_execution_consumer.cpp:7835`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -2885,7 +2893,7 @@ Only completed buckets are published, so this recipe has no lookahead by construction. It is the same class the kernel's own subscription evaluator aggregates with, and the one the kernel's `script_bucket_completions` query feeds when the Pine scheduler asks how its input span buckets -(`TimeframeAggregator` `native_execution_consumer.cpp:7879`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:7917`). What it does **not** give you is what a declared subscription does: an `authoritative_bars` feed, the `gaps` and `lookahead` delivery rules, the lazy-seal chronology, a C spelling, and the diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index abcc04ea..2204e073 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -126,8 +126,8 @@ rather than defaulted. | `currency` | `currency` native_run_spec.hpp:571 | `currency` pineforge.h:537 | `hello_kernel.cpp` | With `ticker` native_run_spec.hpp:568, `tickerid` native_run_spec.hpp:569, `type` native_run_spec.hpp:570, `basecurrency` native_run_spec.hpp:572, `description` native_run_spec.hpp:573 and `volumetype` native_run_spec.hpp:574 this is Pine's whole `syminfo` block, named rather than inherited from a chart. | | `commission_type` | `NativeFeeKind` native_run_spec.hpp:19 | `fee_kind` pineforge.h:540 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | | `commission_value` | `fee_value` native_run_spec.hpp:590 | `fee_value` pineforge.h:540 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | -| `slippage` | `slippage_ticks` native_run_spec.hpp:584 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3959-3960. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7506-7507, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | +| `slippage` | `slippage_ticks` native_run_spec.hpp:584 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3971-3972. TradingView's round-then-slip order is adapter-only. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7542-7543, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | | `pyramiding` | `max_open_lots` native_run_spec.hpp:596 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:2914). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). | | `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:344 with `size_basis` native_c_api.h:2111 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | | `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2115 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | @@ -463,7 +463,7 @@ part both ways (`tests/test_adapter_margin_schedule_differential.cpp`, M11). | `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1763 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | | bar-open pre-pass | `on_native_bar_open` native_host.hpp:889 | `on_bar_open` native_c_api.h:2470 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | | the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1073 | `strategy_native_partial_bar_v1` native_c_api.h:2984 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | -| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2473 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5375. | +| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2473 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5387. | | raw input before aggregation | `on_native_input` native_host.hpp:875 | `on_input` native_c_api.h:2468 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | | a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:928 | `on_sub_bar` native_c_api.h:2509 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | | `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1461 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. | diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 6e592eba..132966cc 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -256,7 +256,12 @@ # summed): # +1 PAR-MARGIN test_native_margin_intrabar_samples (IntrabarSample, item 3) # It registers in release too. -KERNEL_MIN_TESTS = 284 +# 285 = those 284 plus R5 lane PAR-MARGIN-2's source-free row (INT26 pick, +# summed): +# +1 PAR-MARGIN-2 test_native_margin_post_fill_path (the post-fill margin +# check measures from the fill's own waypoint) +# It registers in release too. +KERNEL_MIN_TESTS = 285 # Release-row floor, the same gate for the default profile. Before lane P7 # only the kernel profile had one, so a row that left release alone (a # source-bound TU dropped from TEST_SOURCES, a deleted twin or ABI row) left a @@ -398,7 +403,9 @@ # summed). The lane's own count of 680 also counted H-MEASURE's # test_adapter_margin_schedule_differential, which it had cherry-picked for its # pins; H-MEASURE's twelve above already count that row. -RELEASE_MIN_TESTS = 699 +# 700 = those 699 plus R5 lane PAR-MARGIN-2's kernel row above (INT26 pick, +# summed). +RELEASE_MIN_TESTS = 700 # R5 lane H-DOCGATES (AUDIT4-opus perf N-7) raises both floors by one more: # test_native_runtime_ambient_lifo, the runtime blocks' LIFO death row, is # source-free, so it registers and runs in the kernel profile (279 -> 280) and diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index 05372f01..bd2aab09 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -3158,6 +3158,18 @@ double NativeExecutionConsumer::margin_sizing_price( return adverse; } +NativePathPhase NativeExecutionConsumer::margin_segment_origin( + NativePathPhase phase) const noexcept { + const NativePathPhase first = margin_path_high_first_ ? NativePathPhase::High + : NativePathPhase::Low; + const NativePathPhase second = margin_path_high_first_ ? NativePathPhase::Low + : NativePathPhase::High; + if (phase == first) return NativePathPhase::Open; + if (phase == second) return first; + if (phase == NativePathPhase::Close) return second; + return phase; +} + std::optional NativeExecutionConsumer::margin_call_units( const BacktestEngine& engine, double mark, const native_order::MatchCursor& cursor, NativeMarginCheckKind kind, double* out_equity, double* out_required) const { @@ -7001,14 +7013,19 @@ void NativeExecutionConsumer::drain_queued_notifications(BacktestEngine& engine) applied_notifications_.clear(); notification_head_ = 0; // L4: every applied fill re-arms the margin model against the book it - // left behind. Inert for a run that declares no margin model. + // left behind. Inert for a run that declares no margin model. The + // fill was reached on the segment into its driver point, so the path + // still ahead of it starts at that point's waypoint: a limit filled on + // the way down to the bar's low still faces the low (R5 lane + // PAR-MARGIN-2). Measured from the segment's origin, with the fill + // price as the point's own mark. if (drained && margin_model() != nullptr) { native_order::MatchCursor cursor; cursor.point = last.point.decision.coordinate; - maintain_margin_liquidation(engine, cursor, - last.point.decision.coordinate.path_phase, - last.point.price, - NativeMarginCheckKind::AfterApplied); + maintain_margin_liquidation( + engine, cursor, + margin_segment_origin(last.point.decision.coordinate.path_phase), + last.point.price, NativeMarginCheckKind::AfterApplied); } // L9: the second evaluation point. Every fill of this drain has // already been counted; the account facts are measured once, at the diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 0c869d4d..5da095ba 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -1188,6 +1188,12 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // has no remaining modeled path of its own. double margin_sizing_price(bool short_side, NativePathPhase phase, double fallback) const noexcept; + // The waypoint the segment into `phase` starts from (Open for the first + // extreme, and so on; Open itself has no segment into it). A request + // matched at a driver point was reached on that segment, so the path left + // after its fill starts AT the point's own waypoint: the post-fill check + // measures from here, as fx_roll_margin_check_at's continuous roll does. + NativePathPhase margin_segment_origin(NativePathPhase phase) const noexcept; // Kernel numbers, the host's requirement decision, the breach test, the // kernel sizing, then the host's units override. nullopt means no // liquidation. diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 37f17fa6..e19d7392 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -14877,7 +14877,12 @@ bool PineExecutionAdapter::schedule_margin_call_path( } // ab9714be pine_fills.cpp:1025-1063, :1314-1339: an entry-bar margin // pass sees only the OHLC suffix after the actual opening point. Later - // bars enter here from Open and retain the ordinary remaining path. + // bars enter here from Open and retain the ordinary remaining path. A + // fill matched at a waypoint was reached on the segment into it, so that + // waypoint is still ahead of it (the kernel's post-fill point measures + // from it too, NativeExecutionConsumer::margin_segment_origin): a limit + // filled on the way down to the low still faces the low (R5 lane + // PAR-MARGIN-2). const bool high_first = source_path_uses_high_first(bar); struct Waypoint { NativePathPhase phase; double price; }; const Waypoint path[] = { @@ -14896,7 +14901,7 @@ bool PineExecutionAdapter::schedule_margin_call_path( } } double adverse = kNaN; - for (int index = current + 1; index < 4; ++index) { + for (int index = std::max(current, 0); index < 4; ++index) { if (!finite_positive(path[index].price)) continue; if (!std::isfinite(adverse) || (position.signed_units > 0.0 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c0a7f236..77c905e7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -861,6 +861,12 @@ list(APPEND TEST_SOURCES test_native_kernel_edge) # NativeMarginCheckKind::IntrabarSample at every delivered sample after the # bar's first. Source-free, so it also runs in the kernel-only profile. list(APPEND TEST_SOURCES test_native_margin_intrabar_samples) +# R5 lane PAR-MARGIN-2: the post-fill margin check of a request matched on the +# segment into a waypoint measures from that segment's origin, so the waypoint +# the fill was reached on the way to (a limit filled on its way down to the +# bar's low) is in the scan. Source-free, so it also runs in the kernel-only +# profile. +list(APPEND TEST_SOURCES test_native_margin_post_fill_path) # R5 F5: the Pine adapter selects those facts instead of computing its own: # the same tapes flag for flag through the adapter, its live-probe tail diff --git a/tests/fixtures/margin_entry_bar/README.md b/tests/fixtures/margin_entry_bar/README.md index 9b9611e8..00a709c9 100644 --- a/tests/fixtures/margin_entry_bar/README.md +++ b/tests/fixtures/margin_entry_bar/README.md @@ -1,58 +1,39 @@ -# Leveraged entry-bar margin tapes (R5 lane H-MEASURE, row M7 / G2-09) -TradingView's own trades for one question AUDIT4 §6 left "ruled, unmeasured": -does TradingView check a LEVERAGED opening (margin below 100 %) on the bar its -entry fills, where the adapter's `margin_check_allowed` refuses the kernel's -AfterApplied point? `tests/test_adapter_margin_schedule_differential.cpp` -(section "M7 on tapes") replays every tape through the Pine adapter and through -a bare kernel host with TradingView's money and slice. TradingView does, on 4 -of 4; since R5 lane PAR-MARGIN `on_applied` admits that point for a leveraged -opening's entry bar, and the section asserts the adapter books each tape's -rows (lane H-MEASURE had pinned the adapter's late or missing call instead). -Each directory is one `lab tv` export (pineforge-workflow, channel -`ws-report-v1`, `rangeProof: covered`), byte-identical: `strategy.pine`, -`tv_trades.csv` (times UTC+8), `meta.json`, `metrics.json`. The sha256 of each -tape is `metrics.json` `tvTradesCsvHash`; the campaign note is -`tv-tape--`. +## PAR-MARGIN-2: the M7 shapes PAR-MARGIN left unmeasured (R5 lane PAR-MARGIN-2) -| probe | chart | margin, size | entry bar (UTC) | TradingView | -|---|---|---|---|---| -| `hm-m7a-eth-m5-k16-0621` | BINANCE:ETHUSDT.P 15 | 5 %, 16 x equity | 2025-06-21 21:30 | margin call 6.842 @2230 (the entry bar's low), the whole book | -| `hm-m7a-eth-m5-k16-0922` | same | 5 %, 16 x | 2025-09-22 06:00 | margin call 3.793 @4000.88, the whole book | -| `hm-m7a-eth-m5-k16-1121` | same | 5 %, 16 x | 2025-11-21 07:30 | margin call 5.806 @2642.65, the whole book | -| `hm-m7a-eth-m20-k4-1010` | same | 20 %, 4 x | 2025-10-10 21:15 | margin call 0.4544 @3400 (4 x 0.1136), the rest 0.6196 at the timeout @3895 | +Twelve more `lab tv` exports on BINANCE:ETHUSDT.P 15m (same channel and +layout), each the lane's own synthetic script: one leveraged entry (or an entry +and an add) sized `K x strategy.equity / close` on a 0.001 grid at a signal +bar, closed 3 bars (4 for the adds) after the first open trade's entry bar. +`tests/test_adapter_margin_schedule_differential.cpp` (section "M7 shapes") +replays every one FROM ITS OWN `strategy.pine` (parsed, not restated), and +the adapter books TradingView's rows on all of them: -All four export windows are 2025-04-01 .. 2026-05-01; each script trades once. -`bars.inc` holds each event's signal bar through the entry bar + 4, from the -corpus feed `corpus/data/derived/ohlcv_ETH-USDT-USDT_15m.csv` materialized by -`scripts/derive_corpus_feeds.py` at corpus gitlink eede4a2. +- `pm2-m7-lim-*`: a limit opening (margin 5, `limit = math.round(close * 0.997, 2)`) + filled on the next bar on its way down, whose low then breaches: the call on + that bar at its LOW -- three high-first bars and a low-first one (0402-1945). +- `pm2-m7-slim-*` and `pm2-m7-s1lim-*`: a SHORT limit opening + (`limit = math.round(close * 1.003, 2)`) filled on the next bar on its way UP, + whose high then breaches, leveraged (margin 5) and at full margin (1 x): the + call on that bar at its HIGH -- three low-first bars and a high-first one + (0509-1100). -## M10: the book a default-percent stop entry above 100 % opens (R5 lane PAR-MARGIN) +`pm2_bars.inc` holds each event's bars from the corpus feed +`corpus/data/ohlcv_ETH-USDT-USDT_1m.csv` (15m bars aggregated from it as the +derived feed does; the magnified scripts' one-minute bars as they are). -Eight more `lab tv` exports on NYSE:F 15m, same channel and layout, margin 25, -one buy stop placed on the session's last bar (15:45 America/New_York) and -filled at the next session's open, closed 3 bars after its entry bar: -`pm-m10-f-k4-*` at 400 % with the stop at 0.9 x close (marketable when -placed), `pm-m10-f-gapup-*` at 390 % with the stop at round(1.002 x close, 2) -(not marketable when placed, gapped through by the open). TradingView opens the -quotient of the signal close on its tick for the first and of the snapped -stop level for the second, never the fill's, and books its margin calls on -that book. `m10_bars.inc` holds each event's bars as TradingView reports them, -read by the two `pm-m10-f-ohlc*` exports (each bar printed as an order -comment); they equal the lab feed `80f404ae85ef`, half cents included. -`tests/test_adapter_margin_schedule_differential.cpp` (section "M10 on tapes") -replays all eight. Campaign notes: `tv-tape--`. - -| probe | window | TradingView | tvTradesCsvHash | +| probe | window | TradingView's margin calls | tvTradesCsvHash | |---|---|---|---| -| `pm-m10-f-k4-0402` | 2026-03-25 .. 2026-04-07 | book 3427; margin calls 176 @11.38 | `3bd3141a7089f30c` | -| `pm-m10-f-k4-0410` | 2025-04-02 .. 2025-04-15 | book 4219; margin calls none | `8911072fc9bfb7ad` | -| `pm-m10-f-k4-1007` | 2025-09-29 .. 2025-10-10 | book 3144; margin calls 980 @12.11 | `bd8d6bc27e858bfb` | -| `pm-m10-f-k4-0309` | 2026-03-02 .. 2026-03-12 | book 3294; margin calls 4 @11.83, 56 @11.81 | `ab895fec3d476066` | -| `pm-m10-f-gapup-0501` | 2025-04-24 .. 2025-05-06 | book 3896; margin calls 376 @10.03 | `c672eb182acd2a16` | -| `pm-m10-f-gapup-0331` | 2026-03-24 .. 2026-04-03 | book 3475; margin calls 156 @11.28 | `8f54192f161b9013` | -| `pm-m10-f-gapup-0424` | 2025-04-17 .. 2025-04-29 | book 3987; margin calls 92 @9.81 | `79a6aa531e7bfbb3` | -| `pm-m10-f-gapup-0527` | 2025-05-19 .. 2025-05-30 | book 3757; margin calls 204 @10.35 | `f6222f524d46cd37` | -| `pm-m10-f-ohlc` | 2025-04-01 .. 2026-04-10 | 24 bars' open, high, low and close printed as entry comments | `8b006a00aff01690` | -| `pm-m10-f-ohlc2` | 2025-04-01 .. 2026-04-10 | 24 bars' open, high, low and close printed as entry comments | `52160db7208aea53` | +| `pm2-m7-lim-0402-1945` | 2025-03-30 .. 2025-04-05 | 4.516 @1881 | `c8d52c248bfae6ad` | +| `pm2-m7-lim-0402-2000` | 2025-03-30 .. 2025-04-05 | 3.7668 @1872.48 | `c50706a1de9fa157` | +| `pm2-m7-lim-0801-0030` | 2025-07-29 .. 2025-08-04 | 2.22 @3613.67 | `07b081a7560f112c` | +| `pm2-m7-lim-1201-0215` | 2025-11-28 .. 2025-12-04 | 1.7024 @2824.6 | `8dbe22c6e8867a97` | +| `pm2-m7-s1lim-0402-1330` | 2025-03-30 .. 2025-04-05 | 0.0152 @1892.7 | `d633d75493717e91` | +| `pm2-m7-s1lim-0509-1100` | 2025-05-06 .. 2025-05-12 | 0.0132 @2375.39 | `5f62b5db446e583a` | +| `pm2-m7-s1lim-0709-1930` | 2025-07-06 .. 2025-07-12 | 0.0144 @2795.73 | `8d7299c2d5e6ae4f` | +| `pm2-m7-s1lim-1001-0830` | 2025-09-28 .. 2025-10-04 | 0.0096 @4329 | `8981c3291ddf2d14` | +| `pm2-m7-slim-0402-1330` | 2025-03-30 .. 2025-04-05 | 3.2528 @1892.7 | `1d5f09118992a3fd` | +| `pm2-m7-slim-0509-1100` | 2025-05-06 .. 2025-05-12 | 2.6232 @2375.39 | `8b448336731119f7` | +| `pm2-m7-slim-0709-1930` | 2025-07-06 .. 2025-07-12 | 2.5484 @2795.73 | `02f292a474aef979` | +| `pm2-m7-slim-1001-0830` | 2025-09-28 .. 2025-10-04 | 1.5688 @4329 | `320a3e2199cfaa91` | diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/meta.json new file mode 100644 index 00000000..e4e703e1 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-lim-0402-1945", + "slug": "pm2-m7-lim-0402-1945", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:02.223Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05" + }, + "pine_sha256": "84b83b0466e95e33f1b0e24c0845d93548dddca6cfe7e4efde7a56d8e50cc748", + "tv_trades_sha256": "c8d52c248bfae6adf4564169d4256483b2d419865a5fbd69d5d0c333337c9be1", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/metrics.json new file mode 100644 index 00000000..64efc5e6 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-lim-0402-1945", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "84b83b0466e95e33f1b0e24c0845d93548dddca6cfe7e4efde7a56d8e50cc748", + "tvTradesCsvHash": "c8d52c248bfae6adf4564169d4256483b2d419865a5fbd69d5d0c333337c9be1", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75263", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "returnedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f7ff62bee56c5d4ad42fa13388cec4b51aaced849da8b5acef543d40b0c657fc", + "decodedReportSha256": "f98fa3c7d2074566ed3db6da6be097b4ebbfb7364806bacfff5669036eb6722a", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:02.223Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/strategy.pine new file mode 100644 index 00000000..1318d400 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (M7, a limit opening): a leveraged long LIMIT +// entry (margin 5, 17.93 x equity, limit 0.997 x close) placed on the signal bar 2025-04-02 19:45 UTC and +// filled on the next bar on its way down; that bar's low then breaches and its close recovers. +// process_orders_on_close OFF. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-lim-0402-1945", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 4, 2, 19, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(17.93 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 0.997, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/tv_trades.csv new file mode 100644 index 00000000..f54821da --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-1945/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-03 04:00,Margin call,1881,4.516,8602.21228,-107.61628,-1.25,0,0,0.00,-107.61628,-1.25,-107.61628,-10.76,0 +1,Entry long,2025-04-03 04:00,L,1904.83,4.516,8602.21228,-107.61628,-1.25,0,0,0.00,-107.61628,-1.25,-107.61628,-10.76,0 +2,Exit long,2025-04-03 05:00,timeout,1880.16,4.868,9272.71244,-120.09356,-1.30,0,258.68552,2.79,-193.89244,-2.09,-227.70984,-22.77,4 +2,Entry long,2025-04-03 04:00,L,1904.83,4.868,9272.71244,-120.09356,-1.30,0,258.68552,2.79,-193.89244,-2.09,-227.70984,-22.77,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/meta.json new file mode 100644 index 00000000..304d5dde --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-lim-0402-2000", + "slug": "pm2-m7-lim-0402-2000", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:10.077Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05" + }, + "pine_sha256": "b8fc9fdb77a318a043716505a1a650213f7b4f7ddc0c12d9447febbf1b75369c", + "tv_trades_sha256": "c50706a1de9fa1572abb1ce0b6bca4fd48d1463e50f10215409739c4dae983f6", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/metrics.json new file mode 100644 index 00000000..f0bcc8ac --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-lim-0402-2000", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "b8fc9fdb77a318a043716505a1a650213f7b4f7ddc0c12d9447febbf1b75369c", + "tvTradesCsvHash": "c50706a1de9fa1572abb1ce0b6bca4fd48d1463e50f10215409739c4dae983f6", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75264", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "returnedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "063174fb0e153249d566c79d6f3c9ed0edaa6b7c5c7dca8678ad158dda8dc03b", + "decodedReportSha256": "cdb9054d26c8422ec2c2254cc5863b16e658c9ed057859c935ebd6ac637636df", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:10.077Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/strategy.pine new file mode 100644 index 00000000..a26747ba --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (M7, a limit opening): a leveraged long LIMIT +// entry (margin 5, 14.17 x equity, limit 0.997 x close) placed on the signal bar 2025-04-02 20:00 UTC and +// filled on the next bar on its way down; that bar's low then breaches and its close recovers. +// process_orders_on_close OFF. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-lim-0402-2000", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 4, 2, 20, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(14.17 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 0.997, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/tv_trades.csv new file mode 100644 index 00000000..e80e2ccd --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0402-2000/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-03 04:15,Margin call,1872.48,3.7668,7259.791308,-206.53365,-2.84,0,0,0.00,-206.53365,-2.84,-206.53365,-20.65,0 +1,Entry long,2025-04-03 04:15,L,1927.31,3.7668,7259.791308,-206.53365,-2.84,0,0,0.00,-206.53365,-2.84,-206.53365,-20.65,0 +2,Exit long,2025-04-03 05:15,timeout,1859.76,3.5632,6867.390992,-240.69415,-3.50,0,0,0.00,-261.46762,-3.81,-447.2278,-44.72,4 +2,Entry long,2025-04-03 04:15,L,1927.31,3.5632,6867.390992,-240.69415,-3.50,0,0,0.00,-261.46762,-3.81,-447.2278,-44.72,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/meta.json new file mode 100644 index 00000000..7cb2d506 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-lim-0801-0030", + "slug": "pm2-m7-lim-0801-0030", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:14.473Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-29", + "to": "2025-08-04" + }, + "pine_sha256": "691dca091aff1d1ad069fa0b04fc58734a4a2fbb2ac63e2f988db71c2612e4ce", + "tv_trades_sha256": "07b081a7560f112c5100f3aa50b4993124050c0f1d0a606afd8a67008e3b1543", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/metrics.json new file mode 100644 index 00000000..334c2e78 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-lim-0801-0030", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-29", + "to": "2025-08-04", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "691dca091aff1d1ad069fa0b04fc58734a4a2fbb2ac63e2f988db71c2612e4ce", + "tvTradesCsvHash": "07b081a7560f112c5100f3aa50b4993124050c0f1d0a606afd8a67008e3b1543", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75265", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1753747200000, + "to": 1754265600000 + }, + "returnedRange": { + "from": 1753747200000, + "to": 1754265600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "2303e1ed41760567b7d50b7f2eae9330412b7205fce1b8bda7c5d4acbce5b689", + "decodedReportSha256": "11b51670e7385aee23978021f3c1113303bedee5c232fbb39dca9ac3d1de1c7b", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:14.472Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/strategy.pine new file mode 100644 index 00000000..82f21e1f --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (M7, a limit opening): a leveraged long LIMIT +// entry (margin 5, 18.055 x equity, limit 0.997 x close) placed on the signal bar 2025-08-01 00:30 UTC and +// filled on the next bar on its way down; that bar's low then breaches and its close recovers. +// process_orders_on_close OFF. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-lim-0801-0030", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 8, 1, 0, 30) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(18.055 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 0.997, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/tv_trades.csv new file mode 100644 index 00000000..89f37f82 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-0801-0030/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-08-01 08:45,Margin call,3613.67,2.22,8117.4966,-95.1492,-1.17,0,0,0.00,-95.1492,-1.17,-95.1492,-9.51,0 +1,Entry long,2025-08-01 08:45,L,3656.53,2.22,8117.4966,-95.1492,-1.17,0,0,0.00,-95.1492,-1.17,-95.1492,-9.51,0 +2,Exit long,2025-08-01 09:45,timeout,3690.36,2.702,9879.94406,91.40866,0.93,0,103.94594,1.05,-115.80772,-1.17,-3.74054,-0.37,4 +2,Entry long,2025-08-01 08:45,L,3656.53,2.702,9879.94406,91.40866,0.93,0,103.94594,1.05,-115.80772,-1.17,-3.74054,-0.37,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/meta.json new file mode 100644 index 00000000..b7c3136a --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-lim-1201-0215", + "slug": "pm2-m7-lim-1201-0215", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:18.608Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-11-28", + "to": "2025-12-04" + }, + "pine_sha256": "9000e46fb8e67644555a5bb4ac566019f89a360bb254594aef0edb309a311b60", + "tv_trades_sha256": "8dbe22c6e8867a97756e1f8778358c2280096f6201d04ec250d8834e258abe10", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/metrics.json new file mode 100644 index 00000000..1e28cb39 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-lim-1201-0215", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-11-28", + "to": "2025-12-04", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "9000e46fb8e67644555a5bb4ac566019f89a360bb254594aef0edb309a311b60", + "tvTradesCsvHash": "8dbe22c6e8867a97756e1f8778358c2280096f6201d04ec250d8834e258abe10", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75266", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1764288000000, + "to": 1764806400000 + }, + "returnedRange": { + "from": 1764288000000, + "to": 1764806400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f6a1fa350d56378dfe2b78206b5d2ca661ac8f4237f4cc3d342c15d69c661aa3", + "decodedReportSha256": "f0ba77350026311b19216ec31f6e63320e60b2b2391fed26a7ccaf82eba3b382", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:18.607Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/strategy.pine new file mode 100644 index 00000000..293d5013 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (M7, a limit opening): a leveraged long LIMIT +// entry (margin 5, 18.855 x equity, limit 0.997 x close) placed on the signal bar 2025-12-01 02:15 UTC and +// filled on the next bar on its way down; that bar's low then breaches and its close recovers. +// process_orders_on_close OFF. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-lim-1201-0215", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 12, 1, 2, 15) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(18.855 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 0.997, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/tv_trades.csv new file mode 100644 index 00000000..3a4087b1 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-lim-1201-0215/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-12-01 10:30,Margin call,2824.6,1.7024,4841.2,-32.60096,-0.67,0,0,0.00,-32.60096,-0.67,-32.60096,-3.26,0 +1,Entry long,2025-12-01 10:30,L,2843.75,1.7024,4841.2,-32.60096,-0.67,0,0,0.00,-32.60096,-0.67,-32.60096,-3.26,0 +2,Exit long,2025-12-01 11:30,timeout,2826.4,4.9076,13955.9875,-85.14686,-0.61,0,37.248684,0.27,-152.08652,-1.09,-117.74782,-11.77,4 +2,Entry long,2025-12-01 10:30,L,2843.75,4.9076,13955.9875,-85.14686,-0.61,0,37.248684,0.27,-152.08652,-1.09,-117.74782,-11.77,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/meta.json new file mode 100644 index 00000000..ff13a088 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-s1lim-0402-1330", + "slug": "pm2-m7-s1lim-0402-1330", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T02:22:18.968Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05" + }, + "pine_sha256": "bd1fe9b27884add3c44fc7572602e451f18804e41ff00bb8bcb3992e433ff50e", + "tv_trades_sha256": "d633d75493717e91991b72a05e506c002e4ca0287a8437d142e68cdbd75fa17f", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/metrics.json new file mode 100644 index 00000000..f39fd165 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-s1lim-0402-1330", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "bd1fe9b27884add3c44fc7572602e451f18804e41ff00bb8bcb3992e433ff50e", + "tvTradesCsvHash": "d633d75493717e91991b72a05e506c002e4ca0287a8437d142e68cdbd75fa17f", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78647", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "returnedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "83942a50c43cd63896cdd15b506dc8514165a46a45d3d7ae634b4998781f3007", + "decodedReportSha256": "16cf1f9bb0eedf22e9da1b9908a99404f9cfbc83855864e80f100888552f0998", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T02:22:18.967Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/strategy.pine new file mode 100644 index 00000000..aedfcdc4 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7, a SHORT limit opening): a short LIMIT entry (margin 100, 0.989 x equity, +// limit 1.003 x close) placed on the signal bar 2025-04-02 13:30 UTC and filled on the next bar on its +// way UP; that bar's high then breaches and its close recovers. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-s1lim-0402-1330", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=100, margin_short=100, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 4, 2, 13, 30) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(0.989 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 1.003, 2)) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/tv_trades.csv new file mode 100644 index 00000000..063df7fa --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0402-1330/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-04-02 21:45,Margin call,1892.7,0.0152,28.542712,-0.226328,-0.79,0,0,0.00,-0.226328,-0.79,-0.226328,-0.02,0 +1,Entry short,2025-04-02 21:45,S,1877.81,0.0152,28.542712,-0.226328,-0.79,0,0,0.00,-0.226328,-0.79,-0.226328,-0.02,0 +2,Exit short,2025-04-02 22:45,timeout,1870.74,0.5128,962.940968,3.625496,0.38,0,10.927768,1.13,-7.635592,-0.79,3.399168,0.34,4 +2,Entry short,2025-04-02 21:45,S,1877.81,0.5128,962.940968,3.625496,0.38,0,10.927768,1.13,-7.635592,-0.79,3.399168,0.34,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/meta.json new file mode 100644 index 00000000..b26bb4d7 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-s1lim-0509-1100", + "slug": "pm2-m7-s1lim-0509-1100", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T02:22:22.830Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-05-06", + "to": "2025-05-12" + }, + "pine_sha256": "bde14584dec3c9eb6f1dc6248537cb712824f1406d6074fa7e818800fdeb10dc", + "tv_trades_sha256": "5f62b5db446e583a8707f142e9b1510901192f664646206a2bcdb8d693a95e34", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/metrics.json new file mode 100644 index 00000000..c730b322 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-s1lim-0509-1100", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-05-06", + "to": "2025-05-12", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "bde14584dec3c9eb6f1dc6248537cb712824f1406d6074fa7e818800fdeb10dc", + "tvTradesCsvHash": "5f62b5db446e583a8707f142e9b1510901192f664646206a2bcdb8d693a95e34", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78650", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1746489600000, + "to": 1747008000000 + }, + "returnedRange": { + "from": 1746489600000, + "to": 1747008000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "ce22bdbeeb933b0e86a93b8da56c592a24cd30eb53415d43c6a277d3089f3882", + "decodedReportSha256": "1210c297fb17406753fee08420b913081feb05356d2f428beb9c5ba36c2b2a3d", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T02:22:22.829Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/strategy.pine new file mode 100644 index 00000000..6cf25c73 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7, a SHORT limit opening): a short LIMIT entry (margin 100, 0.99 x equity, +// limit 1.003 x close) placed on the signal bar 2025-05-09 11:00 UTC and filled on the next bar on its +// way UP; that bar's high then breaches and its close recovers. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-s1lim-0509-1100", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=100, margin_short=100, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 5, 9, 11, 0) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(0.99 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 1.003, 2)) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/tv_trades.csv new file mode 100644 index 00000000..478d37dd --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0509-1100/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-05-09 19:15,Margin call,2375.39,0.0132,31.104612,-0.250536,-0.81,0,0,0.00,-0.250536,-0.81,-0.250536,-0.03,0 +1,Entry short,2025-05-09 19:15,S,2356.41,0.0132,31.104612,-0.250536,-0.81,0,0,0.00,-0.250536,-0.81,-0.250536,-0.03,0 +2,Exit short,2025-05-09 20:15,timeout,2330.22,0.4078,960.943998,10.680282,1.11,0,18.18788,1.89,-7.740044,-0.81,10.429746,1.04,4 +2,Entry short,2025-05-09 19:15,S,2356.41,0.4078,960.943998,10.680282,1.11,0,18.18788,1.89,-7.740044,-0.81,10.429746,1.04,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/meta.json new file mode 100644 index 00000000..4bcea773 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-s1lim-0709-1930", + "slug": "pm2-m7-s1lim-0709-1930", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T02:22:28.419Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-06", + "to": "2025-07-12" + }, + "pine_sha256": "5ef8921652a5c0e11fa9518d765cbc3c44e6cd9fdcb4f707fc4f53bdbafbecf3", + "tv_trades_sha256": "8d7299c2d5e6ae4fb2c406e5bb5ed0b94a169d019967bfc5c3b24a87adceffcc", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/metrics.json new file mode 100644 index 00000000..dd888e10 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-s1lim-0709-1930", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-06", + "to": "2025-07-12", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "5ef8921652a5c0e11fa9518d765cbc3c44e6cd9fdcb4f707fc4f53bdbafbecf3", + "tvTradesCsvHash": "8d7299c2d5e6ae4fb2c406e5bb5ed0b94a169d019967bfc5c3b24a87adceffcc", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78655", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1751760000000, + "to": 1752278400000 + }, + "returnedRange": { + "from": 1751760000000, + "to": 1752278400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "83ec1fa15146f6d98da614afdc2790dfddc410a7d45cc4a7e3f17d5227348442", + "decodedReportSha256": "bd53a256dc64ed00cc90f704916ae1fc04e81d8159b892547563d657df1f56e6", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T02:22:28.419Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/strategy.pine new file mode 100644 index 00000000..a5a44004 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7, a SHORT limit opening): a short LIMIT entry (margin 100, 0.989 x equity, +// limit 1.003 x close) placed on the signal bar 2025-07-09 19:30 UTC and filled on the next bar on its +// way UP; that bar's high then breaches and its close recovers. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-s1lim-0709-1930", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=100, margin_short=100, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 7, 9, 19, 30) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(0.989 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 1.003, 2)) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/tv_trades.csv new file mode 100644 index 00000000..d0bee5ed --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-0709-1930/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-10 03:45,Margin call,2795.73,0.0144,39.887712,-0.3708,-0.93,0,0,0.00,-0.3708,-0.93,-0.3708,-0.04,0 +1,Entry short,2025-07-10 03:45,S,2769.98,0.0144,39.887712,-0.3708,-0.93,0,0,0.00,-0.3708,-0.93,-0.3708,-0.04,0 +2,Exit short,2025-07-10 04:45,timeout,2737.55,0.3436,951.765128,11.142948,1.17,0,13.668408,1.44,-8.8477,-0.93,10.772148,1.08,4 +2,Entry short,2025-07-10 03:45,S,2769.98,0.3436,951.765128,11.142948,1.17,0,13.668408,1.44,-8.8477,-0.93,10.772148,1.08,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/meta.json new file mode 100644 index 00000000..8b6dfeac --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-s1lim-1001-0830", + "slug": "pm2-m7-s1lim-1001-0830", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T02:22:31.843Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-09-28", + "to": "2025-10-04" + }, + "pine_sha256": "bc331ba7eee04920d2bff9a9f43c9cc0db7590dd65a4f79ddd4984898d2b7d2a", + "tv_trades_sha256": "8981c3291ddf2d1409543bd6a3ad034995d57e6745a9d36be9b1f64eaa2ef64e", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/metrics.json new file mode 100644 index 00000000..7445967a --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-s1lim-1001-0830", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-09-28", + "to": "2025-10-04", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "bc331ba7eee04920d2bff9a9f43c9cc0db7590dd65a4f79ddd4984898d2b7d2a", + "tvTradesCsvHash": "8981c3291ddf2d1409543bd6a3ad034995d57e6745a9d36be9b1f64eaa2ef64e", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78658", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1759017600000, + "to": 1759536000000 + }, + "returnedRange": { + "from": 1759017600000, + "to": 1759536000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "dc697d92b34f7831929a0df03c291a6a8120a80ca5d9ced40e1662d5667a0075", + "decodedReportSha256": "6bfea609da6882e1b3d4ca74212aec4c1548c1f805e8900910fe070b2dd97c0e", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T02:22:31.842Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/strategy.pine new file mode 100644 index 00000000..5c07c425 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7, a SHORT limit opening): a short LIMIT entry (margin 100, 0.987 x equity, +// limit 1.003 x close) placed on the signal bar 2025-10-01 08:30 UTC and filled on the next bar on its +// way UP; that bar's high then breaches and its close recovers. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-s1lim-1001-0830", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=100, margin_short=100, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 10, 1, 8, 30) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(0.987 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 1.003, 2)) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/tv_trades.csv new file mode 100644 index 00000000..f3717327 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-s1lim-1001-0830/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-10-01 16:45,Margin call,4329,0.0096,41.121216,-0.437184,-1.06,0,0,0.00,-0.437184,-1.06,-0.437184,-0.04,0 +1,Entry short,2025-10-01 16:45,S,4283.46,0.0096,41.121216,-0.437184,-1.06,0,0,0.00,-0.437184,-1.06,-0.437184,-0.04,0 +2,Exit short,2025-10-01 17:45,timeout,4300.66,0.2214,948.358044,-3.80808,-0.40,0,1.337256,0.14,-10.082556,-1.06,-4.245264,-0.42,4 +2,Entry short,2025-10-01 16:45,S,4283.46,0.2214,948.358044,-3.80808,-0.40,0,1.337256,0.14,-10.082556,-1.06,-4.245264,-0.42,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/meta.json new file mode 100644 index 00000000..700a397d --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-slim-0402-1330", + "slug": "pm2-m7-slim-0402-1330", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T02:22:36.574Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05" + }, + "pine_sha256": "ce866c18b22bfe031326a6ca9bd00a9b1e18d2c0c2e0f126ee121d13fb7094bd", + "tv_trades_sha256": "1d5f09118992a3fdb4eb1e2add229a11c5db524525039daa214044f3ccf6ba8b", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/metrics.json new file mode 100644 index 00000000..8fac1ca9 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-slim-0402-1330", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "ce866c18b22bfe031326a6ca9bd00a9b1e18d2c0c2e0f126ee121d13fb7094bd", + "tvTradesCsvHash": "1d5f09118992a3fdb4eb1e2add229a11c5db524525039daa214044f3ccf6ba8b", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78663", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "returnedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "30d2f749db8e7b8e3c27cbc58952d0f122d0b5f1f0ebf06b0c8abf648e55d3b8", + "decodedReportSha256": "7b70d26efaa5f6d1015c6657aa2db9b753a1d4e998aeeb6563111bcd968d828c", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T02:22:36.573Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/strategy.pine new file mode 100644 index 00000000..96e17bd0 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7, a SHORT limit opening): a short LIMIT entry (margin 5, 18.41 x equity, +// limit 1.003 x close) placed on the signal bar 2025-04-02 13:30 UTC and filled on the next bar on its +// way UP; that bar's high then breaches and its close recovers. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-slim-0402-1330", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 4, 2, 13, 30) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(18.41 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 1.003, 2)) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/tv_trades.csv new file mode 100644 index 00000000..399fb3a0 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0402-1330/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-04-02 21:45,Margin call,1892.7,3.2528,6108.140368,-48.434193,-0.79,0,0,0.00,-48.434193,-0.79,-48.434193,-4.84,0 +1,Entry short,2025-04-02 21:45,S,1877.81,3.2528,6108.140368,-48.434193,-0.79,0,0,0.00,-48.434193,-0.79,-48.434193,-4.84,0 +2,Exit short,2025-04-02 22:45,timeout,1870.74,6.5802,12356.365362,46.522015,0.38,0,140.22406,1.13,-97.97918,-0.79,-1.912178,-0.19,4 +2,Entry short,2025-04-02 21:45,S,1877.81,6.5802,12356.365362,46.522015,0.38,0,140.22406,1.13,-97.97918,-0.79,-1.912178,-0.19,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/meta.json new file mode 100644 index 00000000..73a95507 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-slim-0509-1100", + "slug": "pm2-m7-slim-0509-1100", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T02:22:40.208Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-05-06", + "to": "2025-05-12" + }, + "pine_sha256": "10de790d79193a78efe7c03ff55df6bf6443adc430d75a2bed2a135e65dea6ad", + "tv_trades_sha256": "8b448336731119f702f95690ff11b9f3db3ed887c7fad0d6e9bbdf325b4062b0", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/metrics.json new file mode 100644 index 00000000..bd424399 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-slim-0509-1100", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-05-06", + "to": "2025-05-12", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "10de790d79193a78efe7c03ff55df6bf6443adc430d75a2bed2a135e65dea6ad", + "tvTradesCsvHash": "8b448336731119f702f95690ff11b9f3db3ed887c7fad0d6e9bbdf325b4062b0", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78666", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1746489600000, + "to": 1747008000000 + }, + "returnedRange": { + "from": 1746489600000, + "to": 1747008000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "054991be4cd6cf8b83839c84185e69eac59c4cc6b275e5e1fd29cc6ea55104cc", + "decodedReportSha256": "d889939e3b275ed3f60c2eaddeda1498878642c3f2a4c7bc676cc30a970526e7", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T02:22:40.208Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/strategy.pine new file mode 100644 index 00000000..9ccc18d0 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7, a SHORT limit opening): a short LIMIT entry (margin 5, 18.385 x equity, +// limit 1.003 x close) placed on the signal bar 2025-05-09 11:00 UTC and filled on the next bar on its +// way UP; that bar's high then breaches and its close recovers. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-slim-0509-1100", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 5, 9, 11, 0) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(18.385 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 1.003, 2)) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/tv_trades.csv new file mode 100644 index 00000000..c644c18c --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0509-1100/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-05-09 19:15,Margin call,2375.39,2.6232,6181.334712,-49.788338,-0.81,0,0,0.00,-49.788338,-0.81,-49.788338,-4.98,0 +1,Entry short,2025-05-09 19:15,S,2356.41,2.6232,6181.334712,-49.788338,-0.81,0,0,0.00,-49.788338,-0.81,-49.788338,-4.98,0 +2,Exit short,2025-05-09 20:15,timeout,2330.22,5.2018,12257.573538,136.23514,1.11,0,232.00027,1.89,-98.73016,-0.81,86.44681,8.64,4 +2,Entry short,2025-05-09 19:15,S,2356.41,5.2018,12257.573538,136.23514,1.11,0,232.00027,1.89,-98.73016,-0.81,86.44681,8.64,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/meta.json new file mode 100644 index 00000000..284df3c7 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-slim-0709-1930", + "slug": "pm2-m7-slim-0709-1930", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T02:22:44.324Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-06", + "to": "2025-07-12" + }, + "pine_sha256": "22e072a1110e272af8d61114307d8ff99ed6aa043fab6f94a7a9b806f85a62f9", + "tv_trades_sha256": "02f292a474aef9790a1a99f58848cbd2409426bcb42ba48b404126804f3181f9", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/metrics.json new file mode 100644 index 00000000..a6717f05 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-slim-0709-1930", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-06", + "to": "2025-07-12", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "22e072a1110e272af8d61114307d8ff99ed6aa043fab6f94a7a9b806f85a62f9", + "tvTradesCsvHash": "02f292a474aef9790a1a99f58848cbd2409426bcb42ba48b404126804f3181f9", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78669", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1751760000000, + "to": 1752278400000 + }, + "returnedRange": { + "from": 1751760000000, + "to": 1752278400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f7a674686b669e8c310df837afd53f34eda8db80771087057db197d99f191f95", + "decodedReportSha256": "75ec069325170cd510021a73a658b3156eba563c973ef6e01d5d1af795e2b122", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T02:22:44.323Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/strategy.pine new file mode 100644 index 00000000..cdc85105 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7, a SHORT limit opening): a short LIMIT entry (margin 5, 18.17 x equity, +// limit 1.003 x close) placed on the signal bar 2025-07-09 19:30 UTC and filled on the next bar on its +// way UP; that bar's high then breaches and its close recovers. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-slim-0709-1930", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 7, 9, 19, 30) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(18.17 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 1.003, 2)) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/tv_trades.csv new file mode 100644 index 00000000..9875e351 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-0709-1930/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-07-10 03:45,Margin call,2795.73,2.5484,7059.017032,-65.6213,-0.93,0,0,0.00,-65.6213,-0.93,-65.6213,-6.56,0 +1,Entry short,2025-07-10 03:45,S,2769.98,2.5484,7059.017032,-65.6213,-0.93,0,0,0.00,-65.6213,-0.93,-65.6213,-6.56,0 +2,Exit short,2025-07-10 04:45,timeout,2737.55,4.0306,11164.681388,130.71236,1.17,0,160.33727,1.44,-103.78795,-0.93,65.09106,6.51,4 +2,Entry short,2025-07-10 03:45,S,2769.98,4.0306,11164.681388,130.71236,1.17,0,160.33727,1.44,-103.78795,-0.93,65.09106,6.51,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/meta.json new file mode 100644 index 00000000..0cf22a37 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-slim-1001-0830", + "slug": "pm2-m7-slim-1001-0830", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T02:22:47.697Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-09-28", + "to": "2025-10-04" + }, + "pine_sha256": "cfe50454468d9fd734358b050411520358ab0eca10280931c475db88df240e24", + "tv_trades_sha256": "320a3e2199cfaa915863d2aff6f336d0afa8511daca6aae6777a8ad08f441855", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/metrics.json new file mode 100644 index 00000000..410e0b72 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-slim-1001-0830", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-09-28", + "to": "2025-10-04", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "cfe50454468d9fd734358b050411520358ab0eca10280931c475db88df240e24", + "tvTradesCsvHash": "320a3e2199cfaa915863d2aff6f336d0afa8511daca6aae6777a8ad08f441855", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.78672", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1759017600000, + "to": 1759536000000 + }, + "returnedRange": { + "from": 1759017600000, + "to": 1759536000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "eb35e545b7e55d79679cb1a480352cd443824d5d4864702d40d7f8546829c823", + "decodedReportSha256": "03c4c64b4d40fcd8382db6e6a4839173f9042161893bba18039e309ce1b244ae", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T02:22:47.696Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/strategy.pine new file mode 100644 index 00000000..ba2b84ef --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7, a SHORT limit opening): a short LIMIT entry (margin 5, 17.685 x equity, +// limit 1.003 x close) placed on the signal bar 2025-10-01 08:30 UTC and filled on the next bar on its +// way UP; that bar's high then breaches and its close recovers. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-slim-1001-0830", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 10, 1, 8, 30) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(17.685 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 1.003, 2)) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/tv_trades.csv new file mode 100644 index 00000000..2e991ab9 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-slim-1001-0830/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-10-01 16:45,Margin call,4329,1.5688,6719.892048,-71.44315,-1.06,0,0,0.00,-71.44315,-1.06,-71.44315,-7.14,0 +1,Entry short,2025-10-01 16:45,S,4283.46,1.5688,6719.892048,-71.44315,-1.06,0,0,0.00,-71.44315,-1.06,-71.44315,-7.14,0 +2,Exit short,2025-10-01 17:45,timeout,4300.66,2.5722,11017.915812,-44.24184,-0.40,0,15.536088,0.14,-117.137985,-1.06,-115.68499,-11.57,4 +2,Entry short,2025-10-01 16:45,S,4283.46,2.5722,11017.915812,-44.24184,-0.40,0,15.536088,0.14,-117.137985,-1.06,-115.68499,-11.57,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2_bars.inc b/tests/fixtures/margin_entry_bar/pm2_bars.inc new file mode 100644 index 00000000..49284389 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2_bars.inc @@ -0,0 +1,537 @@ +// R5 lane PAR-MARGIN-2: ETH bars per lab tv tape of the M7 shapes, from the corpus feed +// corpus/data/ohlcv_ETH-USDT-USDT_1m.csv (15m bars aggregated from it as the derived feed does: +// first open, highest high, lowest low, last close). {timestamp_ms, open, high, low, close} +constexpr FeedBar kPm2Chart_0402_1945[6] = { // signal bar 2025-04-02 19:45 UTC .. +5 + {1743623100000LL, 1905.25, 1915.28, 1904.98, 1910.56}, + {1743624000000LL, 1910.59, 1946.03, 1881, 1933.11}, + {1743624900000LL, 1933.11, 1957.97, 1872.48, 1897.82}, + {1743625800000LL, 1897.82, 1904.55, 1865, 1870.06}, + {1743626700000LL, 1870, 1883.75, 1866.79, 1880}, + {1743627600000LL, 1880.16, 1883.91, 1853.93, 1859.77}, +}; +constexpr FeedBar kPm2Chart_0402_2000[6] = { // signal bar 2025-04-02 20:00 UTC .. +5 + {1743624000000LL, 1910.59, 1946.03, 1881, 1933.11}, + {1743624900000LL, 1933.11, 1957.97, 1872.48, 1897.82}, + {1743625800000LL, 1897.82, 1904.55, 1865, 1870.06}, + {1743626700000LL, 1870, 1883.75, 1866.79, 1880}, + {1743627600000LL, 1880.16, 1883.91, 1853.93, 1859.77}, + {1743628500000LL, 1859.76, 1878.45, 1856.75, 1862.71}, +}; +constexpr FeedBar kPm2Chart_0801_0030[6] = { // signal bar 2025-08-01 00:30 UTC .. +5 + {1754008200000LL, 3688.71, 3690.61, 3666.43, 3667.53}, + {1754009100000LL, 3667.52, 3685.46, 3613.67, 3680.01}, + {1754010000000LL, 3680.01, 3694.46, 3650.04, 3655}, + {1754010900000LL, 3655, 3693.09, 3651.83, 3681.48}, + {1754011800000LL, 3681.48, 3695, 3669.45, 3690.36}, + {1754012700000LL, 3690.36, 3695, 3671.74, 3687.1}, +}; +constexpr FeedBar kPm2Chart_1201_0215[6] = { // signal bar 2025-12-01 02:15 UTC .. +5 + {1764555300000LL, 2865.71, 2867.73, 2852.31, 2852.31}, + {1764556200000LL, 2852.3, 2861.76, 2824.6, 2850.67}, + {1764557100000LL, 2850.66, 2851.34, 2833.75, 2836.79}, + {1764558000000LL, 2836.8, 2841.86, 2812.76, 2824.21}, + {1764558900000LL, 2824.21, 2833.33, 2817.74, 2826.4}, + {1764559800000LL, 2826.4, 2830.8, 2812.5, 2830.6}, +}; +constexpr FeedBar kPm2Chart_0105_1415[6] = { // signal bar 2026-01-05 14:15 UTC .. +5 + {1767622500000LL, 3150.57, 3154.99, 3148.55, 3153.18}, + {1767623400000LL, 3153.18, 3170.65, 3132.58, 3168.01}, + {1767624300000LL, 3168, 3176.5, 3147.05, 3165.42}, + {1767625200000LL, 3165.42, 3170.34, 3150.29, 3161.08}, + {1767626100000LL, 3161.08, 3173.82, 3154, 3163.23}, + {1767627000000LL, 3163.22, 3205, 3161, 3191.5}, +}; +constexpr FeedBar kPm2Chart_0702_0100[6] = { // signal bar 2025-07-02 01:00 UTC .. +5 + {1751418000000LL, 2397.4, 2400.99, 2397.4, 2400.99}, + {1751418900000LL, 2400.98, 2405.28, 2367.53, 2404.79}, + {1751419800000LL, 2404.8, 2410, 2401, 2409.01}, + {1751420700000LL, 2409, 2410.58, 2406.33, 2410.03}, + {1751421600000LL, 2410.02, 2417.16, 2406.8, 2407.51}, + {1751422500000LL, 2407.51, 2413.91, 2406.02, 2413.38}, +}; +constexpr FeedBar kPm2Chart_1002_1445[6] = { // signal bar 2025-10-02 14:45 UTC .. +5 + {1759416300000LL, 4384.28, 4390, 4364.58, 4372.55}, + {1759417200000LL, 4372.54, 4394.32, 4332.23, 4390.98}, + {1759418100000LL, 4390.98, 4417.96, 4387.17, 4405.82}, + {1759419000000LL, 4405.82, 4414.37, 4390.44, 4408.26}, + {1759419900000LL, 4408.27, 4441.67, 4406.48, 4435.49}, + {1759420800000LL, 4435.5, 4469, 4428.67, 4456.99}, +}; +constexpr FeedBar kPm2Chart_0402_1330[6] = { // signal bar 2025-04-02 13:30 UTC .. +5 + {1743600600000LL, 1858.15, 1876.61, 1853.12, 1872.19}, + {1743601500000LL, 1872.27, 1892.7, 1869.82, 1870.41}, + {1743602400000LL, 1870.4, 1880.71, 1856.5, 1863.13}, + {1743603300000LL, 1863.14, 1872.65, 1860.01, 1870.75}, + {1743604200000LL, 1870.75, 1875.85, 1863.52, 1870.75}, + {1743605100000LL, 1870.74, 1877.04, 1868.55, 1874.69}, +}; +constexpr FeedBar kPm2Chart_0509_1100[6] = { // signal bar 2025-05-09 11:00 UTC .. +5 + {1746788400000LL, 2348.17, 2352.18, 2332.09, 2349.36}, + {1746789300000LL, 2349.36, 2375.39, 2311.81, 2342.63}, + {1746790200000LL, 2342.67, 2352, 2333, 2351.64}, + {1746791100000LL, 2351.64, 2360.44, 2343.27, 2350.76}, + {1746792000000LL, 2350.75, 2352.81, 2325.21, 2330.21}, + {1746792900000LL, 2330.22, 2342.48, 2325.09, 2340.06}, +}; +constexpr FeedBar kPm2Chart_0709_1930[6] = { // signal bar 2025-07-09 19:30 UTC .. +5 + {1752089400000LL, 2741.09, 2763.54, 2735, 2761.69}, + {1752090300000LL, 2761.69, 2795.73, 2750.74, 2763.56}, + {1752091200000LL, 2763.56, 2765.18, 2743.2, 2748.75}, + {1752092100000LL, 2748.75, 2757.6, 2737.55, 2744.73}, + {1752093000000LL, 2744.73, 2744.91, 2730.2, 2737.55}, + {1752093900000LL, 2737.55, 2741.57, 2732.9, 2737.03}, +}; +constexpr FeedBar kPm2Chart_1001_0830[6] = { // signal bar 2025-10-01 08:30 UTC .. +5 + {1759307400000LL, 4164.77, 4274.97, 4164.77, 4270.65}, + {1759308300000LL, 4270.65, 4329, 4267.55, 4289.99}, + {1759309200000LL, 4289.99, 4301.01, 4277.76, 4284.91}, + {1759310100000LL, 4284.91, 4292.92, 4277.42, 4292.89}, + {1759311000000LL, 4292.9, 4304.87, 4288.9, 4300.67}, + {1759311900000LL, 4300.66, 4317.11, 4298.05, 4308.43}, +}; +constexpr FeedBar kPm2Add_0402_1930[9] = { // L1 signal bar 2025-04-02 19:30 UTC .. +8 + {1743622200000LL, 1899.69, 1905.9, 1896.53, 1905.26}, + {1743623100000LL, 1905.25, 1915.28, 1904.98, 1910.56}, + {1743624000000LL, 1910.59, 1946.03, 1881, 1933.11}, + {1743624900000LL, 1933.11, 1957.97, 1872.48, 1897.82}, + {1743625800000LL, 1897.82, 1904.55, 1865, 1870.06}, + {1743626700000LL, 1870, 1883.75, 1866.79, 1880}, + {1743627600000LL, 1880.16, 1883.91, 1853.93, 1859.77}, + {1743628500000LL, 1859.76, 1878.45, 1856.75, 1862.71}, + {1743629400000LL, 1862.71, 1870.79, 1859.48, 1869}, +}; +constexpr FeedBar kPm2Add_0402_1945[9] = { // L1 signal bar 2025-04-02 19:45 UTC .. +8 + {1743623100000LL, 1905.25, 1915.28, 1904.98, 1910.56}, + {1743624000000LL, 1910.59, 1946.03, 1881, 1933.11}, + {1743624900000LL, 1933.11, 1957.97, 1872.48, 1897.82}, + {1743625800000LL, 1897.82, 1904.55, 1865, 1870.06}, + {1743626700000LL, 1870, 1883.75, 1866.79, 1880}, + {1743627600000LL, 1880.16, 1883.91, 1853.93, 1859.77}, + {1743628500000LL, 1859.76, 1878.45, 1856.75, 1862.71}, + {1743629400000LL, 1862.71, 1870.79, 1859.48, 1869}, + {1743630300000LL, 1868.99, 1869, 1858.69, 1866.28}, +}; +constexpr FeedBar kPm2Add_0801_0015[9] = { // L1 signal bar 2025-08-01 00:15 UTC .. +8 + {1754007300000LL, 3656.89, 3698.08, 3655, 3688.71}, + {1754008200000LL, 3688.71, 3690.61, 3666.43, 3667.53}, + {1754009100000LL, 3667.52, 3685.46, 3613.67, 3680.01}, + {1754010000000LL, 3680.01, 3694.46, 3650.04, 3655}, + {1754010900000LL, 3655, 3693.09, 3651.83, 3681.48}, + {1754011800000LL, 3681.48, 3695, 3669.45, 3690.36}, + {1754012700000LL, 3690.36, 3695, 3671.74, 3687.1}, + {1754013600000LL, 3687.11, 3710, 3678.29, 3706.4}, + {1754014500000LL, 3706.39, 3717.73, 3701.99, 3706.48}, +}; +constexpr FeedBar kPm2Add_1207_1345[9] = { // L1 signal bar 2025-12-07 13:45 UTC .. +8 + {1765115100000LL, 3029.39, 3033.4, 3027.15, 3030.18}, + {1765116000000LL, 3030.19, 3034.63, 3021.06, 3024}, + {1765116900000LL, 3024, 3027.99, 2903.75, 2938.07}, + {1765117800000LL, 2938.08, 2957.18, 2924.97, 2946.78}, + {1765118700000LL, 2946.77, 2953.66, 2938.89, 2941.32}, + {1765119600000LL, 2941.32, 2974.85, 2938.5, 2959.79}, + {1765120500000LL, 2959.78, 2968.66, 2948.46, 2953.21}, + {1765121400000LL, 2953.2, 2984.67, 2952.68, 2971.37}, + {1765122300000LL, 2971.38, 3040.2, 2966.41, 3025.82}, +}; +constexpr FeedBar kPm2Add_0105_1400[9] = { // L1 signal bar 2026-01-05 14:00 UTC .. +8 + {1767621600000LL, 3153.01, 3159.31, 3149.02, 3150.58}, + {1767622500000LL, 3150.57, 3154.99, 3148.55, 3153.18}, + {1767623400000LL, 3153.18, 3170.65, 3132.58, 3168.01}, + {1767624300000LL, 3168, 3176.5, 3147.05, 3165.42}, + {1767625200000LL, 3165.42, 3170.34, 3150.29, 3161.08}, + {1767626100000LL, 3161.08, 3173.82, 3154, 3163.23}, + {1767627000000LL, 3163.22, 3205, 3161, 3191.5}, + {1767627900000LL, 3191.51, 3198.47, 3184.42, 3194.49}, + {1767628800000LL, 3194.48, 3201.69, 3184.15, 3188.43}, +}; +constexpr FeedBar kPm2Add_0702_0045[9] = { // L1 signal bar 2025-07-02 00:45 UTC .. +8 + {1751417100000LL, 2405.88, 2405.88, 2396.4, 2397.41}, + {1751418000000LL, 2397.4, 2400.99, 2397.4, 2400.99}, + {1751418900000LL, 2400.98, 2405.28, 2367.53, 2404.79}, + {1751419800000LL, 2404.8, 2410, 2401, 2409.01}, + {1751420700000LL, 2409, 2410.58, 2406.33, 2410.03}, + {1751421600000LL, 2410.02, 2417.16, 2406.8, 2407.51}, + {1751422500000LL, 2407.51, 2413.91, 2406.02, 2413.38}, + {1751423400000LL, 2413.38, 2414.81, 2410.2, 2414.33}, + {1751424300000LL, 2414.34, 2417.99, 2413.21, 2416.86}, +}; +constexpr FeedBar kPm2Add_1002_1430[9] = { // L1 signal bar 2025-10-02 14:30 UTC .. +8 + {1759415400000LL, 4413.77, 4417.41, 4380.72, 4384.28}, + {1759416300000LL, 4384.28, 4390, 4364.58, 4372.55}, + {1759417200000LL, 4372.54, 4394.32, 4332.23, 4390.98}, + {1759418100000LL, 4390.98, 4417.96, 4387.17, 4405.82}, + {1759419000000LL, 4405.82, 4414.37, 4390.44, 4408.26}, + {1759419900000LL, 4408.27, 4441.67, 4406.48, 4435.49}, + {1759420800000LL, 4435.5, 4469, 4428.67, 4456.99}, + {1759421700000LL, 4457, 4463.37, 4434.42, 4439.16}, + {1759422600000LL, 4439.15, 4449.18, 4423.33, 4430.89}, +}; +// ETH 1-minute bars per M7-A event, for the magnified re-exports (pm2-m7-mag-*). +constexpr FeedBar kPm2Mag_0621[90] = { // signal bar 2025-06-21 21:15 UTC + {1750540500000LL, 2394.21, 2394.21, 2390, 2391.6}, + {1750540560000LL, 2391.59, 2392.56, 2390.17, 2392.56}, + {1750540620000LL, 2392.58, 2396.44, 2392.58, 2394.01}, + {1750540680000LL, 2394.01, 2397, 2394, 2396.85}, + {1750540740000LL, 2396.85, 2396.85, 2395.29, 2395.29}, + {1750540800000LL, 2395.29, 2395.38, 2394, 2394}, + {1750540860000LL, 2394, 2394.01, 2390, 2391.2}, + {1750540920000LL, 2391.2, 2392.29, 2390.01, 2390.51}, + {1750540980000LL, 2390.51, 2390.65, 2386, 2387.12}, + {1750541040000LL, 2387.12, 2387.5, 2300, 2303.2}, + {1750541100000LL, 2301.28, 2345.36, 2296.97, 2342.71}, + {1750541160000LL, 2342.71, 2345.66, 2335, 2339.21}, + {1750541220000LL, 2339.22, 2339.88, 2321.55, 2324.75}, + {1750541280000LL, 2324.75, 2339.83, 2322.51, 2335.74}, + {1750541340000LL, 2335.74, 2339.13, 2331.15, 2338.34}, + {1750541400000LL, 2338.34, 2338.68, 2326.98, 2331.45}, + {1750541460000LL, 2331.45, 2331.87, 2230, 2254.15}, + {1750541520000LL, 2254, 2293, 2254, 2289.07}, + {1750541580000LL, 2289.53, 2292.64, 2263.07, 2278.01}, + {1750541640000LL, 2277.89, 2287.17, 2270, 2278.06}, + {1750541700000LL, 2278.06, 2288.8, 2270.4, 2281.55}, + {1750541760000LL, 2281.55, 2291.95, 2280.71, 2285.54}, + {1750541820000LL, 2285.54, 2287.15, 2274.57, 2280.64}, + {1750541880000LL, 2280.67, 2286.65, 2274.63, 2285.16}, + {1750541940000LL, 2285.16, 2288.48, 2283.43, 2284.62}, + {1750542000000LL, 2284.61, 2293.26, 2279.51, 2292.73}, + {1750542060000LL, 2292.77, 2299.65, 2290.68, 2299.21}, + {1750542120000LL, 2299.21, 2308.57, 2298.23, 2300.12}, + {1750542180000LL, 2300.12, 2300.12, 2292.48, 2296.08}, + {1750542240000LL, 2296.09, 2297.55, 2288.73, 2297.51}, + {1750542300000LL, 2297.51, 2298.55, 2280.41, 2285.94}, + {1750542360000LL, 2285.84, 2294.61, 2285.84, 2290.8}, + {1750542420000LL, 2290.79, 2291.96, 2284.12, 2285.81}, + {1750542480000LL, 2285.8, 2295.84, 2285.35, 2293.02}, + {1750542540000LL, 2292.97, 2299.96, 2290.9, 2299.77}, + {1750542600000LL, 2299.77, 2301.5, 2294.45, 2294.45}, + {1750542660000LL, 2294.44, 2295.18, 2289.15, 2289.37}, + {1750542720000LL, 2289.37, 2296.41, 2288.54, 2294.28}, + {1750542780000LL, 2294.27, 2295.84, 2290.79, 2294.07}, + {1750542840000LL, 2294.06, 2294.96, 2291.93, 2293.72}, + {1750542900000LL, 2293.73, 2293.73, 2286.35, 2286.6}, + {1750542960000LL, 2286.59, 2288.16, 2277.73, 2280.6}, + {1750543020000LL, 2280.61, 2286.84, 2279.83, 2283.17}, + {1750543080000LL, 2283.16, 2285.75, 2279.12, 2284.64}, + {1750543140000LL, 2284.64, 2284.64, 2274.47, 2280.35}, + {1750543200000LL, 2280.35, 2285.67, 2278.14, 2282.86}, + {1750543260000LL, 2282.86, 2291.58, 2282.05, 2290.71}, + {1750543320000LL, 2290.8, 2291.88, 2287.56, 2287.56}, + {1750543380000LL, 2287.57, 2289.3, 2284.43, 2288.16}, + {1750543440000LL, 2288.16, 2288.46, 2283, 2285.05}, + {1750543500000LL, 2285.05, 2291.61, 2284.88, 2290.73}, + {1750543560000LL, 2290.73, 2291.94, 2288.68, 2289.19}, + {1750543620000LL, 2289.2, 2295.78, 2288.64, 2295.4}, + {1750543680000LL, 2295.4, 2306.85, 2294.05, 2303.01}, + {1750543740000LL, 2303, 2304.56, 2298.65, 2303.6}, + {1750543800000LL, 2303.59, 2305.79, 2302.01, 2302.88}, + {1750543860000LL, 2302.9, 2303.3, 2299.19, 2301.46}, + {1750543920000LL, 2301.46, 2304.13, 2298, 2299.21}, + {1750543980000LL, 2299.22, 2301.28, 2298.29, 2299.3}, + {1750544040000LL, 2299.3, 2300.62, 2295.1, 2298.95}, + {1750544100000LL, 2298.94, 2301.17, 2296.41, 2299.02}, + {1750544160000LL, 2299.02, 2301, 2295.65, 2296.46}, + {1750544220000LL, 2296.46, 2297.33, 2293.61, 2295.44}, + {1750544280000LL, 2295.44, 2295.44, 2292.36, 2294.5}, + {1750544340000LL, 2294.5, 2295, 2292.84, 2293.86}, + {1750544400000LL, 2293.86, 2299.75, 2293.27, 2298.32}, + {1750544460000LL, 2298.32, 2302.36, 2297.76, 2301.9}, + {1750544520000LL, 2301.9, 2304.23, 2301.89, 2302.08}, + {1750544580000LL, 2302.08, 2302.65, 2296.07, 2298.79}, + {1750544640000LL, 2298.8, 2301.01, 2298.02, 2299.83}, + {1750544700000LL, 2299.83, 2302.89, 2299.7, 2302.84}, + {1750544760000LL, 2302.85, 2305.61, 2302.39, 2305.32}, + {1750544820000LL, 2305.31, 2305.32, 2302.85, 2304.63}, + {1750544880000LL, 2304.62, 2305.48, 2302.11, 2304.2}, + {1750544940000LL, 2304.2, 2304.21, 2299.82, 2301.61}, + {1750545000000LL, 2301.62, 2302.23, 2300, 2301.65}, + {1750545060000LL, 2301.64, 2302.18, 2296.47, 2299.33}, + {1750545120000LL, 2299.32, 2299.53, 2296.6, 2296.77}, + {1750545180000LL, 2296.77, 2299.18, 2296.76, 2298.87}, + {1750545240000LL, 2298.87, 2298.87, 2294.55, 2295.76}, + {1750545300000LL, 2295.76, 2297.69, 2294.71, 2296.01}, + {1750545360000LL, 2296.01, 2297.35, 2294.81, 2295.44}, + {1750545420000LL, 2295.43, 2296.57, 2294.17, 2295.4}, + {1750545480000LL, 2295.39, 2295.7, 2290.12, 2290.16}, + {1750545540000LL, 2290.16, 2290.16, 2279.43, 2285.49}, + {1750545600000LL, 2285.49, 2286.56, 2282.98, 2283}, + {1750545660000LL, 2283.01, 2284.96, 2282.67, 2284.32}, + {1750545720000LL, 2284.32, 2285.58, 2281.84, 2282.52}, + {1750545780000LL, 2282.54, 2284.5, 2277.77, 2278.25}, + {1750545840000LL, 2278.26, 2281.23, 2277.22, 2280.4}, +}; +constexpr FeedBar kPm2Mag_0922[90] = { // signal bar 2025-09-22 05:45 UTC + {1758519900000LL, 4278.21, 4280.59, 4276.15, 4280.58}, + {1758519960000LL, 4280.59, 4284.74, 4280.59, 4284.74}, + {1758520020000LL, 4284.74, 4285, 4283, 4283.01}, + {1758520080000LL, 4283, 4285, 4283, 4284.56}, + {1758520140000LL, 4284.57, 4284.57, 4280.66, 4282.42}, + {1758520200000LL, 4282.42, 4284.56, 4281.31, 4281.31}, + {1758520260000LL, 4281.31, 4282.41, 4280.51, 4282.41}, + {1758520320000LL, 4282.41, 4282.41, 4281, 4281.53}, + {1758520380000LL, 4281.53, 4281.54, 4280.45, 4281.24}, + {1758520440000LL, 4281.24, 4282.44, 4281.12, 4281.66}, + {1758520500000LL, 4281.66, 4282.52, 4280.57, 4282.11}, + {1758520560000LL, 4282.11, 4282.27, 4280.48, 4280.49}, + {1758520620000LL, 4280.48, 4281.05, 4277.9, 4279.03}, + {1758520680000LL, 4279.04, 4279.05, 4278, 4278.06}, + {1758520740000LL, 4278.06, 4278.07, 4210, 4217.7}, + {1758520800000LL, 4217.7, 4224.69, 4076.79, 4174.99}, + {1758520860000LL, 4174.98, 4185.68, 4150, 4160.48}, + {1758520920000LL, 4160.48, 4164.26, 4000.88, 4085.94}, + {1758520980000LL, 4085.94, 4143.72, 4085.45, 4138.17}, + {1758521040000LL, 4140.61, 4149.11, 4119.47, 4131.94}, + {1758521100000LL, 4131.94, 4144.84, 4128.6, 4141.73}, + {1758521160000LL, 4141.73, 4149.36, 4131.94, 4138.61}, + {1758521220000LL, 4138.62, 4155.69, 4136.77, 4150.93}, + {1758521280000LL, 4150.92, 4159.41, 4137, 4138.64}, + {1758521340000LL, 4138.63, 4151.9, 4137, 4151.89}, + {1758521400000LL, 4151.9, 4159, 4149.28, 4157.78}, + {1758521460000LL, 4157.78, 4159.99, 4144.52, 4146.46}, + {1758521520000LL, 4146.46, 4157.42, 4145.56, 4153.96}, + {1758521580000LL, 4153.95, 4153.95, 4148.02, 4153.03}, + {1758521640000LL, 4153.03, 4156, 4151.63, 4152.54}, + {1758521700000LL, 4152.54, 4154.27, 4144.24, 4153.09}, + {1758521760000LL, 4153.1, 4173.61, 4152.52, 4173.11}, + {1758521820000LL, 4173.11, 4176.36, 4165.1, 4169.81}, + {1758521880000LL, 4169.81, 4172.34, 4166, 4171.81}, + {1758521940000LL, 4171.81, 4180, 4171.39, 4175.65}, + {1758522000000LL, 4175.66, 4178.94, 4172, 4176.37}, + {1758522060000LL, 4176.36, 4176.4, 4168.14, 4168.14}, + {1758522120000LL, 4168.15, 4171.87, 4162.53, 4170.04}, + {1758522180000LL, 4170.04, 4185.79, 4170.03, 4184.59}, + {1758522240000LL, 4184.6, 4193.6, 4184.51, 4190.01}, + {1758522300000LL, 4190.01, 4202.15, 4185.6, 4192.99}, + {1758522360000LL, 4192.99, 4195.41, 4181.83, 4183.24}, + {1758522420000LL, 4183.24, 4185.24, 4177.27, 4181.99}, + {1758522480000LL, 4181.99, 4182.71, 4173, 4175.9}, + {1758522540000LL, 4175.91, 4182.3, 4175.68, 4178.59}, + {1758522600000LL, 4178.6, 4179.7, 4170.94, 4172.83}, + {1758522660000LL, 4172.82, 4184.96, 4172.65, 4178.69}, + {1758522720000LL, 4178.69, 4180.13, 4176.92, 4179.14}, + {1758522780000LL, 4179.15, 4180, 4175.54, 4178.56}, + {1758522840000LL, 4178.6, 4188.6, 4176.22, 4188.38}, + {1758522900000LL, 4188.37, 4194.96, 4185.08, 4194}, + {1758522960000LL, 4194, 4196.75, 4187.66, 4189.16}, + {1758523020000LL, 4189.16, 4191.44, 4186.13, 4191.12}, + {1758523080000LL, 4191.11, 4192.99, 4189.41, 4190.4}, + {1758523140000LL, 4190.4, 4192.01, 4183.9, 4185.92}, + {1758523200000LL, 4185.91, 4186, 4179.34, 4181.29}, + {1758523260000LL, 4181.28, 4192.05, 4180.97, 4187.55}, + {1758523320000LL, 4187.56, 4191.04, 4186.43, 4187.56}, + {1758523380000LL, 4187.56, 4192, 4187.56, 4191.49}, + {1758523440000LL, 4191.49, 4192.8, 4185.8, 4191.45}, + {1758523500000LL, 4191.46, 4192.39, 4187.07, 4189.58}, + {1758523560000LL, 4189.58, 4191.39, 4185.82, 4190.65}, + {1758523620000LL, 4190.65, 4192.95, 4189.5, 4192}, + {1758523680000LL, 4192, 4193.48, 4187.57, 4188.57}, + {1758523740000LL, 4188.57, 4191.66, 4186.22, 4186.68}, + {1758523800000LL, 4186.69, 4190.98, 4185.42, 4190.35}, + {1758523860000LL, 4190.34, 4190.35, 4185.4, 4186.67}, + {1758523920000LL, 4186.68, 4186.68, 4182.49, 4182.91}, + {1758523980000LL, 4182.91, 4184.04, 4180, 4180.89}, + {1758524040000LL, 4180.9, 4183.37, 4178.38, 4180.3}, + {1758524100000LL, 4180.3, 4181.62, 4177.09, 4181.4}, + {1758524160000LL, 4181.4, 4182.22, 4178.17, 4181.99}, + {1758524220000LL, 4182, 4183.43, 4180.17, 4183.01}, + {1758524280000LL, 4183, 4190.99, 4183, 4190.16}, + {1758524340000LL, 4190.17, 4190.58, 4187.85, 4190}, + {1758524400000LL, 4190.01, 4193.54, 4186.8, 4188.27}, + {1758524460000LL, 4188.28, 4192.24, 4187.1, 4190.97}, + {1758524520000LL, 4190.98, 4191.91, 4187.8, 4191.57}, + {1758524580000LL, 4191.58, 4214.7, 4190.01, 4209.36}, + {1758524640000LL, 4209.4, 4209.4, 4197.49, 4199.76}, + {1758524700000LL, 4199.76, 4199.76, 4195.03, 4196.19}, + {1758524760000LL, 4196.2, 4198.01, 4191.69, 4196.13}, + {1758524820000LL, 4196.13, 4197.4, 4191.52, 4192.12}, + {1758524880000LL, 4192.12, 4193.72, 4190.45, 4190.85}, + {1758524940000LL, 4190.86, 4192.27, 4183.36, 4190.12}, + {1758525000000LL, 4190.13, 4190.67, 4186, 4188.03}, + {1758525060000LL, 4188.03, 4190.43, 4186, 4190.41}, + {1758525120000LL, 4190.42, 4191.28, 4186.69, 4189.49}, + {1758525180000LL, 4189.49, 4191, 4187.47, 4187.88}, + {1758525240000LL, 4187.89, 4190.94, 4187.88, 4190}, +}; +constexpr FeedBar kPm2Mag_1010[90] = { // signal bar 2025-10-10 21:00 UTC + {1760130000000LL, 3864.76, 3887.98, 3851.36, 3860.76}, + {1760130060000LL, 3860.76, 3899.99, 3858.39, 3892.33}, + {1760130120000LL, 3892.33, 3903.63, 3874.55, 3891.38}, + {1760130180000LL, 3891.38, 3905, 3888.11, 3891.62}, + {1760130240000LL, 3891.61, 3909.99, 3887.3, 3908.32}, + {1760130300000LL, 3908.34, 3911.79, 3896.66, 3902.44}, + {1760130360000LL, 3902.44, 3908.89, 3891.66, 3896.88}, + {1760130420000LL, 3896.9, 3907.18, 3893.9, 3893.9}, + {1760130480000LL, 3893.91, 3897.29, 3884.32, 3889.4}, + {1760130540000LL, 3889.4, 3907, 3888.95, 3895.35}, + {1760130600000LL, 3895.35, 3901.51, 3873.54, 3881.26}, + {1760130660000LL, 3881.25, 3881.25, 3849.28, 3849.6}, + {1760130720000LL, 3849.6, 3861.67, 3760.01, 3777.02}, + {1760130780000LL, 3777.02, 3777.54, 3650, 3742.94}, + {1760130840000LL, 3742.94, 3751.42, 3610, 3723.05}, + {1760130900000LL, 3723.05, 3789.87, 3702.74, 3728.43}, + {1760130960000LL, 3727.34, 3732.99, 3531.17, 3553.67}, + {1760131020000LL, 3553.67, 3714, 3506.47, 3613.14}, + {1760131080000LL, 3613.73, 3638.44, 3515.7, 3528.64}, + {1760131140000LL, 3532.97, 3650, 3428.11, 3476.18}, + {1760131200000LL, 3476.19, 3593.23, 3400, 3500}, + {1760131260000LL, 3500, 3780, 3500, 3706.38}, + {1760131320000LL, 3706.38, 3734.22, 3550, 3700}, + {1760131380000LL, 3700, 3720.65, 3587, 3649.67}, + {1760131440000LL, 3654.33, 3659.57, 3600, 3651.84}, + {1760131500000LL, 3653.03, 3685.77, 3574, 3642.99}, + {1760131560000LL, 3630.36, 3652, 3550, 3560.52}, + {1760131620000LL, 3558.44, 3610.86, 3534.17, 3564.62}, + {1760131680000LL, 3564.62, 3629.99, 3546.08, 3614.45}, + {1760131740000LL, 3613.97, 3655, 3588.37, 3617.44}, + {1760131800000LL, 3617.43, 3680, 3594.44, 3668.55}, + {1760131860000LL, 3665.8, 3690, 3600, 3664.79}, + {1760131920000LL, 3664.79, 3755, 3650.49, 3725.98}, + {1760131980000LL, 3729.32, 3750, 3700, 3747.52}, + {1760132040000LL, 3747.58, 3778.29, 3730, 3739.71}, + {1760132100000LL, 3739.7, 3780, 3733.71, 3769.98}, + {1760132160000LL, 3769.42, 3775.3, 3716.66, 3732.55}, + {1760132220000LL, 3735.48, 3739.13, 3695.25, 3718.57}, + {1760132280000LL, 3718.46, 3736.27, 3690, 3694.24}, + {1760132340000LL, 3694.25, 3739.22, 3683, 3708.34}, + {1760132400000LL, 3708.34, 3724.19, 3707.18, 3719.25}, + {1760132460000LL, 3719.26, 3750, 3717.65, 3749.77}, + {1760132520000LL, 3749.49, 3759, 3715.33, 3742.59}, + {1760132580000LL, 3742.58, 3790, 3700, 3773.77}, + {1760132640000LL, 3777.31, 3884.92, 3745, 3820.15}, + {1760132700000LL, 3822.2, 3889.07, 3806.69, 3855.13}, + {1760132760000LL, 3851, 3955, 3833.74, 3864.91}, + {1760132820000LL, 3863.86, 3867.83, 3796.54, 3804.51}, + {1760132880000LL, 3804.51, 3820.93, 3788.68, 3802.83}, + {1760132940000LL, 3802.83, 3888, 3800.91, 3840}, + {1760133000000LL, 3840, 3869.99, 3835.61, 3856.02}, + {1760133060000LL, 3856.3, 3935, 3843.83, 3914.02}, + {1760133120000LL, 3914.02, 3980.68, 3905.42, 3919.26}, + {1760133180000LL, 3919.14, 3942, 3893.08, 3905.5}, + {1760133240000LL, 3904.8, 3913.57, 3862.05, 3888.56}, + {1760133300000LL, 3888.56, 3910, 3861.24, 3889.04}, + {1760133360000LL, 3889.04, 3960, 3880.01, 3956.52}, + {1760133420000LL, 3957.26, 3965.45, 3923.36, 3936.94}, + {1760133480000LL, 3936.3, 3943.2, 3911, 3932.9}, + {1760133540000LL, 3932.9, 3945.68, 3924.9, 3941.17}, + {1760133600000LL, 3941.17, 3947.66, 3919, 3925.06}, + {1760133660000LL, 3923.76, 3931.53, 3910.15, 3916.9}, + {1760133720000LL, 3916.63, 3921.21, 3870, 3880.46}, + {1760133780000LL, 3880.46, 3896.65, 3866.99, 3873.52}, + {1760133840000LL, 3873.08, 3889.95, 3848.72, 3861.44}, + {1760133900000LL, 3860.89, 3864.63, 3820, 3835.7}, + {1760133960000LL, 3835.7, 3896.64, 3834.98, 3855.28}, + {1760134020000LL, 3853.3, 3881.09, 3845.2, 3868.4}, + {1760134080000LL, 3868.4, 3912.83, 3854.92, 3890.82}, + {1760134140000LL, 3891.43, 3904.01, 3875.56, 3891.63}, + {1760134200000LL, 3891.63, 3897.45, 3872.92, 3886.93}, + {1760134260000LL, 3886.94, 3911.68, 3879.49, 3893.9}, + {1760134320000LL, 3893.75, 3909.86, 3884.8, 3890.06}, + {1760134380000LL, 3889.43, 3898.88, 3879, 3883.01}, + {1760134440000LL, 3882.53, 3898.63, 3875.05, 3895.43}, + {1760134500000LL, 3895, 3907, 3892.45, 3904.67}, + {1760134560000LL, 3904.33, 3913, 3896.42, 3899.61}, + {1760134620000LL, 3899.54, 3911.76, 3881.98, 3895.29}, + {1760134680000LL, 3895.3, 3898.82, 3873.26, 3880.82}, + {1760134740000LL, 3880.94, 3886.27, 3858.55, 3879.92}, + {1760134800000LL, 3880.25, 3901.32, 3875.21, 3883.95}, + {1760134860000LL, 3884.85, 3893.99, 3878.31, 3887.39}, + {1760134920000LL, 3887.39, 3891.3, 3856.7, 3869.02}, + {1760134980000LL, 3869.03, 3871.38, 3832.77, 3841.79}, + {1760135040000LL, 3841.81, 3860.68, 3832, 3851.23}, + {1760135100000LL, 3852.03, 3860, 3837.69, 3852.06}, + {1760135160000LL, 3852, 3866.72, 3846.37, 3852.71}, + {1760135220000LL, 3852.47, 3869.84, 3846.45, 3854.32}, + {1760135280000LL, 3854.33, 3867.53, 3847.42, 3861.87}, + {1760135340000LL, 3861.87, 3876.03, 3854.92, 3866.7}, +}; +constexpr FeedBar kPm2Mag_1121[90] = { // signal bar 2025-11-21 07:15 UTC + {1763709300000LL, 2778.6, 2780.59, 2776.4, 2777.14}, + {1763709360000LL, 2777.15, 2777.15, 2760, 2769.77}, + {1763709420000LL, 2769.77, 2773.2, 2766.51, 2768.19}, + {1763709480000LL, 2768.19, 2774.34, 2766.26, 2773.37}, + {1763709540000LL, 2773.37, 2775.62, 2768.83, 2768.96}, + {1763709600000LL, 2768.96, 2769.74, 2760.8, 2760.8}, + {1763709660000LL, 2760.8, 2766.9, 2756.94, 2757.31}, + {1763709720000LL, 2757.34, 2761.98, 2756.95, 2760.33}, + {1763709780000LL, 2760.34, 2764.6, 2758.27, 2762.86}, + {1763709840000LL, 2762.85, 2764.73, 2758.79, 2762.17}, + {1763709900000LL, 2762.17, 2765.57, 2760.26, 2762.41}, + {1763709960000LL, 2762.4, 2765.03, 2760.99, 2763.57}, + {1763710020000LL, 2763.58, 2765.85, 2762.45, 2765.84}, + {1763710080000LL, 2765.84, 2767.87, 2764.68, 2766.4}, + {1763710140000LL, 2766.4, 2766.9, 2753.33, 2755.45}, + {1763710200000LL, 2755.46, 2760.88, 2743.59, 2753.06}, + {1763710260000LL, 2753.06, 2756.48, 2743, 2749.71}, + {1763710320000LL, 2749.72, 2750.11, 2722, 2731.24}, + {1763710380000LL, 2731.24, 2735.9, 2680.37, 2695}, + {1763710440000LL, 2695, 2699.02, 2642.65, 2681.56}, + {1763710500000LL, 2681.51, 2710, 2679.64, 2704.85}, + {1763710560000LL, 2704.84, 2706.99, 2687.36, 2697.21}, + {1763710620000LL, 2697.2, 2717.49, 2694.64, 2713.55}, + {1763710680000LL, 2713.54, 2717.13, 2701.01, 2713.57}, + {1763710740000LL, 2713.57, 2724.34, 2712.87, 2719.67}, + {1763710800000LL, 2719.67, 2733.66, 2717.27, 2727.06}, + {1763710860000LL, 2727.16, 2736.24, 2726.01, 2734.31}, + {1763710920000LL, 2734.38, 2737.99, 2723.3, 2727.76}, + {1763710980000LL, 2727.76, 2731, 2721.02, 2730.63}, + {1763711040000LL, 2730.63, 2734.72, 2728.18, 2731.13}, + {1763711100000LL, 2731.13, 2733, 2723.22, 2725.56}, + {1763711160000LL, 2725.56, 2726.37, 2718.72, 2721.68}, + {1763711220000LL, 2721.68, 2724.28, 2708.58, 2716.29}, + {1763711280000LL, 2716.29, 2717.9, 2701.42, 2713.24}, + {1763711340000LL, 2713.24, 2713.75, 2706.59, 2711.29}, + {1763711400000LL, 2711.29, 2732, 2710.01, 2727.65}, + {1763711460000LL, 2727.65, 2727.65, 2720.37, 2720.95}, + {1763711520000LL, 2720.96, 2726.46, 2720.52, 2723.61}, + {1763711580000LL, 2723.6, 2723.84, 2713, 2718.17}, + {1763711640000LL, 2718.2, 2719.47, 2710.35, 2712.21}, + {1763711700000LL, 2712.21, 2714.77, 2708, 2714.19}, + {1763711760000LL, 2714.19, 2721.77, 2713.02, 2719.66}, + {1763711820000LL, 2719.66, 2720.66, 2712.12, 2712.54}, + {1763711880000LL, 2712.54, 2718.29, 2711.32, 2717.51}, + {1763711940000LL, 2717.52, 2723.5, 2716.01, 2718.66}, + {1763712000000LL, 2718.67, 2718.95, 2711.56, 2712.9}, + {1763712060000LL, 2712.91, 2718.38, 2711.88, 2717.01}, + {1763712120000LL, 2716.95, 2718.88, 2713.76, 2716.2}, + {1763712180000LL, 2716.21, 2717.98, 2708, 2710.03}, + {1763712240000LL, 2710.03, 2713.51, 2705, 2709.57}, + {1763712300000LL, 2709.56, 2726.08, 2708.47, 2723.95}, + {1763712360000LL, 2723.94, 2737.77, 2723.46, 2730.03}, + {1763712420000LL, 2730.03, 2732, 2725.64, 2731.94}, + {1763712480000LL, 2731.94, 2734, 2728.95, 2731.39}, + {1763712540000LL, 2731.39, 2734.77, 2727.23, 2734.77}, + {1763712600000LL, 2734.52, 2735.43, 2728.87, 2730.52}, + {1763712660000LL, 2730.52, 2744.94, 2730.52, 2739.04}, + {1763712720000LL, 2739.04, 2750, 2738.38, 2745.59}, + {1763712780000LL, 2745.59, 2750.57, 2745.11, 2747.78}, + {1763712840000LL, 2747.78, 2748.29, 2739.42, 2741.67}, + {1763712900000LL, 2741.67, 2750.83, 2738.38, 2750.81}, + {1763712960000LL, 2750.81, 2753.18, 2744, 2745.75}, + {1763713020000LL, 2745.76, 2750.69, 2744.2, 2748.02}, + {1763713080000LL, 2748.02, 2748.02, 2742.75, 2743.74}, + {1763713140000LL, 2743.74, 2746.13, 2739.03, 2739.18}, + {1763713200000LL, 2739.19, 2745.42, 2737, 2744.12}, + {1763713260000LL, 2744.13, 2747.61, 2736.6, 2741.21}, + {1763713320000LL, 2741.21, 2747.36, 2740.33, 2744.41}, + {1763713380000LL, 2744.4, 2751.44, 2743.37, 2750.69}, + {1763713440000LL, 2750.69, 2752.62, 2745, 2745.95}, + {1763713500000LL, 2745.94, 2746.49, 2741.83, 2742.34}, + {1763713560000LL, 2742.34, 2742.43, 2738.28, 2738.59}, + {1763713620000LL, 2738.59, 2744.82, 2738.14, 2740.93}, + {1763713680000LL, 2740.93, 2740.98, 2735.16, 2737.07}, + {1763713740000LL, 2737.06, 2738.13, 2733.46, 2734.82}, + {1763713800000LL, 2734.82, 2741.54, 2733.6, 2740.3}, + {1763713860000LL, 2740.31, 2745.9, 2740.31, 2744.24}, + {1763713920000LL, 2744.24, 2748.02, 2742.5, 2747.07}, + {1763713980000LL, 2747.07, 2747.68, 2737.3, 2738.17}, + {1763714040000LL, 2738.17, 2739.35, 2730, 2731.96}, + {1763714100000LL, 2731.96, 2733.47, 2725.7, 2729.43}, + {1763714160000LL, 2729.42, 2729.42, 2722.07, 2725.09}, + {1763714220000LL, 2725.08, 2729.03, 2723.53, 2727.64}, + {1763714280000LL, 2727.63, 2731.6, 2723.61, 2729.24}, + {1763714340000LL, 2729.24, 2731.82, 2726.76, 2729.9}, + {1763714400000LL, 2729.88, 2730.08, 2723.69, 2723.69}, + {1763714460000LL, 2723.68, 2728.53, 2722.27, 2726.18}, + {1763714520000LL, 2726.18, 2727.46, 2720.46, 2722.09}, + {1763714580000LL, 2722.1, 2727.64, 2721.46, 2725.19}, + {1763714640000LL, 2725.19, 2729.58, 2724.65, 2727.64}, +}; diff --git a/tests/test_adapter_live_state_equivalence_data.hpp b/tests/test_adapter_live_state_equivalence_data.hpp index 48767aaa..5495b4ec 100644 --- a/tests/test_adapter_live_state_equivalence_data.hpp +++ b/tests/test_adapter_live_state_equivalence_data.hpp @@ -28,6 +28,13 @@ // (TradingView's rule: tests/fixtures/margin_entry_bar), so each books that // margin call on the entry bar instead of a bar later; the other 105 values // are unchanged. +// +// R5 lane PAR-MARGIN-2 re-harvested one more the same way, on its tree: chains +// seed 1570937, whose limit long filled on its way down is now called on its +// own bar at the low (46 @106 on bar 25, a bar earlier) -- the kernel's +// post-fill point measures from the fill's own waypoint, TradingView's rule on +// its tapes (tests/fixtures/margin_entry_bar/pm2-m7-lim-*). The other 107 +// values are unchanged. // R5 lane PAR-ORDERS re-harvested it the same way on its tree (29 of 108 digests // move; see the lane's hash commit for why each moves). @@ -137,7 +144,7 @@ constexpr std::uint64_t kTranscriptDigests[] = { 12725802557554099964ull, // chains seed 1256750, 65 trades, 319 commands, 302 rows placed 1798455623603086886ull, // chains seed 1361479, 64 trades, 312 commands, 323 rows placed 9973593188851025006ull, // chains seed 1466208, 73 trades, 382 commands, 338 rows placed - 15204985627708005050ull, // chains seed 1570937, 90 trades, 366 commands, 370 rows placed + 16038855060654239956ull, // chains seed 1570937, 90 trades, 366 commands, 370 rows placed 4161494051921551918ull, // chains seed 1675666, 64 trades, 321 commands, 339 rows placed 11993708214650334332ull, // chains seed 1780395, 82 trades, 480 commands, 436 rows placed 8033768331525115464ull, // chains seed 1885124, 66 trades, 360 commands, 304 rows placed diff --git a/tests/test_adapter_margin_schedule_differential.cpp b/tests/test_adapter_margin_schedule_differential.cpp index cdca01a3..896365e5 100644 --- a/tests/test_adapter_margin_schedule_differential.cpp +++ b/tests/test_adapter_margin_schedule_differential.cpp @@ -56,6 +56,12 @@ * AfterApplied point with TradingView's money. Until R5 lane PAR-MARGIN * the adapter never checked that bar (a recorded divergence H-MEASURE * pinned); it now books TradingView's rows on all four tapes. + * R5 lane PAR-MARGIN-2, each section replaying the lane's `lab tv` tapes from + * the fixture's own strategy.pine: + * M7 shapes: limit openings, long and short, leveraged and 1x -- 12 tapes, + * all TradingView's rows (the kernel's post-fill point now measures from + * the fill's own waypoint: a limit filled on its way down faces the + * bar's low, a short filled on its way up its high); * * Source-bound (includes pineforge/source): release profile only. */ @@ -93,6 +99,7 @@ struct FeedBar { #include "fixtures/margin_entry_bar/bars.inc" #include "fixtures/margin_entry_bar/m10_bars.inc" #include "fixtures/intrabar_margin/bars_1m.inc" +#include "fixtures/margin_entry_bar/pm2_bars.inc" // ------------------------------------------------------------ TV policy // The twin's TradingView money and slice (test_native_margin_hooks_twin.cpp, @@ -118,6 +125,7 @@ struct Config { double qty_step = 0.0; double mintick = 0.01; bool pooc = false; + bool coof = false; CommissionType commission = CommissionType::PERCENT; double commission_value = 0.0; int pyramiding = 1; @@ -208,6 +216,7 @@ struct PineSide final : source::PineStrategyHost { std::function script; int calls = 0; double last_close = 0.0; + std::int64_t last_time = 0; explicit PineSide(const Config& c) { auto& tv = fixture_configuration(); tv.initial_capital = c.capital; @@ -218,6 +227,7 @@ struct PineSide final : source::PineStrategyHost { tv.margin_long = c.margin_long; tv.margin_short = c.margin_short; tv.process_orders_on_close = c.pooc; + tv.calc_on_order_fills = c.coof; tv.pyramiding = c.pyramiding; initial_capital_ = c.capital; qty_step_ = c.qty_step; @@ -228,12 +238,19 @@ struct PineSide final : source::PineStrategyHost { void on_source_bar(const Bar& bar) override { ++calls; last_close = bar.close; + last_time = bar.timestamp; if (script) script(*this, pine_bar_index()); } void entry(const char* id, bool is_long, double limit, double stop, double qty) { strategy_entry(id, is_long, limit, stop, qty); } void close(const char* id) { strategy_close(id); } + void close_all() { strategy_close_all(); } + std::string entry_id0() const { return open_trade_entry_id(0); } + // strategy.equity at the script's calculation: realized plus open profit. + double strategy_equity() const { + return initial_capital_ + net_profit_sum_ + open_profit(last_close); + } int entry_bar_index() const { return open_trade_entry_bar_index(0); } double position() const { return physical_position().signed_units; } double equity() const { return initial_capital_ + net_profit_sum_; } @@ -412,13 +429,15 @@ void m7_leveraged_opening_entry_bar() { // B. A carried 10 @100 long (50 %, capital 1050) adds 10 @95 on a buy limit // filled on the H->L leg of bar 2 (O100 H101 L80 C81; high-first). After the -// add the rest of the path is the close 81: 720 against 810. -// kernel: AfterApplied at the add's point books 4 x 90 / 40.5 at 81; -// adapter: the point is refused (no scheduling after a leveraged add), and -// bar 3 (O100) is solvent -- no margin row at all. -// (ab9714be books a third answer, 10 @80: the post-add book at the bar's own -// low. The kernel's AfterApplied mark after a mid-segment fill starts at the -// segment's destination waypoint, so the rest of the H->L leg is not seen.) +// add the rest of the path runs down to the low 80: 700 against 800. +// kernel: AfterApplied at the add's point books 4 x 100 / 40 = 10 at 80 -- +// ab9714be's answer too (the post-add book at the bar's own low). +// Until R5 lane PAR-MARGIN-2 the kernel's post-fill mark scanned only +// the waypoints after the fill's own -- the fill is presented at the +// low it was falling toward -- so it booked 4 x 90 / 40.5 = 8.888.. +// at the close 81; +// adapter: the point is refused (no scheduling after an add to a carried +// book), and bar 3 (O100) is solvent -- no margin row at all. void m7_mid_bar_add() { std::printf("-- M7-B mid-bar pyramid add: the add's AfterApplied point\n"); Config c; @@ -447,8 +466,8 @@ void m7_mid_bar_add() { CHECK(adapter.empty()); CHECK(pine.position() == 20.0); REQUIRE(native.size() == 1); - check_fill(native[0], 2, NativePathPhase::Low, 8.8888888888888893, 81.0, true); - CHECK(same_value(kernel.position(), 11.111111111111111)); + check_fill(native[0], 2, NativePathPhase::Low, 10.0, 80.0, true); + CHECK(same_value(kernel.position(), 10.0)); } // C. A carried POOC short 9.5 @100 (100 %, 0.1 % fee) whose bar 1 reaches @@ -1225,6 +1244,319 @@ void intrabar_margin_tapes() { } } +// ================================================================= PAR-MARGIN-2 +// R5 lane PAR-MARGIN-2 measured the margin shapes lane PAR-MARGIN left open, +// each against TradingView: 57 `lab tv` exports (ws-report-v1, rangeProof +// covered), every one the lane's own synthetic script. They are replayed here +// FROM THE SCRIPT TRADINGVIEW RAN: each fixture's strategy.pine is parsed for +// its signal times, sizes, levels and strategy() flags, so a parameter cannot +// drift between the tape and its replay. The fixture READMEs name every tape. +struct ProbeEntry { + std::int64_t at = 0; // the signal bar's open, UTC ms + std::string id; + bool is_long = true; + bool add = false; // placed on an open position (else: flat) + double K = kNaN; // qty = floor(K * strategy.equity / close * 1000) / 1000 + double limit_mult = kNaN; // limit = math.round(close * mult, 2) + double stop_abs = kNaN; + double limit_abs = kNaN; + double stop_offset = kNaN; // stop = close + offset +}; +struct ProbeScript { + std::vector entries; + double capital = 1000.0; + double margin = 100.0; + double qty_value = 1.0; + int pyramiding = 1; + bool pooc = false; + bool coof = false; + bool magnifier = false; + int timeout = 3; + bool close_all = false; + bool per_id_close = false; // close the open trade entry_id(0) names +}; + +std::string read_text(const std::string& path) { + std::ifstream in(path); + std::stringstream buffer; + buffer << in.rdbuf(); + return buffer.str(); +} + +double number_after(const std::string& text, const std::string& key, double fallback) { + const auto at = text.find(key); + return at == std::string::npos ? fallback : std::atof(text.c_str() + at + key.size()); +} + +// UTC wall time -> ms (days from civil). +std::int64_t utc_ms(int y, int mo, int d, int h, int mi) { + y -= mo <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const int yoe = y - era * 400; + const int doy = (153 * (mo + (mo > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + const std::int64_t days = static_cast(era) * 146097 + doe - 719468; + return ((days * 24 + h) * 60 + mi) * 60000; +} + +ProbeScript parse_probe(const std::string& path) { + const std::string text = read_text(path); + ProbeScript ps; + ps.capital = number_after(text, "initial_capital=", 1000.0); + ps.qty_value = number_after(text, "default_qty_value=", 1.0); + ps.pyramiding = static_cast(number_after(text, "pyramiding=", 1.0)); + ps.margin = number_after(text, "margin_long=", 100.0); + ps.pooc = text.find("process_orders_on_close=true") != std::string::npos; + ps.coof = text.find("calc_on_order_fills=true") != std::string::npos; + ps.magnifier = text.find("use_bar_magnifier=true") != std::string::npos; + ps.timeout = static_cast(number_after(text, "strategy.opentrades.entry_bar_index(0) >= ", 3.0)); + ps.close_all = text.find("strategy.close_all(") != std::string::npos; + ps.per_id_close = text.find("strategy.opentrades.entry_id(0) ==") != std::string::npos; + std::stringstream lines(text); + std::string line; + std::int64_t at = 0; + bool add = false; + while (std::getline(lines, line)) { + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + const auto ts = line.find("timestamp(\"UTC\", "); + if (line.rfind("if time == ", 0) == 0 && ts != std::string::npos + && std::sscanf(line.c_str() + ts, "timestamp(\"UTC\", %d, %d, %d, %d, %d)", &y, &mo, + &d, &h, &mi) == 5) { + at = utc_ms(y, mo, d, h, mi); + add = line.find("strategy.position_size > 0") != std::string::npos; + continue; + } + const auto call = line.find("strategy.entry(\""); + if (call == std::string::npos || at == 0) continue; + ProbeEntry e; + e.at = at; + e.add = add; + const auto id_end = line.find('"', call + 16); + e.id = line.substr(call + 16, id_end - call - 16); + e.is_long = line.find("strategy.long") != std::string::npos; + e.K = number_after(line, "math.floor(", kNaN); + e.limit_mult = number_after(line, "limit = math.round(close * ", kNaN); + if (line.find("stop = close ") != std::string::npos) { + e.stop_offset = (line.find("stop = close - ") != std::string::npos ? -1.0 : 1.0) + * number_after(line, line.find("stop = close - ") != std::string::npos + ? "stop = close - " : "stop = close + ", kNaN); + } else { + e.stop_abs = number_after(line, "stop = ", kNaN); + } + if (!std::isfinite(e.limit_mult)) e.limit_abs = number_after(line, "limit = ", kNaN); + ps.entries.push_back(e); + at = 0; + } + return ps; +} + +// One exit as both sides report it: the chart bar it books on, price, units. +struct ProbeExit { + std::int64_t bar = 0; + double price = 0.0; + double qty = 0.0; + bool margin_call = false; + double entry = kNaN; // the trade's entry price +}; + +bool exit_order(const ProbeExit& a, const ProbeExit& b) { + return a.bar != b.bar ? a.bar < b.bar : a.qty < b.qty; +} + +std::vector csv_cells(const std::string& line) { + std::vector cells(1); + bool quoted = false; + for (char ch : line) { + if (ch == '"') quoted = !quoted; + else if (ch == ',' && !quoted) cells.emplace_back(); + else if (ch != '\r') cells.back() += ch; + } + return cells; +} + +std::vector> tape_rows(const std::string& dir) { + std::ifstream in(dir + "/tv_trades.csv"); + std::vector> out; + std::string line; + std::getline(in, line); + while (std::getline(in, line)) out.push_back(csv_cells(line)); + return out; +} + +std::vector tape_exit_rows(const std::string& dir) { + std::vector out; + std::map entries; + const auto rows = tape_rows(dir); + for (const auto& cell : rows) + if (cell.size() >= 6 && cell[1].rfind("Entry", 0) == 0) entries[cell[0]] = std::stod(cell[4]); + for (const auto& cell : rows) { + if (cell.size() < 6 || cell[1].rfind("Exit", 0) != 0) continue; + const auto entry = entries.find(cell[0]); + out.push_back({tape_ms(cell[2]), std::stod(cell[4]), std::stod(cell[5]), + cell[3] == "Margin call", entry == entries.end() ? kNaN : entry->second}); + } + std::sort(out.begin(), out.end(), exit_order); + return out; +} + +std::vector feed_bars(const FeedBar* rows, int n) { + std::vector out; + for (int i = 0; i < n; ++i) + out.push_back({rows[i].open, rows[i].high, rows[i].low, rows[i].close, 1.0, rows[i].ts}); + return out; +} + +std::vector fifteen_of(const std::vector& one) { + std::vector out; + for (const Bar& b : one) { + const std::int64_t open = b.timestamp - b.timestamp % (15 * 60000LL); + if (out.empty() || out.back().timestamp != open) { + out.push_back({b.open, b.high, b.low, b.close, 1.0, open}); + } else { + out.back().high = std::max(out.back().high, b.high); + out.back().low = std::min(out.back().low, b.low); + out.back().close = b.close; + } + } + return out; +} + +// The adapter side of one tape: the parsed script through the Pine adapter, +// magnified on the one-minute feed when the script asks for the magnifier. +std::vector run_probe(const ProbeScript& ps, const std::vector& input, + bool one_minute_input, double qty_step, double mintick) { + Config c; + c.capital = ps.capital; + c.margin_long = ps.margin; + c.margin_short = ps.margin; + c.pooc = ps.pooc; + c.coof = ps.coof; + c.pyramiding = ps.pyramiding; + c.qty_step = qty_step; + c.mintick = mintick; + c.qty_value = ps.qty_value; + PineSide pine(c); + pine.script = [&ps](PineSide& h, int bar) { + const double equity = h.strategy_equity(); + const double held = h.position(); + for (const ProbeEntry& e : ps.entries) { + if (h.last_time != e.at || (e.add ? held == 0.0 : held != 0.0)) continue; + const double qty = std::isfinite(e.K) + ? std::floor(e.K * equity / h.last_close * 1000.0) / 1000.0 : kNaN; + const double limit = std::isfinite(e.limit_mult) + ? std::round(h.last_close * e.limit_mult * 100.0) / 100.0 : e.limit_abs; + const double stop = std::isfinite(e.stop_offset) ? h.last_close + e.stop_offset + : e.stop_abs; + h.entry(e.id.c_str(), e.is_long, limit, stop, qty); + } + if (h.position() != 0.0 && bar - h.entry_bar_index() >= ps.timeout) { + if (ps.close_all) h.close_all(); + else h.close(ps.per_id_close ? h.entry_id0().c_str() : ps.entries.front().id.c_str()); + } + }; + if (ps.magnifier) { + pine.run(input.data(), static_cast(input.size()), "1", "15", true); + } else { + const auto bars = one_minute_input ? fifteen_of(input) : input; + pine.run(bars.data(), static_cast(bars.size()), "15", "15", false); + } + CHECK(pine.last_error().empty()); + std::vector out; + for (int i = 0; i < pine.trade_count(); ++i) { + const Trade& t = pine.get_trade(i); + out.push_back({t.exit_time - t.exit_time % (15 * 60000LL), t.exit_price, t.qty, + t.exit_comment == "Margin call", t.entry_price}); + } + std::sort(out.begin(), out.end(), exit_order); + return out; +} + +bool same_exits(const std::vector& a, const std::vector& b, + bool margin_calls_only = false) { + std::vector x, y; + for (const auto& r : a) if (!margin_calls_only || r.margin_call) x.push_back(r); + for (const auto& r : b) if (!margin_calls_only || r.margin_call) y.push_back(r); + if (x.size() != y.size()) return false; + for (std::size_t i = 0; i < x.size(); ++i) { + // TradingView prints four quantity decimals and the symbol's price. + if (x[i].bar != y[i].bar || std::abs(x[i].price - y[i].price) > 5e-3 + || std::abs(x[i].qty - y[i].qty) > 5e-5 || x[i].margin_call != y[i].margin_call + || std::abs(x[i].entry - y[i].entry) > 5e-3) { + return false; + } + } + return true; +} + +std::string utc_text(std::int64_t ms) { + std::int64_t z = ms / 86400000 + 719468; + const int minutes = static_cast(ms % 86400000 / 60000); + const std::int64_t era = (z >= 0 ? z : z - 146096) / 146097; + const unsigned doe = static_cast(z - era * 146097); + const unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + const unsigned mp = (5 * doy + 2) / 153; + const unsigned d = doy - (153 * mp + 2) / 5 + 1; + const unsigned m = mp < 10 ? mp + 3 : mp - 9; + const long long y = static_cast(yoe) + era * 400 + (m <= 2); + char text[32]; + std::snprintf(text, sizeof text, "%04lld-%02u-%02u %02d:%02d", y, m, d, minutes / 60, + minutes % 60); + return text; +} + +void print_exits(const char* side, const std::vector& rows) { + std::printf(" %-8s", side); + for (const auto& r : rows) { + std::printf(" | %s %.17g @ %.4f%s (in %.4f)", utc_text(r.bar).c_str(), r.qty, r.price, + r.margin_call ? " MC" : "", r.entry); + } + std::printf("\n"); +} + +// ---- the M7 shapes (tests/fixtures/margin_entry_bar/pm2-*) +// A leveraged opening, on the bar it fills. TradingView books the +// margin call on that bar over the rest of its path, sized on the book the +// fill left: +// pm2-m7-lim-* a limit opening filled on its way down: the call at that +// bar's LOW, 4 of 4 (the kernel's post-fill point scanned the +// waypoints after the fill's own, so it missed the low -- the +// call came a bar late, or not at all); +// pm2-m7-slim-*, pm2-m7-s1lim-* a SHORT limit opening filled on its way up, +// leveraged (margin 5) and at full margin (1x): the call at +// that bar's HIGH, 8 of 8 (none was booked before); +struct ShapeTape { + const char* slug; + const FeedBar* bars; + int n; +}; + +void m7_shapes_on_tapes() { + const std::string dir = PINEFORGE_HM_M7A_FIXTURE_DIR; + const ShapeTape tapes[] = { + {"pm2-m7-lim-0402-1945", kPm2Chart_0402_1945, 6}, {"pm2-m7-lim-0402-2000", kPm2Chart_0402_2000, 6}, + {"pm2-m7-lim-0801-0030", kPm2Chart_0801_0030, 6}, {"pm2-m7-lim-1201-0215", kPm2Chart_1201_0215, 6}, + {"pm2-m7-slim-0402-1330", kPm2Chart_0402_1330, 6}, {"pm2-m7-slim-0509-1100", kPm2Chart_0509_1100, 6}, + {"pm2-m7-slim-0709-1930", kPm2Chart_0709_1930, 6}, {"pm2-m7-slim-1001-0830", kPm2Chart_1001_0830, 6}, + {"pm2-m7-s1lim-0402-1330", kPm2Chart_0402_1330, 6}, {"pm2-m7-s1lim-0509-1100", kPm2Chart_0509_1100, 6}, + {"pm2-m7-s1lim-0709-1930", kPm2Chart_0709_1930, 6}, {"pm2-m7-s1lim-1001-0830", kPm2Chart_1001_0830, 6}, + }; + for (const ShapeTape& tape : tapes) { + std::printf("-- M7 shape %s\n", tape.slug); + const std::string at = dir + "/" + tape.slug; + const ProbeScript ps = parse_probe(at + "/strategy.pine"); + REQUIRE(!ps.entries.empty()); + // TradingView floors the margin call's minimum to 0.0001 of a contract + // on this symbol (M7 above). + const auto adapter = run_probe(ps, feed_bars(tape.bars, tape.n), false, 0.0001, 0.01); + const auto tv = tape_exit_rows(at); + print_exits("tape", tv); + print_exits("adapter", adapter); + CHECK(std::any_of(tv.begin(), tv.end(), [](const ProbeExit& r) { return r.margin_call; })); + CHECK(same_exits(adapter, tv)); + } +} + } // namespace int main() { @@ -1243,6 +1575,7 @@ int main() { test("M7 TradingView tapes", m7_tradingview_tapes); test("M10 TradingView tapes", m10_tradingview_tapes); test("intrabar margin tapes", intrabar_margin_tapes); + test("PAR-MARGIN-2 M7 shapes", m7_shapes_on_tapes); std::printf("%d checks, %d failures\n", checks, failures); return failures == 0 ? 0 : 1; } diff --git a/tests/test_native_margin_post_fill_path.cpp b/tests/test_native_margin_post_fill_path.cpp new file mode 100644 index 00000000..85018021 --- /dev/null +++ b/tests/test_native_margin_post_fill_path.cpp @@ -0,0 +1,222 @@ +// R5 lane PAR-MARGIN-2: the path a post-fill margin check measures. +// +// A resting request is matched at the driver point its segment reaches: a buy +// limit at 95 on a bar that runs O100 H101 L80 C81 is filled on the segment +// from the high to the low, and that fill is presented at the low's point. The +// book the fill leaves still faces the rest of that segment -- down to the low +// -- before the close. The kernel's AfterApplied point measured only the +// waypoints AFTER the point's phase, so it skipped the low and measured the +// close (the same held for a short filled on its way up to the high). It now +// measures from the segment's origin (NativeExecutionConsumer:: +// margin_segment_origin), with the fill price as the point's own mark: the +// low is in the scan, the waypoints the path already passed are not. +// TradingView books the call at that low on the limit's own bar (lab tv tapes +// tests/fixtures/margin_entry_bar/pm2-m7-lim-*, pm2-m7-poocl-*, +// pm2-m7b-lim-*; the adapter half is test_adapter_margin_schedule_differential), +// as ab9714be's entry-bar suffix did. +// +// One margin model (maintenance only, the mark check, the kernel's own sizing), +// every check point admitted: +// 1. a long limit filled on the H->L leg of a high-first bar: the post-fill +// point measures 80 (it measured 81, the close) and the call rests and +// books at 80; +// 2. a long limit filled on the O->L leg of a low-first bar: 80 breaches +// where the high and the close after it do not -- the call is booked on +// the bar (it was booked nowhere); +// 3. a short limit filled on the L->H leg of a low-first bar: 120 (it +// measured the fill price, 105, and booked nothing); +// 4. a market fill at the open is unchanged: its remaining path already +// started at the open. +// +// Source-free: runs in the kernel-only profile too. +#include + +#include +#include +#include + +using namespace pineforge; +namespace no = pineforge::native_order; + +namespace { + +int checks = 0; +int failures = 0; +#define CHECK(expr) do { \ + ++checks; \ + if (!(expr)) { ++failures; std::printf("FAIL %d %s\n", __LINE__, #expr); } \ +} while (0) + +struct Point { + NativeMarginCheckKind kind; + int bar; + double mark; +}; + +struct Probe final : NativeStrategyHost { + no::Request order; + int bars = 0; + mutable std::vector points; + std::vector calls; + void on_native_bar(const Bar&, const NativeDecisionContext&) override { + if (bars++ == 0) (void)submit(order); + } + bool margin_check_allowed(const NativeMarginCheckPoint& p) const override { + points.push_back({p.kind, p.cursor.point.interval_index, p.mark}); + return true; + } + void on_native_margin_call(const no::MarginCallEvent& e) override { calls.push_back(e); } + // The AfterApplied point of the bar the order filled on. + const Point* after_applied(int bar) const { + for (const auto& p : points) { + if (p.kind == NativeMarginCheckKind::AfterApplied && p.bar == bar) return &p; + } + return nullptr; + } +}; + +const int64_t T0 = 1791190800000LL; // Mon 5 Oct 2026 09:00 UTC +const int64_t M15 = 15 * 60000LL; + +NativeRunSpec spec() { + NativeRunSpec s; + s.identity = {"margin-post-fill-path", 1}; + s.input_tf = "15"; + s.script_tf = "15"; + s.ticker = "P"; + s.tickerid = "PROBE:P"; + s.type = "cfd"; + s.currency = "USD"; + s.timezone = "UTC"; + s.session = "24x7"; + s.initial_capital = 1000.0; + s.point_value = 1.0; + s.account_fx = 1.0; + s.price_tick = 0.01; + s.event_retention = NativeEventRetention::Full; + NativeMarginModel m; + m.initial_long = 0.0; // maintenance only: the host admits + m.initial_short = 0.0; + m.maintenance_long = 0.5; + m.maintenance_short = 0.5; + m.sizing = NativeLiquidationSizing::RestoreMinimum; + m.check = NativeLiquidationCheck::PathAdverseExtremeMark; + s.margin = m; + return s; +} + +no::Request limit(double units, double level) { + no::Request r; + r.intent = no::Transact{units}; + r.label = "entry"; + r.trigger = no::Limit{level}; + return r; +} + +void run(Probe& h, const std::vector& bars) { + const auto setup = h.configure_native(spec()); + CHECK(setup.status == NativeSetupStatus::Applied); + if (setup.status != NativeSetupStatus::Applied) return; + h.run(bars.data(), static_cast(bars.size())); + CHECK(h.last_error().empty()); +} + +void print(const char* label, const Probe& h, int bar) { + const Point* p = h.after_applied(bar); + std::printf("%-44s AfterApplied mark %.6g calls %zu", label, p ? p->mark : -1.0, + h.calls.size()); + for (const auto& c : h.calls) + std::printf(" [bar %d units %.6g at %.4f]", c.cursor.point.interval_index, c.units, c.mark); + std::printf(" position %.6g\n", h.physical_position().signed_units); +} + +std::vector tape(std::initializer_list> rows) { + std::vector out; + int64_t t = T0; + for (const auto& r : rows) { + out.push_back({r[0], r[1], r[2], r[3], 10, t}); + t += M15; + } + return out; +} + +// 1. High-first bar: O100 H101 L80 C81, a buy limit of 20 at 95 filled on the +// way down. At 80 the book is 700 against 800; at the close, 720 against 810. +void long_limit_high_first() { + Probe h; + h.order = limit(20.0, 95.0); + run(h, tape({{100, 100.5, 99.5, 100}, {100, 101, 80, 81}, {100, 100.5, 99.5, 100}})); + print("long limit on the H->L leg (high first)", h, 1); + const Point* p = h.after_applied(1); + CHECK(p != nullptr); + if (p) CHECK(p->mark == 80.0); + CHECK(!h.calls.empty()); + if (!h.calls.empty()) { + CHECK(h.calls[0].cursor.point.interval_index == 1); + CHECK(h.calls[0].mark == 80.0); + } +} + +// 2. Low-first bar: O100 L80 H121 C100 (|O-L| 20 < |H-O| 21), the same limit +// filled on the way down to the low. At 80: 700 against 800. The high and the +// close after it are solvent (at 95 the book is 1000 against 950). +void long_limit_low_first() { + Probe h; + h.order = limit(20.0, 95.0); + run(h, tape({{100, 100.5, 99.5, 100}, {100, 121, 80, 100}, {100, 100.5, 99.5, 100}})); + print("long limit on the O->L leg (low first)", h, 1); + const Point* p = h.after_applied(1); + CHECK(p != nullptr); + if (p) CHECK(p->mark == 80.0); + CHECK(!h.calls.empty()); + if (!h.calls.empty()) { + CHECK(h.calls[0].cursor.point.interval_index == 1); + CHECK(h.calls[0].mark == 80.0); + } +} + +// 3. A sell limit of 15 at 105 on a low-first bar O100 L99 H120 C101: filled +// on the way up to the high. At 120 the short's book is 775 against 900; at +// the fill, 1000 against 787.5. +void short_limit_rising_leg() { + Probe h; + h.order = limit(-15.0, 105.0); + run(h, tape({{100, 100.5, 99.5, 100}, {100, 120, 99, 101}, {100, 100.5, 99.5, 100}})); + print("short limit on the L->H leg (low first)", h, 1); + const Point* p = h.after_applied(1); + CHECK(p != nullptr); + if (p) CHECK(p->mark == 120.0); + CHECK(!h.calls.empty()); + if (!h.calls.empty()) { + CHECK(h.calls[0].cursor.point.interval_index == 1); + CHECK(h.calls[0].mark == 120.0); + } +} + +// 4. A market buy of 20 at bar 1's open (O100 H101 L80 C81): the point is the +// open's own, whose remaining path already started there -- the low, as before. +void market_at_open_unchanged() { + Probe h; + no::Request r; + r.intent = no::Transact{20.0}; + r.label = "entry"; + h.order = r; + run(h, tape({{100, 100.5, 99.5, 100}, {100, 101, 80, 81}, {100, 100.5, 99.5, 100}})); + print("market at the open (control)", h, 1); + const Point* p = h.after_applied(1); + CHECK(p != nullptr); + if (p) CHECK(p->mark == 80.0); + CHECK(!h.calls.empty()); + if (!h.calls.empty()) CHECK(h.calls[0].mark == 80.0); +} + +} // namespace + +int main() { + long_limit_high_first(); + long_limit_low_first(); + short_limit_rising_leg(); + market_at_open_unchanged(); + std::printf("test_native_margin_post_fill_path: %d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} diff --git a/tests/test_publication_witness.cpp b/tests/test_publication_witness.cpp index 72aaada5..bbe508ce 100644 --- a/tests/test_publication_witness.cpp +++ b/tests/test_publication_witness.cpp @@ -73,6 +73,18 @@ // in the close pass (4 runs). With the three hash inputs taken out, this TU // prints INT25's table byte for byte but for Storm11 and Storm11M, whose // trades the cap moves. +// Re-harvested once more for R5 lane PAR-MARGIN-2 (its v19 re-pin): one run, +// Config03, opens a short with a sell limit filled at 111.25 on its way up a +// low-first bar (O110.5 L109.5 H112.5 C112.25), and the kernel's post-fill +// margin point now measures that bar's high -- the waypoint the fill was +// reached on the way to -- where it measured the waypoints after it. The run +// books its margin call on the fill bar at the high, 4 @112.5, where it booked +// 2 @112.25 there and 6 @114.75 a bar later, so it closes 48 trades instead of +// 50 and its digest moves (461a919ebdcd41e9 -> 4593ef08ab33e045 on the lane's +// tree; INT26 moves it on the integrated tree in its v19 hash re-pin); TradingView +// books a short limit's call at its fill bar's high +// (tests/fixtures/margin_entry_bar/pm2-m7-slim-*, pm2-m7-s1lim-*). The other 155 +// runs keep their digest and every count. #include #include @@ -766,8 +778,9 @@ void harvest() { "// row per run of its battery (name, digest, source folds, closed trades,\n" "// recorded broker-state hash rows).\n" "// Harvested on 6c081f5d, re-harvested once for V19-D's v4 fold (INT23),\n" - "// once for K-ULP3's whole-lot close and once for V19-FIX's hash step: see\n" - "// the provenance note in the test.\n" + "// once for K-ULP3's whole-lot close, once for V19-FIX's hash step and once\n" + "// for PAR-MARGIN-2's post-fill margin path: see the provenance note in the\n" + "// test.\n" "// Generated -- never edit a row by hand.\n"); std::printf("constexpr Pinned kPinned[] = {\n"); for (const Scenario& s : battery()) { diff --git a/tests/test_publication_witness_pinned.inc b/tests/test_publication_witness_pinned.inc index a68b95be..7c414967 100644 --- a/tests/test_publication_witness_pinned.inc +++ b/tests/test_publication_witness_pinned.inc @@ -2,8 +2,9 @@ // row per run of its battery (name, digest, source folds, closed trades, // recorded broker-state hash rows). // Harvested on 6c081f5d, re-harvested once for V19-D's v4 fold (INT23), -// once for K-ULP3's whole-lot close and once for V19-FIX's hash step: see -// the provenance note in the test. +// once for K-ULP3's whole-lot close, once for V19-FIX's hash step and once +// for PAR-MARGIN-2's post-fill margin path: see the provenance note in the +// test. // Generated -- never edit a row by hand. // R5 lane PAR-ORDERS re-harvested it the same way on its tree (21 of 156 runs move; // see the lane's hash commit for why each moves): @@ -37,7 +38,7 @@ constexpr Pinned kPinned[] = { {"ConfigFlags102", 0x91619a8b469ea6aeull, 144, 52, 144}, {"Config02M", 0xd04764a2d7139486ull, 48, 11, 48}, {"ConfigFlags102M", 0x854413a4a59d0d33ull, 144, 52, 144}, - {"Config03", 0x461a919ebdcd41e9ull, 144, 50, 144}, + {"Config03", 0x461a919ebdcd41e9ull, 144, 48, 144}, {"ConfigFlags103", 0xcf60de51b3acfcc3ull, 184, 65, 144}, {"Config03M", 0x4511d5cd864e706full, 144, 48, 144}, {"ConfigFlags103M", 0xdd7937eb65073a91ull, 184, 66, 144}, From 4d25e69ee8ba246fec612511d231d2ed100d7884 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:33:42 +0800 Subject: [PATCH 43/83] Adapter: a leveraged fill is margin-checked on its own bar under POOC and COOF and for an add to a carried book, TradingView's book (R5 lane PAR-MARGIN-2, M7 shapes) PAR-MARGIN admitted the kernel's post-fill point on a leveraged opening's entry bar only for the run shapes ab9714be covered and only when the position opened on that bar. Twenty-four more lab tv exports (tests/fixtures/margin_entry_bar/ pm2-*, BINANCE:ETHUSDT.P 15m, ws-report-v1, rangeProof covered; the lane's own synthetic scripts) measure the shapes it left open: - pm2-m7-coof-* (M7-A's events under calc_on_order_fills): the call on the entry bar at its low, 4 of 4 -- the M7-A rows; - pm2-m7-poocl-* (the limit openings under process_orders_on_close): the call on the fill bar at its LOW, 4 of 4; - pm2-m7b-lim-* (a limit ADD to a carried book): the call on the add's bar at its low on the combined book, first-in lot first, 4 of 4; - pm2-m7b-mkt-*, pm2-m7-poocm-*, pm2-m7-mag-* (market adds, POOC market openings, magnified openings): 12 of 12, already booked before the lane. leveraged_entry_bar_checked now admits process_orders_on_close and calc_on_order_fills runs (a magnified or timestamped-FX run keeps its route), and on_applied admits the point after every opening-side fill of its bar, an add to a book carried in from an earlier bar included. The adapter books all 24 tapes' rows (before: 12 of 24; the 12 others booked the call a bar late or not at all); the differential's M7-B pin, adapter side: no margin row -> the kernel's and ab9714be's 10 @80. Test data (not hash values), harvested with -DPINEFORGE_V19E_HARVEST: tests/test_adapter_live_state_equivalence_data.hpp, four digests whose margin calls move onto their fill's bar or are re-sized there reversals seed 314187 (COOF; trades unchanged, one more request placed) 9660742953292018077 -> 17536362036531980071 reversals seed 1570935 (adds) 11252195188350860763 -> 17000758005854428354 reversals seed 2199309 (POOC) 17071186865396902026 -> 10570937877030436735 chains seed 2199311 (POOC) 2330688028628663175 -> 8124177192198952658 the other 104 unchanged. tests/test_adapter_margin_schedule_differential.cpp ("M7 shapes"): all 36 tapes. Docs: native-engine.md, ADR-0001 (margin bullet, rule-2 row), design MG15, the fixture README; anchors carried by an exact line map. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit fd5e6f9a5a78962b7c23f1066ae9a70b3b536b7d) INT26 pick note: the live-state transcripts are harvested on this tree with the TU's own switch: exactly the lane's four configurations move (reversals seeds 314187, 1570935 and 2199309, chains seed 2199311) and no other pinned behaviour does. Three of them take this tree's own value, since PAR-ORDERS-2 had already moved them (reversals 314187 4920319354091947109 -> 9508064597864072295, one more row placed; reversals 2199309 12203651668804466516 -> 9639971099840596709, counts unchanged; chains 2199311 11831052386180063129 -> 13103985213974940416, 54 trades, 326 commands, 285 rows placed); reversals 1570935 takes the lane's own value. The ADR row this commit rewrites keeps its place after PAR-MARGIN's IntrabarSample row; the docs merged on anchor digits otherwise. --- docs/adr/0001-kernel-adapter-boundary.md | 37 +++---- docs/design/native-feature-parity.md | 90 ++++++++--------- docs/pages/native-engine.md | 16 +-- src/source/pine_adapter.cpp | 34 ++++--- tests/fixtures/margin_entry_bar/README.md | 97 ++++++++++++++++++- .../pm2-m7-coof-0621/meta.json | 16 +++ .../pm2-m7-coof-0621/metrics.json | 40 ++++++++ .../pm2-m7-coof-0621/strategy.pine | 12 +++ .../pm2-m7-coof-0621/tv_trades.csv | 3 + .../pm2-m7-coof-0922/meta.json | 16 +++ .../pm2-m7-coof-0922/metrics.json | 40 ++++++++ .../pm2-m7-coof-0922/strategy.pine | 12 +++ .../pm2-m7-coof-0922/tv_trades.csv | 3 + .../pm2-m7-coof-1010/meta.json | 16 +++ .../pm2-m7-coof-1010/metrics.json | 40 ++++++++ .../pm2-m7-coof-1010/strategy.pine | 12 +++ .../pm2-m7-coof-1010/tv_trades.csv | 5 + .../pm2-m7-coof-1121/meta.json | 16 +++ .../pm2-m7-coof-1121/metrics.json | 40 ++++++++ .../pm2-m7-coof-1121/strategy.pine | 12 +++ .../pm2-m7-coof-1121/tv_trades.csv | 3 + .../pm2-m7-mag-0621/meta.json | 16 +++ .../pm2-m7-mag-0621/metrics.json | 40 ++++++++ .../pm2-m7-mag-0621/strategy.pine | 12 +++ .../pm2-m7-mag-0621/tv_trades.csv | 3 + .../pm2-m7-mag-0922/meta.json | 16 +++ .../pm2-m7-mag-0922/metrics.json | 40 ++++++++ .../pm2-m7-mag-0922/strategy.pine | 12 +++ .../pm2-m7-mag-0922/tv_trades.csv | 3 + .../pm2-m7-mag-1010/meta.json | 16 +++ .../pm2-m7-mag-1010/metrics.json | 40 ++++++++ .../pm2-m7-mag-1010/strategy.pine | 12 +++ .../pm2-m7-mag-1010/tv_trades.csv | 5 + .../pm2-m7-mag-1121/meta.json | 16 +++ .../pm2-m7-mag-1121/metrics.json | 40 ++++++++ .../pm2-m7-mag-1121/strategy.pine | 12 +++ .../pm2-m7-mag-1121/tv_trades.csv | 3 + .../pm2-m7-poocl-0402-1945/meta.json | 16 +++ .../pm2-m7-poocl-0402-1945/metrics.json | 40 ++++++++ .../pm2-m7-poocl-0402-1945/strategy.pine | 13 +++ .../pm2-m7-poocl-0402-1945/tv_trades.csv | 5 + .../pm2-m7-poocl-0402-2000/meta.json | 16 +++ .../pm2-m7-poocl-0402-2000/metrics.json | 40 ++++++++ .../pm2-m7-poocl-0402-2000/strategy.pine | 13 +++ .../pm2-m7-poocl-0402-2000/tv_trades.csv | 5 + .../pm2-m7-poocl-0801-0030/meta.json | 16 +++ .../pm2-m7-poocl-0801-0030/metrics.json | 40 ++++++++ .../pm2-m7-poocl-0801-0030/strategy.pine | 13 +++ .../pm2-m7-poocl-0801-0030/tv_trades.csv | 5 + .../pm2-m7-poocl-1201-0215/meta.json | 16 +++ .../pm2-m7-poocl-1201-0215/metrics.json | 40 ++++++++ .../pm2-m7-poocl-1201-0215/strategy.pine | 13 +++ .../pm2-m7-poocl-1201-0215/tv_trades.csv | 5 + .../pm2-m7-poocm-0105-1415/meta.json | 16 +++ .../pm2-m7-poocm-0105-1415/metrics.json | 40 ++++++++ .../pm2-m7-poocm-0105-1415/strategy.pine | 12 +++ .../pm2-m7-poocm-0105-1415/tv_trades.csv | 5 + .../pm2-m7-poocm-0402-1945/meta.json | 16 +++ .../pm2-m7-poocm-0402-1945/metrics.json | 40 ++++++++ .../pm2-m7-poocm-0402-1945/strategy.pine | 12 +++ .../pm2-m7-poocm-0402-1945/tv_trades.csv | 5 + .../pm2-m7-poocm-0702-0100/meta.json | 16 +++ .../pm2-m7-poocm-0702-0100/metrics.json | 40 ++++++++ .../pm2-m7-poocm-0702-0100/strategy.pine | 12 +++ .../pm2-m7-poocm-0702-0100/tv_trades.csv | 5 + .../pm2-m7-poocm-1002-1445/meta.json | 16 +++ .../pm2-m7-poocm-1002-1445/metrics.json | 40 ++++++++ .../pm2-m7-poocm-1002-1445/strategy.pine | 12 +++ .../pm2-m7-poocm-1002-1445/tv_trades.csv | 5 + .../pm2-m7b-lim-0402-1930/meta.json | 16 +++ .../pm2-m7b-lim-0402-1930/metrics.json | 40 ++++++++ .../pm2-m7b-lim-0402-1930/strategy.pine | 15 +++ .../pm2-m7b-lim-0402-1930/tv_trades.csv | 7 ++ .../pm2-m7b-lim-0402-1945/meta.json | 16 +++ .../pm2-m7b-lim-0402-1945/metrics.json | 40 ++++++++ .../pm2-m7b-lim-0402-1945/strategy.pine | 15 +++ .../pm2-m7b-lim-0402-1945/tv_trades.csv | 7 ++ .../pm2-m7b-lim-0801-0015/meta.json | 16 +++ .../pm2-m7b-lim-0801-0015/metrics.json | 40 ++++++++ .../pm2-m7b-lim-0801-0015/strategy.pine | 15 +++ .../pm2-m7b-lim-0801-0015/tv_trades.csv | 7 ++ .../pm2-m7b-lim-1207-1345/meta.json | 16 +++ .../pm2-m7b-lim-1207-1345/metrics.json | 40 ++++++++ .../pm2-m7b-lim-1207-1345/strategy.pine | 15 +++ .../pm2-m7b-lim-1207-1345/tv_trades.csv | 7 ++ .../pm2-m7b-mkt-0105-1400/meta.json | 16 +++ .../pm2-m7b-mkt-0105-1400/metrics.json | 40 ++++++++ .../pm2-m7b-mkt-0105-1400/strategy.pine | 14 +++ .../pm2-m7b-mkt-0105-1400/tv_trades.csv | 7 ++ .../pm2-m7b-mkt-0402-1930/meta.json | 16 +++ .../pm2-m7b-mkt-0402-1930/metrics.json | 40 ++++++++ .../pm2-m7b-mkt-0402-1930/strategy.pine | 14 +++ .../pm2-m7b-mkt-0402-1930/tv_trades.csv | 7 ++ .../pm2-m7b-mkt-0702-0045/meta.json | 16 +++ .../pm2-m7b-mkt-0702-0045/metrics.json | 40 ++++++++ .../pm2-m7b-mkt-0702-0045/strategy.pine | 14 +++ .../pm2-m7b-mkt-0702-0045/tv_trades.csv | 7 ++ .../pm2-m7b-mkt-1002-1430/meta.json | 16 +++ .../pm2-m7b-mkt-1002-1430/metrics.json | 40 ++++++++ .../pm2-m7b-mkt-1002-1430/strategy.pine | 14 +++ .../pm2-m7b-mkt-1002-1430/tv_trades.csv | 7 ++ ...st_adapter_live_state_equivalence_data.hpp | 26 +++-- ...t_adapter_margin_schedule_differential.cpp | 65 +++++++++---- 103 files changed, 2032 insertions(+), 113 deletions(-) create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/tv_trades.csv create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/meta.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/metrics.json create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/strategy.pine create mode 100644 tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/tv_trades.csv diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index fefc5ecc..c4f3b84d 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -209,8 +209,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:677`) for TV sizing and fill spelling; `validate_execution_precommit` (`pine_strategy_host.cpp:702`), whose - `validate_precommit` (`pine_adapter.cpp:13065`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:13310`) to own the opening margin decision; `owns_lot_excursions() = true` + `validate_precommit` (`pine_adapter.cpp:13068`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:13313`) to own the opening margin decision; `owns_lot_excursions() = true` (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:787`), so the Pine host keeps its own per-lot MFE/MAE, `ab9714be`'s model (RULING A48). Measured against TradingView by R5 lane H-MEASURE (`tests/test_e19_excursion_tape.cpp`, `lab tv` tapes): where @@ -232,9 +232,9 @@ kernel and own these quirks, each at its site: (`select_exit_activation` src/compat/pine/exit_activation.cpp:42, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:10, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:46) 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:9623-9628`); the half-tick + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:9626-9631`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:333) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11703`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11706`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:454-460`, 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 @@ -249,8 +249,8 @@ kernel and own these quirks, each at its site: (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9). - **Margin.** The kernel owns the margin *mechanism* — the level solve, the check points, the kernel request, its re-pricing, the receipt — and the adapter answers its three policy hooks - with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14100`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14164`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14103`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:14167`) 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 @@ -261,7 +261,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:16868`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16871`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -281,12 +281,15 @@ kernel and own these quirks, each at its site: four `lab tv` tapes (`tests/fixtures/margin_entry_bar`) book TradingView's margin call on that bar at its low, as the kernel's `AfterApplied` point does, so `on_applied` admits that point for a leveraged opening's entry bar (R5 lane PAR-MARGIN; until then the adapter booked the - call a bar late or not at all, the divergence H-MEASURE recorded). It does so on the run - shapes `ab9714be`'s entry-bar pass covered (`leveraged_entry_bar_checked`): a - `process_orders_on_close`, `calc_on_order_fills`, magnified or timestamped-FX run keeps its - own route, unmeasured against TradingView, an add to a carried book is still refused there, and - the default-percent stop entry the pre-open slice answers for keeps that slice's verdict - (`preopen_slice_class`). Under the bar magnifier the adapter admits the kernel's + call a bar late or not at all, the divergence H-MEASURE recorded). Since R5 lane PAR-MARGIN-2 + it does so on every opening-side fill of that bar -- an add to a book carried in from an + earlier bar included -- and under `process_orders_on_close` and `calc_on_order_fills` + (`leveraged_entry_bar_checked`): twenty more `lab tv` tapes (limit openings with and without + `process_orders_on_close`, `calc_on_order_fills` openings, limit and market adds) book the call + on that bar at its low, which the kernel's post-fill point now reaches when the fill was + matched on the way down to it (the rule-2 row below). A magnified or timestamped-FX run keeps + its own route, and the default-percent stop entry the pre-open slice answers for keeps that + slice's verdict (`preopen_slice_class`). Under the bar magnifier the adapter admits the kernel's `IntrabarSample` points on a leveraged book (`intrabar_sample_checked`): TradingView's magnified broker books its call at the first lower-timeframe low that crosses and again at each later one that crosses the reduced book's line (`tests/fixtures/intrabar_margin`: every row on 3 of 4 @@ -297,9 +300,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:8314`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:8317`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:680`), and the two fills Pine refuses to - recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5663`). + recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5666`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the three session flags, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` @@ -551,7 +554,7 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | the order of one cause's group-effect receipts -- the queue order `WorkingRequestCore::group_recipients` walks, which a `ReplaceOptions::keep_handle` re-price parts from incarnation order: `receipt_lookup` bisects the receipts by cause and walks that cause's, and `prepare_group_effect` / `apply_group_effect` refuse only a cause older than the newest receipt's (`src/native_order.cpp`; `WorkingRequestCore::group_effect_receipt`, `include/pineforge/native_order.hpp`) | any C++ host that re-prices a group member with `keep_handle` and then fills a sibling (`NativeStrategyHost::replace(handle, request, options)`; no C spelling, the C replace takes no options); the Pine adapter never sets `keep_handle` | **generic: a drain applies a group's effect in queue order, whatever the handles' numbers** (R5 lane K-OCA-KEEP, K-ULP5's finding 1). Rule 2 (a), mechanism: which live siblings a member's fill reaches, and in what order, is the book's own queue for a host of any venue, and the receipts are that drain's idempotence record, in the order it applied them; a request re-priced with its handle kept is reached where a plain replace's successor is, so the two runs record one timeline, as V19-D's contract already said. Rule 2 (b), knob: none -- it replaces a whole-run `Contract` failure (code 2, discriminator 1, `CoreFailure::InvalidCause`, after the member's fill was booked) that no host could want: the receipt check held one cause's receipts to incarnation order, and the re-priced member, drained behind a younger sibling, was refused; a cause older than the newest receipt's is still refused. What moved: runs that stopped on such a drain now complete -- K-ULP5's `probe_keep_handle_oca`, staged and direct, under `Cancel` and `Reduce`, and all 180 runs of the seeded battery under a keep_handle option set (of its 360), which stopped on 2a03c658 -- each recording the plain replace's run event for event. No completed run moves and no hash value moved: a completed run's receipts were already in incarnation order within each cause, and the lookup answers such a store as before (the K-ULP4 and K-ULP5 batteries' per-run hashes unchanged; corpus parity unchanged by construction, the Pine adapter never setting `keep_handle`). Pinned by `tests/test_native_group_keep_handle.cpp` (kernel-only). | | a request on a quantity grid whose pending group deduction its units absorb -- it settles them all in one fill (`settles_in_one_fill`, `src/native_execution_consumer.cpp`, which asks `WorkingRequestCore::effective_host_units` whether the pending total takes any), so a whole-scope `ScopeFraction` resolves to its scope's held total unfloored and a host-sized close answered with that total is its own quantity, at the match and at the current execution alike | any host, by the OCA-Reduce groups and the quantity grid it declares (C: a fraction, and a close sized by `on_close_units`); the Pine adapter does not reach it: it emits no `ScopeFraction`, and it answers a close of its scope's whole total with `ExecutionGridPolicy::ExplicitUnits`, whose grid check never asks whether the request settles in one fill | **one definition of settling in one fill** (R5 lane K-OCA-KEEP, K-ULP5's finding 2). Rule 2 (a), mechanism: K-ULP4's grid rule admits the book's own quantity for a request that settles it in one fill, and K-ULP5's absorption makes a sub-ulp pending total take nothing, for a host of any venue; the two now agree. Rule 2 (b), knob: none. What moved: a run in which such a request closes its scope's total now closes it whole -- a whole-scope fraction was floored onto the grid and closed short, leaving a dust lot (before K-ULP5 such a run stopped wherever the floored units absorbed the total too, code 6, discriminator 7), and a host-sized close answered with its scope's total was refused `InvalidTerms` and left the lot open, before K-ULP5 as after: the `grid-absorbed-*` rows of `tests/test_native_group_keep_handle.cpp`, whose fraction closed 1.1000000000000001 of a 1.1000000000000227 lot on 2a03c658. K-ULP4's own battery answers every scenario's hash as before, K-ULP5's absorption and C-host batteries are unchanged (no grid), `grid-fraction-oca-pending` of `tests/test_native_unrepresentable_refusal.cpp` (a pending total that takes 0.3) is still floored, and no pinned hash moved. Pinned by `tests/test_native_group_keep_handle.cpp` (kernel-only). | | the margin model's check points on an intrabar path -- `NativeMarginCheckKind::IntrabarSample` (`include/pineforge/native_host.hpp`; C `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE`), offered at every delivered sample after the script bar's first of a `lower_tf` path matched as `ContinuousSegments`, at that sample's price, immediately before it is matched (`NativeExecutionConsumer::deliver_intrabar_script`, `src/native_execution_consumer.cpp`) | every host with a margin model and such a path, through `margin_check_allowed`; the Pine adapter admits it on a magnified run's leveraged book (`PineExecutionAdapter::intrabar_sample_checked`, `src/source/pine_adapter.cpp`) | **a check point of the kernel's own** (R5 lane PAR-MARGIN, item 3; H-MEASURE's acid finding A). Rule 2 (a), mechanism: a run with no whole-bar waypoint model measures its maintenance requirement where its path is -- at each delivered sample -- the rule the docs and `native_run_spec.hpp` stated and the kernel had not kept (it checked the first sample and after fills only); nothing in it knows a venue, and a host that checks somewhere else suppresses the points it does not share, as for every other kind. Rule 2 (b), knob: none -- the path is the run's own declaration, and a one-price distribution path (`synthesized`, `DistributionSamples`) is not offered the point, since a request armed at a discrete point is matched only at a later one. What moved: a margin run on a continuous intrabar path whose book crosses its line inside a bar now books that call at the crossing sample (H-MEASURE's intrabar probe: 55 points offered, a slice at 105 where none was booked). TradingView's magnified broker does the same (`tests/fixtures/intrabar_margin`: the first call at the first lower-timeframe low that crosses on 4 of 4 tapes, every row on 3). No pinned value moved. Pinned by `tests/test_native_margin_intrabar_samples.cpp` (kernel-only) and the "intrabar margin on tapes" section of `tests/test_adapter_margin_schedule_differential.cpp`. | -| the path a post-fill margin check measures -- `NativeExecutionConsumer::margin_segment_origin` (`src/native_execution_consumer.cpp`), the phase the `AfterApplied` re-arm hands `margin_sizing_price`: the waypoint the segment into the fill's driver point starts from, with the fill price as the point's own mark | every host with a margin model on a path walked by its modeled waypoints (no intrabar path), through the `AfterApplied` point `margin_check_allowed` admits | **a mechanism correction** (R5 lane PAR-MARGIN-2). Rule 2 (a), mechanism: a request matched at a driver point was reached on the segment into it, so the book its fill leaves still faces that point's waypoint -- a limit filled on its way down to the bar's low faces the low, a short filled on its way up faces the high -- and the scan that started strictly after the point's phase measured a path the book had not finished; the waypoints the path already passed stay out, as `fx_roll_margin_check_at`'s continuous roll measures from its segment's origin. Nothing in it knows a venue. Rule 2 (b), knob: none. What moved: a post-fill point after a fill matched on a segment now measures that segment's destination waypoint -- on a high-first bar O100 H101 L80 C81 a buy limit at 95 measures 80 where it measured the close 81, and a low-first bar or a short filled on its way up is called where nothing was (`tests/test_native_margin_post_fill_path.cpp`, kernel-only); a fill at a waypoint's own price, the open's included, measures what it did. TradingView books that call on the fill's bar (lab tv tapes `tests/fixtures/margin_entry_bar/pm2-m7-lim-*` at the low, `pm2-m7-slim-*` and `pm2-m7-s1lim-*` at the high, 12 of 12), as `ab9714be`'s entry-bar suffix did; the differential's M7-B pin moved to that answer, 10 @80. One v19 witness value moved with it: `tests/test_publication_witness.cpp` Config03, a short filled on its way up a bar, now called at that bar's high (461a919ebdcd41e9 -> 4593ef08ab33e045); the corpus is 312/312, moved 0. | +| the path a post-fill margin check measures -- `NativeExecutionConsumer::margin_segment_origin` (`src/native_execution_consumer.cpp`), the phase the `AfterApplied` re-arm hands `margin_sizing_price`: the waypoint the segment into the fill's driver point starts from, with the fill price as the point's own mark | every host with a margin model on a path walked by its modeled waypoints (no intrabar path), through the `AfterApplied` point `margin_check_allowed` admits | **a mechanism correction** (R5 lane PAR-MARGIN-2). Rule 2 (a), mechanism: a request matched at a driver point was reached on the segment into it, so the book its fill leaves still faces that point's waypoint -- a limit filled on its way down to the bar's low faces the low, a short filled on its way up faces the high -- and the scan that started strictly after the point's phase measured a path the book had not finished; the waypoints the path already passed stay out, as `fx_roll_margin_check_at`'s continuous roll measures from its segment's origin. Nothing in it knows a venue. Rule 2 (b), knob: none. What moved: a post-fill point after a fill matched on a segment now measures that segment's destination waypoint -- on a high-first bar O100 H101 L80 C81 a buy limit at 95 measures 80 where it measured the close 81, and a low-first bar or a short filled on its way up is called where nothing was (`tests/test_native_margin_post_fill_path.cpp`, kernel-only); a fill at a waypoint's own price, the open's included, measures what it did. TradingView books that call on the fill's bar (lab tv tapes `tests/fixtures/margin_entry_bar/pm2-m7-lim-*` at the low, `pm2-m7-slim-*` and `pm2-m7-s1lim-*` at the high, 12 of 12; with the adapter's admissions `pm2-m7-poocl-*` and `pm2-m7b-lim-*` too), as `ab9714be`'s entry-bar suffix did; the differential's M7-B pin moved to that answer, 10 @80. One v19 witness value moved with it: `tests/test_publication_witness.cpp` Config03, a short filled on its way up a bar, now called at that bar's high (461a919ebdcd41e9 -> 4593ef08ab33e045); the corpus is 312/312, moved 0. | | the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:1825` and `:1934`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:295`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | | the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1478`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | | the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1616`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | @@ -661,7 +664,7 @@ Every line number in this section names its symbol on this tree, which |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:342`), `source_level_on_price_grid` (`:328`), `nearest_tick` (`:266`) / `source_bar_fill_tick` (`:287`) / `directional_tick` (`:311`), 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`, `tests/test_native_price_grid.cpp` | | `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13953`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:15008`), `chart_day_key` (`pine_adapter.cpp:13850`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13257-13258`): 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_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:13260-13261`): 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:2361-2363`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2388-2396`, `margin.maintenance_long` `pine_adapter.cpp:2403`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 52b4f63e..199eaadf 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,19 +47,19 @@ 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:2738-2749; `entry()` pine_adapter.cpp:5311, `order()` pine_adapter.cpp:10512) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5698-5745; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2738-2749; `entry()` pine_adapter.cpp:5314, `order()` pine_adapter.cpp:10515) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5698-5745; `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:8417 → `Flatten{}` pine_adapter.cpp:8446-8447; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2708-3915 → `host.replace` pine_adapter.cpp:2724-2804) | **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:4723-4769, `cancel_all` pine_adapter.cpp:18096-18111; `live_by_source_key_` pine_adapter.hpp:1999; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **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. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4723-4769, `cancel_all` pine_adapter.cpp:18099-18114; `live_by_source_key_` pine_adapter.hpp:1999; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5535-5570, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7505-7522) | **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) | +| 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:5538-5573, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7508-7525) | **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:2747-2753) | **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:13035; lowering `submit_one` pine_adapter.cpp:9072-9084, `BindCohort` pine_adapter.cpp:9573-9579, `submit_leg` pine_adapter.cpp:9597-9621; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8677-8680; `materialize_relative_exits` pine_adapter.cpp:17386-17427, pine_adapter.hpp:1824-1836) | **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:5624-5658. `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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19167-19196) | **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:2001-2019); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10912-10936), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1585-1608, `CloseCallsiteState` pine_adapter.hpp:1679-1694; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15695, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15929) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:13038; lowering `submit_one` pine_adapter.cpp:9075-9087, `BindCohort` pine_adapter.cpp:9576-9582, `submit_leg` pine_adapter.cpp:9600-9624; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8680-8683; `materialize_relative_exits` pine_adapter.cpp:17389-17430, pine_adapter.hpp:1824-1836) | **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:5624-5658. `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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19173-19202) | **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:2001-2019); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10915-10939), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1585-1608, `CloseCallsiteState` pine_adapter.hpp:1679-1694; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15698, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15932) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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,26 +68,26 @@ 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:5710-5732; `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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11811-11823) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4464-4469) **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:11724-11861) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11814-11826) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4464-4469) **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:11727-11864) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4022-4068) | **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:287-289) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9623-9628, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11724-11861, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8647-8873, `trail_one_shot` pine_adapter.cpp:9712-9841 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9626-9631, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11727-11864, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8650-8876, `trail_one_shot` pine_adapter.cpp:9715-9844 | 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:5924-5928, pine_adapter.cpp:5924) | **yes** — `Transact` in `OrderIntent` native_order.hpp:229 | K | — | F:C1 O:C1 S:S1 | — | +| SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5927-5931, pine_adapter.cpp:5927) | **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:10989-12193) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4935-4969 | 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:2596-2651) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4976-4978 **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: rounded equity `source_money_round` pine_adapter.cpp:2548, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2557-2559) | **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:2609, `typed_entry_units` pine_adapter.cpp:2627; `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:2588; `percent_commission_live_equity` pine_adapter.cpp:4329-4369) | **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. | -| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2514-2529, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6672-6698) | **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`. | +| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2514-2529, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6680-6706) | **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:592, 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:4992-4995; 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:12510-12538, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:8999-9309) | **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. | +| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:12513-12541, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:9002-9312) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4444-4459) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6733), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7945), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12766), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13362, pine_adapter.cpp:13415) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15743) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18763) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7207-7558; ledger `close_batch_bar_` pine_adapter.hpp:2042-2053; 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. | +| 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:6741), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7948), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12769), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13365, pine_adapter.cpp:13418) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15746) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18769) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7210-7561; ledger `close_batch_bar_` pine_adapter.hpp:2042-2053; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | ### 1.4 Margin, admission, liquidation, risk @@ -95,21 +95,21 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2362) | **yes** — `allowed_open_directions` native_run_spec.hpp:597 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:595 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13257-13260) is TV. | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:595 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13260-13263) is TV. | | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6230-6279, cap left unset `max_open_lots` pine_adapter.cpp:2361-2363) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13311-13376 and leaves the kernel gate unset pine_adapter.cpp:13310-13312) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13966-13967, `margin_pct` pine_adapter.cpp:13374) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:14066-14272, `submit_margin_call_units` pine_adapter.cpp:14233-14279, `schedule_margin_call_path` pine_adapter.cpp:18249-18347) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13997-14033) | **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. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13314-13379 and leaves the kernel gate unset pine_adapter.cpp:13313-13315) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13969-13970, `margin_pct` pine_adapter.cpp:13377) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:14069-14275, `submit_margin_call_units` pine_adapter.cpp:14236-14282, `schedule_margin_call_path` pine_adapter.cpp:18252-18353) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14000-14036) | **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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16347-16363, `apply_fx_opening_margin_slice` pine_adapter.cpp:18348-18376) | **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:16838-16877) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13346-13349) | **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:13927-13941, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14986-15016) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15029-15263) | **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:13737-13816) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1323 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:13955, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14014, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14750-14790, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14274-14401, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14274) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged book, where TradingView books its call at the first lower-timeframe low that crosses (`tests/fixtures/intrabar_margin`, every row on 3 of 4 tapes, the fourth a recorded divergence). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15267-15325) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16350-16366, `apply_fx_opening_margin_slice` pine_adapter.cpp:18354-18382) | **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:16841-16880) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13349-13352) | **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:13930-13944, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14989-15019) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15032-15266) | **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:13740-13819) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1323 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:13958, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14017, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14753-14793, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14277-14404, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14277) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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, `tests/fixtures/margin_entry_bar/pm2-*`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged book, where TradingView books its call at the first lower-timeframe low that crosses (`tests/fixtures/intrabar_margin`, every row on 3 of 4 tapes, the fourth a recorded divergence). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15270-15328) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -117,15 +117,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7542-7546 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16182) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16185) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `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:7027 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7078 (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:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5632, `flush_coof_tail` pine_adapter.cpp:16325, `end_coof_recalc` pine_adapter.cpp:5653) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5635, `flush_coof_tail` pine_adapter.cpp:16328, `end_coof_recalc` pine_adapter.cpp:5656) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7166-7170; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | | 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:7536-7655 **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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:234-320) 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:5779), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1660-1667 | 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:5782), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1660-1667 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds @@ -145,7 +145,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:5698-5745, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6281-6289; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | | FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:584 applied once native_execution_consumer.cpp:4611-4612, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12032-12041) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:472-479) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11994-12003, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4073; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6363-6364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11997-12006, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4073; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6363-6364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:8739). **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). | @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19167-19196 | 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:19173-19202 | 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:523, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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:604. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1274 | 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) | @@ -298,7 +298,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3033-3101) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3005-3073) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:45, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -522,11 +522,11 @@ line numbers are this tree's (683a82f); kernel semantics are | 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:2362); 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:13257-13258; 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:16838, `update_risk_state` pine_adapter.cpp:13898-13925): 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:13250), 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_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:13260-13261; 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:16841, `update_risk_state` pine_adapter.cpp:13901-13928): 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:13253), 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:1200-1208): +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:17724-17733); 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:16341); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16453), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15016-15025, `submit_intraday_loss_close` pine_adapter.cpp:14986-15014), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17195); 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:5983, `:7652`, `:8376`, `:8504`, `:11481`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18094-18097) | `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:13889-13891); 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:13795-13816, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16344); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16456), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15019-15028, `submit_intraday_loss_close` pine_adapter.cpp:14989-15017), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17198); 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:5986, `:7655`, `:8379`, `:8507`, `:11484`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18097-18100) | `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:13892-13894); 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:13798-13819, `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, @@ -1053,10 +1053,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:8788). Full-table scans + (`has_full_entry_bracket` pine_adapter.cpp:8791). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1696, `revive_brackets_after_margin` pine_adapter.cpp:1918, - `preserved_by_close_all` pine_adapter.cpp:11512), and + `preserved_by_close_all` pine_adapter.cpp:11515), 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. @@ -1299,8 +1299,8 @@ unchanged after it. | E1 | **Spec hashing.** `hash_spec` folds every field unconditionally (native_execution_consumer.cpp:234-256) | 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:1620); 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5779) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6290-6303); 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:16182-16211) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E4 | **Calc on fill.** Kernel suffix eligibility is continuous along the segment; TV refills only at O/H/L/C waypoints (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5782) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6290-6303); 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:16185-16214) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | @@ -1345,7 +1345,7 @@ unchanged after it. | **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:16347-16363) 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:2646) 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:16350-16366) 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:2646) 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. | @@ -1443,7 +1443,7 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1700-1701) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6995-7007) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1684-1685) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6995-7007) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). 2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5629-5633) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1660-1667) is calibrated against it. 3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6848-6855). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2553); valid in bar-open, applied and tick callbacks. @@ -1531,7 +1531,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:1329) (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:373), 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:13721-13819); 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:13724-13822); 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:4675-4688 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 64526fb2..f6fce543 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1480,7 +1480,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:14100-14133`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:14103-14136`), 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 @@ -1524,12 +1524,14 @@ tapes (`tests/fixtures/margin_entry_bar`) book TradingView's margin call on that bar at its low, as the kernel's `AfterApplied` point does, and since R5 lane PAR-MARGIN the adapter admits that point for a leveraged opening's entry bar and books the tapes' rows (before it, the call came a bar late or not at -all). The admission covers the run shapes `ab9714be`'s entry-bar pass covered; -a `process_orders_on_close`, `calc_on_order_fills`, magnified or -timestamped-FX run keeps its own route, unmeasured against TradingView, the -`AfterApplied` point after an add to a carried book is still refused, and a -default-percent stop entry taken at its bar's open keeps the pre-open slice's -verdict. Under the bar magnifier the adapter admits the kernel's +all). Since R5 lane PAR-MARGIN-2 the admission also covers a limit opening +filled on its way down to the bar's low, `process_orders_on_close` and +`calc_on_order_fills` runs, and an add to a book carried in from an earlier +bar, on the combined book: twenty more tapes book the call on that bar at its +low (`tests/fixtures/margin_entry_bar`: `pm2-m7-coof-*`, `pm2-m7-lim-*`, +`pm2-m7-poocl-*`, `pm2-m7b-*`). A magnified or timestamped-FX run keeps +its own route, and a default-percent stop entry taken at its bar's open keeps +the pre-open slice's verdict. Under the bar magnifier the adapter admits the kernel's `IntrabarSample` points on a leveraged book: TradingView's magnified broker books its call at the first lower-timeframe low that crosses and again at each later one that crosses the reduced book's line (`tests/fixtures/intrabar_margin`, diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index e19d7392..608b09bb 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -4874,12 +4874,15 @@ void PineExecutionAdapter::apply_fx_opening_margin_slice( } // Whether on_applied admits the kernel's post-fill point on a leveraged -// opening's entry bar (the run shapes ab9714be took the entry-bar suffix on, -// pine_fills.cpp:1014-1023, less a timestamped-FX run, whose path checks -// on_bar_open does not schedule either). +// opening's entry bar: the run shapes ab9714be took the entry-bar suffix on +// (pine_fills.cpp:1014-1023), and process_orders_on_close and +// calc_on_order_fills, where TradingView books the same call on the same bar +// (R5 lane PAR-MARGIN-2: tests/fixtures/margin_entry_bar/pm2-m7-coof-* and +// pm2-m7-poocl-*). Not a timestamped-FX run, whose path checks on_bar_open +// does not schedule either, nor a magnified one, whose later samples the +// kernel's IntrabarSample point checks (intrabar_sample_checked). bool PineExecutionAdapter::leveraged_entry_bar_checked() const noexcept { - return !config_.process_orders_on_close && !config_.calc_on_order_fills - && !bar_magnifier_ && staged_.account_fx_effective_from_ms.empty(); + return !bar_magnifier_ && staged_.account_fx_effective_from_ms.empty(); } // The opening schedule_preopen_margin_slice answers for: a default-percent @@ -18409,7 +18412,6 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& } } else if (!full_margin_opening && !placement_snapshot->has_full_entry_bracket && !preopen_margin_already_scheduled && stable_opening_fx - && position_open_script_bar_ == context.script_bar_open_ms && leveraged_entry_bar_checked() && !preopen_slice_class(*placement_snapshot, context)) { // A leveraged opening (margin below 100) has no fill-price trim, @@ -18418,15 +18420,19 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& // book the margin call at that bar's low, as ab9714be's // entry-bar pass did (pine_fills.cpp:1025-1063 and :1535-1546, // the suffix after the opening's fill). The run shapes ab9714be - // took no such suffix on keep their own routes here, unmeasured - // against TradingView (leveraged_entry_bar_checked), and so does - // the opening schedule_preopen_margin_slice answers for, sliced or - // not (preopen_slice_class). Admit the kernel's post-fill point - // for this driver point: it measures the same suffix and sizes - // the call with TradingView's money and slice + // took no such suffix on keep their own routes here + // (leveraged_entry_bar_checked), and so does the opening + // schedule_preopen_margin_slice answers for, sliced or not + // (preopen_slice_class). Admit the kernel's post-fill point for + // this driver point: it measures the same suffix and sizes the + // call with TradingView's money and slice // (resolve_margin_requirement, resolve_margin_call_units). Every - // opening fill of the entry bar is admitted, so an add re-sizes a - // call armed on the smaller book. + // opening-side fill is admitted on its own bar -- the position's + // first, an add on the same bar, and an add to a book carried in + // from an earlier bar, which TradingView checks over the rest of + // its bar on the combined book too (R5 lane PAR-MARGIN-2, + // pm2-m7b-lim-* and pm2-m7b-mkt-*) -- so an add re-sizes a call + // armed on the smaller book. (void)schedule_margin_call_path(policy_script_bar_, context); } schedule_intraday_loss_path(policy_script_bar_, context); diff --git a/tests/fixtures/margin_entry_bar/README.md b/tests/fixtures/margin_entry_bar/README.md index 00a709c9..826ad2ef 100644 --- a/tests/fixtures/margin_entry_bar/README.md +++ b/tests/fixtures/margin_entry_bar/README.md @@ -1,8 +1,65 @@ +# Leveraged entry-bar margin tapes (R5 lane H-MEASURE, row M7 / G2-09) +TradingView's own trades for one question AUDIT4 §6 left "ruled, unmeasured": +does TradingView check a LEVERAGED opening (margin below 100 %) on the bar its +entry fills, where the adapter's `margin_check_allowed` refuses the kernel's +AfterApplied point? `tests/test_adapter_margin_schedule_differential.cpp` +(section "M7 on tapes") replays every tape through the Pine adapter and through +a bare kernel host with TradingView's money and slice. TradingView does, on 4 +of 4; since R5 lane PAR-MARGIN `on_applied` admits that point for a leveraged +opening's entry bar, and the section asserts the adapter books each tape's +rows (lane H-MEASURE had pinned the adapter's late or missing call instead). + +Each directory is one `lab tv` export (pineforge-workflow, channel +`ws-report-v1`, `rangeProof: covered`), byte-identical: `strategy.pine`, +`tv_trades.csv` (times UTC+8), `meta.json`, `metrics.json`. The sha256 of each +tape is `metrics.json` `tvTradesCsvHash`; the campaign note is +`tv-tape--`. + +| probe | chart | margin, size | entry bar (UTC) | TradingView | +|---|---|---|---|---| +| `hm-m7a-eth-m5-k16-0621` | BINANCE:ETHUSDT.P 15 | 5 %, 16 x equity | 2025-06-21 21:30 | margin call 6.842 @2230 (the entry bar's low), the whole book | +| `hm-m7a-eth-m5-k16-0922` | same | 5 %, 16 x | 2025-09-22 06:00 | margin call 3.793 @4000.88, the whole book | +| `hm-m7a-eth-m5-k16-1121` | same | 5 %, 16 x | 2025-11-21 07:30 | margin call 5.806 @2642.65, the whole book | +| `hm-m7a-eth-m20-k4-1010` | same | 20 %, 4 x | 2025-10-10 21:15 | margin call 0.4544 @3400 (4 x 0.1136), the rest 0.6196 at the timeout @3895 | + +All four export windows are 2025-04-01 .. 2026-05-01; each script trades once. +`bars.inc` holds each event's signal bar through the entry bar + 4, from the +corpus feed `corpus/data/derived/ohlcv_ETH-USDT-USDT_15m.csv` materialized by +`scripts/derive_corpus_feeds.py` at corpus gitlink eede4a2. + +## M10: the book a default-percent stop entry above 100 % opens (R5 lane PAR-MARGIN) + +Eight more `lab tv` exports on NYSE:F 15m, same channel and layout, margin 25, +one buy stop placed on the session's last bar (15:45 America/New_York) and +filled at the next session's open, closed 3 bars after its entry bar: +`pm-m10-f-k4-*` at 400 % with the stop at 0.9 x close (marketable when +placed), `pm-m10-f-gapup-*` at 390 % with the stop at round(1.002 x close, 2) +(not marketable when placed, gapped through by the open). TradingView opens the +quotient of the signal close on its tick for the first and of the snapped +stop level for the second, never the fill's, and books its margin calls on +that book. `m10_bars.inc` holds each event's bars as TradingView reports them, +read by the two `pm-m10-f-ohlc*` exports (each bar printed as an order +comment); they equal the lab feed `80f404ae85ef`, half cents included. +`tests/test_adapter_margin_schedule_differential.cpp` (section "M10 on tapes") +replays all eight. Campaign notes: `tv-tape--`. + +| probe | window | TradingView | tvTradesCsvHash | +|---|---|---|---| +| `pm-m10-f-k4-0402` | 2026-03-25 .. 2026-04-07 | book 3427; margin calls 176 @11.38 | `3bd3141a7089f30c` | +| `pm-m10-f-k4-0410` | 2025-04-02 .. 2025-04-15 | book 4219; margin calls none | `8911072fc9bfb7ad` | +| `pm-m10-f-k4-1007` | 2025-09-29 .. 2025-10-10 | book 3144; margin calls 980 @12.11 | `bd8d6bc27e858bfb` | +| `pm-m10-f-k4-0309` | 2026-03-02 .. 2026-03-12 | book 3294; margin calls 4 @11.83, 56 @11.81 | `ab895fec3d476066` | +| `pm-m10-f-gapup-0501` | 2025-04-24 .. 2025-05-06 | book 3896; margin calls 376 @10.03 | `c672eb182acd2a16` | +| `pm-m10-f-gapup-0331` | 2026-03-24 .. 2026-04-03 | book 3475; margin calls 156 @11.28 | `8f54192f161b9013` | +| `pm-m10-f-gapup-0424` | 2025-04-17 .. 2025-04-29 | book 3987; margin calls 92 @9.81 | `79a6aa531e7bfbb3` | +| `pm-m10-f-gapup-0527` | 2025-05-19 .. 2025-05-30 | book 3757; margin calls 204 @10.35 | `f6222f524d46cd37` | +| `pm-m10-f-ohlc` | 2025-04-01 .. 2026-04-10 | 24 bars' open, high, low and close printed as entry comments | `8b006a00aff01690` | +| `pm-m10-f-ohlc2` | 2025-04-01 .. 2026-04-10 | 24 bars' open, high, low and close printed as entry comments | `52160db7208aea53` | ## PAR-MARGIN-2: the M7 shapes PAR-MARGIN left unmeasured (R5 lane PAR-MARGIN-2) -Twelve more `lab tv` exports on BINANCE:ETHUSDT.P 15m (same channel and +Thirty-six more `lab tv` exports on BINANCE:ETHUSDT.P 15m (same channel and layout), each the lane's own synthetic script: one leveraged entry (or an entry and an add) sized `K x strategy.equity / close` on a 0.001 grid at a signal bar, closed 3 bars (4 for the adds) after the first open trade's entry bar. @@ -10,6 +67,8 @@ bar, closed 3 bars (4 for the adds) after the first open trade's entry bar. replays every one FROM ITS OWN `strategy.pine` (parsed, not restated), and the adapter books TradingView's rows on all of them: +- `pm2-m7-coof-*`: M7-A's four scripts under `calc_on_order_fills` -- the same + rows as `hm-m7a-*` (the same tape hashes): the call on the entry bar at its low. - `pm2-m7-lim-*`: a limit opening (margin 5, `limit = math.round(close * 0.997, 2)`) filled on the next bar on its way down, whose low then breaches: the call on that bar at its LOW -- three high-first bars and a low-first one (0402-1945). @@ -18,6 +77,18 @@ the adapter books TradingView's rows on all of them: whose high then breaches, leveraged (margin 5) and at full margin (1 x): the call on that bar at its HIGH -- three low-first bars and a high-first one (0509-1100). +- `pm2-m7-poocl-*`: the four `pm2-m7-lim-*` scripts under + `process_orders_on_close`: the same calls. +- `pm2-m7-poocm-*`: a `process_orders_on_close` market opening filled at the + signal close; the call on the next bar at its low. +- `pm2-m7b-lim-*`: a carried long (margin 5, 4 x equity, never near its own + line) and a LIMIT add (about 8-14 x) filled on the next bar on its way down: + the call on the add's bar at its low, sized on the combined book, the + first-in lot (L1) closed first. +- `pm2-m7b-mkt-*`: a carried 1 x long (margin 20) and a market add at the next + open: the call on the add's bar at its low, on the combined book. +- `pm2-m7-mag-*`: M7-A's four scripts under the bar magnifier: the call at the + first crossing low of TradingView's intrabars (see `../intrabar_margin`). `pm2_bars.inc` holds each event's bars from the corpus feed `corpus/data/ohlcv_ETH-USDT-USDT_1m.csv` (15m bars aggregated from it as the @@ -25,10 +96,26 @@ derived feed does; the magnified scripts' one-minute bars as they are). | probe | window | TradingView's margin calls | tvTradesCsvHash | |---|---|---|---| +| `pm2-m7-coof-0621` | 2025-04-01 .. 2026-05-01 | 6.842 @2230 | `82afdec0f73135df` | +| `pm2-m7-coof-0922` | 2025-04-01 .. 2026-05-01 | 3.793 @4000.88 | `a56518c2145a0aa7` | +| `pm2-m7-coof-1010` | 2025-04-01 .. 2026-05-01 | 0.4544 @3400 | `5904c2624a0219b9` | +| `pm2-m7-coof-1121` | 2025-04-01 .. 2026-05-01 | 5.806 @2642.65 | `9f1b6f0dc175e472` | | `pm2-m7-lim-0402-1945` | 2025-03-30 .. 2025-04-05 | 4.516 @1881 | `c8d52c248bfae6ad` | | `pm2-m7-lim-0402-2000` | 2025-03-30 .. 2025-04-05 | 3.7668 @1872.48 | `c50706a1de9fa157` | | `pm2-m7-lim-0801-0030` | 2025-07-29 .. 2025-08-04 | 2.22 @3613.67 | `07b081a7560f112c` | | `pm2-m7-lim-1201-0215` | 2025-11-28 .. 2025-12-04 | 1.7024 @2824.6 | `8dbe22c6e8867a97` | +| `pm2-m7-mag-0621` | 2025-06-19 .. 2025-06-23 | 6.842 @2230 | `82afdec0f73135df` | +| `pm2-m7-mag-0922` | 2025-09-20 .. 2025-09-24 | 3.793 @4076.79 | `5c3468e8176b5aad` | +| `pm2-m7-mag-1010` | 2025-10-08 .. 2025-10-12 | 0.3096 @3428.11 | `bbac24deb07eed25` | +| `pm2-m7-mag-1121` | 2025-11-19 .. 2025-11-23 | 5.806 @2680.37 | `d2ebdc4cff50a62c` | +| `pm2-m7-poocl-0402-1945` | 2025-03-30 .. 2025-04-05 | 4.516 @1881 | `e7852d9c3904147c` | +| `pm2-m7-poocl-0402-2000` | 2025-03-30 .. 2025-04-05 | 3.7668 @1872.48 | `0d4698a024f7cbf8` | +| `pm2-m7-poocl-0801-0030` | 2025-07-29 .. 2025-08-04 | 2.22 @3613.67 | `816a9c1fda6dff83` | +| `pm2-m7-poocl-1201-0215` | 2025-11-28 .. 2025-12-04 | 1.7024 @2824.6 | `b6332c9af3571acf` | +| `pm2-m7-poocm-0105-1415` | 2026-01-02 .. 2026-01-08 | 1.496 @3132.58 | `b0ae1e3b89dcf62c` | +| `pm2-m7-poocm-0402-1945` | 2025-03-30 .. 2025-04-05 | 5.4568 @1881 | `b7e708608a5cea37` | +| `pm2-m7-poocm-0702-0100` | 2025-06-29 .. 2025-07-05 | 3.9452 @2367.53 | `78ad71bf244a719e` | +| `pm2-m7-poocm-1002-1445` | 2025-09-29 .. 2025-10-05 | 1.4844 @4332.23 | `3c75ca7c5cd23668` | | `pm2-m7-s1lim-0402-1330` | 2025-03-30 .. 2025-04-05 | 0.0152 @1892.7 | `d633d75493717e91` | | `pm2-m7-s1lim-0509-1100` | 2025-05-06 .. 2025-05-12 | 0.0132 @2375.39 | `5f62b5db446e583a` | | `pm2-m7-s1lim-0709-1930` | 2025-07-06 .. 2025-07-12 | 0.0144 @2795.73 | `8d7299c2d5e6ae4f` | @@ -37,3 +124,11 @@ derived feed does; the magnified scripts' one-minute bars as they are). | `pm2-m7-slim-0509-1100` | 2025-05-06 .. 2025-05-12 | 2.6232 @2375.39 | `8b448336731119f7` | | `pm2-m7-slim-0709-1930` | 2025-07-06 .. 2025-07-12 | 2.5484 @2795.73 | `02f292a474aef979` | | `pm2-m7-slim-1001-0830` | 2025-09-28 .. 2025-10-04 | 1.5688 @4329 | `320a3e2199cfaa91` | +| `pm2-m7b-lim-0402-1930` | 2025-03-30 .. 2025-04-05 | 2.099 @1881, 2.4142 @1881 | `e6aaeb8a522871f6` | +| `pm2-m7b-lim-0402-1945` | 2025-03-30 .. 2025-04-05 | 2.093 @1872.48, 1.8338 @1872.48 | `785ea45bf94a33ee` | +| `pm2-m7b-lim-0801-0015` | 2025-07-29 .. 2025-08-04 | 1.084 @3613.67, 1.042 @3613.67 | `0fcd4f3a0df2f3cb` | +| `pm2-m7b-lim-1207-1345` | 2025-12-04 .. 2025-12-10 | 1.32 @2903.75, 0.5228 @2903.75 | `b0e782a34b812dd2` | +| `pm2-m7b-mkt-0105-1400` | 2026-01-02 .. 2026-01-08 | 0.084 @3132.58 | `080f04615748c335` | +| `pm2-m7b-mkt-0402-1930` | 2025-03-30 .. 2025-04-05 | 0.3136 @1881 | `a5f4d89f44b0fa93` | +| `pm2-m7b-mkt-0702-0045` | 2025-06-29 .. 2025-07-05 | 0.2288 @2367.53 | `cf8b4bacde9150a9` | +| `pm2-m7b-mkt-1002-1430` | 2025-09-29 .. 2025-10-05 | 0.084 @4332.23 | `c2c01658ce472bb6` | diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/meta.json new file mode 100644 index 00000000..dda007a0 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-coof-0621", + "slug": "pm2-m7-coof-0621", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:36:57.843Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01" + }, + "pine_sha256": "6aa3e34cf35ed3d3b3afb2fb1241530e40db285486f2e6c2232fc3d1d32bdd1f", + "tv_trades_sha256": "82afdec0f73135dfa1af943f73db2cd6ee01a1fa2125935355b5a65cc7e85ca0", + "tv_trades_rows": 2 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/metrics.json new file mode 100644 index 00000000..e2c6bfd4 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-coof-0621", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01", + "deepBacktesting": true, + "trades": 1, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "6aa3e34cf35ed3d3b3afb2fb1241530e40db285486f2e6c2232fc3d1d32bdd1f", + "tvTradesCsvHash": "82afdec0f73135dfa1af943f73db2cd6ee01a1fa2125935355b5a65cc7e85ca0", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75173", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "eea3e1bd6d8b4e76983d7180b644c995df0b9de4633439e18149895e32a8dd6c", + "decodedReportSha256": "0ef387e8afad397a35bf5460a4ed90a04daf33f096bfc921955fff9c846712e8", + "closedTrades": 1, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:36:57.842Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/strategy.pine new file mode 100644 index 00000000..eba74877 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7 under calc_on_order_fills): a leveraged long (margin 5, 16 x equity) +// placed on the signal bar 2025-06-21 21:15 UTC, filled at the next bar's open, whose low breaches. +// calc_on_order_fills ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-coof-0621", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false, calc_on_order_fills=true) +if time == timestamp("UTC", 2025, 6, 21, 21, 15) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(16 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/tv_trades.csv new file mode 100644 index 00000000..66689658 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0621/tv_trades.csv @@ -0,0 +1,3 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-22 05:30,Margin call,2230,6.842,15998.92228,-741.26227,-4.63,0,2.32628,0.01,-741.26227,-4.63,-741.26227,-74.13,0 +1,Entry long,2025-06-22 05:30,L,2338.34,6.842,15998.92228,-741.26227,-4.63,0,2.32628,0.01,-741.26227,-4.63,-741.26227,-74.13,0 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/meta.json new file mode 100644 index 00000000..4a0e9696 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-coof-0922", + "slug": "pm2-m7-coof-0922", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:39:50.494Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01" + }, + "pine_sha256": "779dc72cfbe0eb92f5e87335940d05ade76a0fc0464187b3209b91c22616b163", + "tv_trades_sha256": "a56518c2145a0aa788b494a247ad79dbafea2768ee688a8da0e3b3636c5f4b9c", + "tv_trades_rows": 2 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/metrics.json new file mode 100644 index 00000000..4deddd8e --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-coof-0922", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01", + "deepBacktesting": true, + "trades": 1, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "779dc72cfbe0eb92f5e87335940d05ade76a0fc0464187b3209b91c22616b163", + "tvTradesCsvHash": "a56518c2145a0aa788b494a247ad79dbafea2768ee688a8da0e3b3636c5f4b9c", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75260", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "3c8b029c71e8a8a39360d3caf9cb1b0848d24486adf84eeea59891b3bd853189", + "decodedReportSha256": "77b8e1c072a5cb48129e9435f63686855ccd08f804d48de8e9060a50b21c0418", + "closedTrades": 1, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:39:50.493Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/strategy.pine new file mode 100644 index 00000000..7478656f --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7 under calc_on_order_fills): a leveraged long (margin 5, 16 x equity) +// placed on the signal bar 2025-09-22 05:45 UTC, filled at the next bar's open, whose low breaches. +// calc_on_order_fills ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-coof-0922", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false, calc_on_order_fills=true) +if time == timestamp("UTC", 2025, 9, 22, 5, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(16 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/tv_trades.csv new file mode 100644 index 00000000..dfd9d766 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-0922/tv_trades.csv @@ -0,0 +1,3 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-09-22 14:00,Margin call,4000.88,3.793,15997.7361,-822.39825,-5.14,0,26.51307,0.17,-822.39825,-5.14,-822.39825,-82.24,0 +1,Entry long,2025-09-22 14:00,L,4217.7,3.793,15997.7361,-822.39825,-5.14,0,26.51307,0.17,-822.39825,-5.14,-822.39825,-82.24,0 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/meta.json new file mode 100644 index 00000000..2e891030 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-coof-1010", + "slug": "pm2-m7-coof-1010", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:39:54.343Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01" + }, + "pine_sha256": "f5f5fa1839592fb12de4cfa73ea51dc515444e81ad19d3cffdd2c4b1e965a4a1", + "tv_trades_sha256": "5904c2624a0219b943b699f38334453f61ebac1ecd6f5361c9ac6278d091149b", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/metrics.json new file mode 100644 index 00000000..9c8df40e --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-coof-1010", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "f5f5fa1839592fb12de4cfa73ea51dc515444e81ad19d3cffdd2c4b1e965a4a1", + "tvTradesCsvHash": "5904c2624a0219b943b699f38334453f61ebac1ecd6f5361c9ac6278d091149b", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75261", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "548b526d5000e015067385cd89eedeff491ea3e96d86a812f64970b18e6e8418", + "decodedReportSha256": "81ecac82a281b9ee2875a086520a4748a399d66fd6314de1bf299156f35b21e5", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:39:54.343Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/strategy.pine new file mode 100644 index 00000000..29bcfa1a --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7 under calc_on_order_fills): a leveraged long (margin 20, 4 x equity) +// placed on the signal bar 2025-10-10 21:00 UTC, filled at the next bar's open, whose low breaches. +// calc_on_order_fills ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-coof-1010", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, calc_on_order_fills=true) +if time == timestamp("UTC", 2025, 10, 10, 21, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/tv_trades.csv new file mode 100644 index 00000000..87b6a068 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1010/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-10-11 05:15,Margin call,3400,0.4544,1691.75392,-146.79391,-8.68,0,30.363008,1.79,-146.79391,-8.68,-146.79391,-14.68,0 +1,Entry long,2025-10-11 05:15,L,3723.05,0.4544,1691.75392,-146.79391,-8.68,0,30.363008,1.79,-146.79391,-8.68,-146.79391,-14.68,0 +2,Exit long,2025-10-11 06:15,timeout,3895,0.6196,2306.80178,106.54022,4.62,0,159.62755,6.92,-200.16177,-8.68,-40.2537,-4.03,4 +2,Entry long,2025-10-11 05:15,L,3723.05,0.6196,2306.80178,106.54022,4.62,0,159.62755,6.92,-200.16177,-8.68,-40.2537,-4.03,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/meta.json new file mode 100644 index 00000000..50a10d0d --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-coof-1121", + "slug": "pm2-m7-coof-1121", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:39:58.187Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01" + }, + "pine_sha256": "f6fa5e04d57567aa74bbd158ee3f61f2efacd8099218a7b248d191ef8eb5fae7", + "tv_trades_sha256": "9f1b6f0dc175e472a236fe34254599b53657d07cc261d7a468621a8f72c5e47c", + "tv_trades_rows": 2 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/metrics.json new file mode 100644 index 00000000..89c5fe83 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-coof-1121", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2026-05-01", + "deepBacktesting": true, + "trades": 1, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "f6fa5e04d57567aa74bbd158ee3f61f2efacd8099218a7b248d191ef8eb5fae7", + "tvTradesCsvHash": "9f1b6f0dc175e472a236fe34254599b53657d07cc261d7a468621a8f72c5e47c", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75262", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1777593600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "5d17c6ad46e47a118435a6d4da67b54af156946da6c476635be79cf2cb428c16", + "decodedReportSha256": "bb23edecc9f08b7c1cb256f9db3aa353cdaef867cfbbaae96bd85a30cdd6f81e", + "closedTrades": 1, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:39:58.186Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/strategy.pine new file mode 100644 index 00000000..c2d4971d --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7 under calc_on_order_fills): a leveraged long (margin 5, 16 x equity) +// placed on the signal bar 2025-11-21 07:15 UTC, filled at the next bar's open, whose low breaches. +// calc_on_order_fills ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-coof-1121", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false, calc_on_order_fills=true) +if time == timestamp("UTC", 2025, 11, 21, 7, 15) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(16 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/tv_trades.csv new file mode 100644 index 00000000..44582680 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-coof-1121/tv_trades.csv @@ -0,0 +1,3 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-11-21 15:30,Margin call,2642.65,5.806,15998.20076,-654.97485,-4.09,0,31.46852,0.20,-654.97485,-4.09,-654.97485,-65.50,0 +1,Entry long,2025-11-21 15:30,L,2755.46,5.806,15998.20076,-654.97485,-4.09,0,31.46852,0.20,-654.97485,-4.09,-654.97485,-65.50,0 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/meta.json new file mode 100644 index 00000000..ffdba4bd --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-mag-0621", + "slug": "pm2-m7-mag-0621", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:22.422Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-19", + "to": "2025-06-23" + }, + "pine_sha256": "c811e8b2435cb1e25c51c89c3d49382b7606f34f5d2e74031cf5be4248b10f2e", + "tv_trades_sha256": "82afdec0f73135dfa1af943f73db2cd6ee01a1fa2125935355b5a65cc7e85ca0", + "tv_trades_rows": 2 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/metrics.json new file mode 100644 index 00000000..b6144ea0 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-mag-0621", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-19", + "to": "2025-06-23", + "deepBacktesting": true, + "trades": 1, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "c811e8b2435cb1e25c51c89c3d49382b7606f34f5d2e74031cf5be4248b10f2e", + "tvTradesCsvHash": "82afdec0f73135dfa1af943f73db2cd6ee01a1fa2125935355b5a65cc7e85ca0", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75267", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1750291200000, + "to": 1750636800000 + }, + "returnedRange": { + "from": 1750291200000, + "to": 1750636800000 + }, + "rangeProof": "covered", + "rawFrameSha256": "e38977eb83b022554ddd80a081dba6020a16ef65cb5541e2be2f2c18734cae31", + "decodedReportSha256": "b1c2d1f0e7056db97a179749db7edb3bd9c3de9ab8f25698d69d718bd53ad347", + "closedTrades": 1, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:22.421Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/strategy.pine new file mode 100644 index 00000000..a0bb5f3c --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7 under the bar magnifier): a leveraged long (margin 5, 16 x equity) +// placed on the signal bar 2025-06-21 21:15 UTC, filled at the next bar's open, whose 1-minute lows breach. +// Magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-mag-0621", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 6, 21, 21, 15) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(16 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/tv_trades.csv new file mode 100644 index 00000000..66689658 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0621/tv_trades.csv @@ -0,0 +1,3 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-22 05:30,Margin call,2230,6.842,15998.92228,-741.26227,-4.63,0,2.32628,0.01,-741.26227,-4.63,-741.26227,-74.13,0 +1,Entry long,2025-06-22 05:30,L,2338.34,6.842,15998.92228,-741.26227,-4.63,0,2.32628,0.01,-741.26227,-4.63,-741.26227,-74.13,0 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/meta.json new file mode 100644 index 00000000..61da5dcd --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-mag-0922", + "slug": "pm2-m7-mag-0922", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:26.322Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-09-20", + "to": "2025-09-24" + }, + "pine_sha256": "58a917a3f2861200e216817504c4dd81984f34ba6cdd888fd701e1935ad22dd5", + "tv_trades_sha256": "5c3468e8176b5aadbd545b2e4c0ef20ac85dcaf7151a54f8d6854397fdf51e99", + "tv_trades_rows": 2 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/metrics.json new file mode 100644 index 00000000..cf431e50 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-mag-0922", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-09-20", + "to": "2025-09-24", + "deepBacktesting": true, + "trades": 1, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "58a917a3f2861200e216817504c4dd81984f34ba6cdd888fd701e1935ad22dd5", + "tvTradesCsvHash": "5c3468e8176b5aadbd545b2e4c0ef20ac85dcaf7151a54f8d6854397fdf51e99", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75268", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1758326400000, + "to": 1758672000000 + }, + "returnedRange": { + "from": 1758326400000, + "to": 1758672000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "84344b2a963547a75a4b8eb83be1381c1e6fc8c9d8e2360906821ede00ed5957", + "decodedReportSha256": "ebb0ff995e43783a73de320bbc564498ff07ff3e64d9c2f6f513e702c6e34f60", + "closedTrades": 1, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:26.321Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/strategy.pine new file mode 100644 index 00000000..0b5a6247 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7 under the bar magnifier): a leveraged long (margin 5, 16 x equity) +// placed on the signal bar 2025-09-22 05:45 UTC, filled at the next bar's open, whose 1-minute lows breach. +// Magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-mag-0922", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 9, 22, 5, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(16 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/tv_trades.csv new file mode 100644 index 00000000..40222b79 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-0922/tv_trades.csv @@ -0,0 +1,3 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-09-22 14:00,Margin call,4076.79,3.793,15997.7361,-534.4716,-3.34,0,26.51307,0.17,-534.4716,-3.34,-534.4716,-53.45,0 +1,Entry long,2025-09-22 14:00,L,4217.7,3.793,15997.7361,-534.4716,-3.34,0,26.51307,0.17,-534.4716,-3.34,-534.4716,-53.45,0 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/meta.json new file mode 100644 index 00000000..007f8d61 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-mag-1010", + "slug": "pm2-m7-mag-1010", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:30.700Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-10-08", + "to": "2025-10-12" + }, + "pine_sha256": "bfcb1e965b8867c3b6d2ce57cd6d9bb87d2defb6b499e2c948eda398f2e579fc", + "tv_trades_sha256": "bbac24deb07eed256ff449d0a9eece7755b85d9af4390f8a56d1dc1fd9423d04", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/metrics.json new file mode 100644 index 00000000..8fa0116f --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-mag-1010", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-10-08", + "to": "2025-10-12", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "bfcb1e965b8867c3b6d2ce57cd6d9bb87d2defb6b499e2c948eda398f2e579fc", + "tvTradesCsvHash": "bbac24deb07eed256ff449d0a9eece7755b85d9af4390f8a56d1dc1fd9423d04", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75269", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1759881600000, + "to": 1760227200000 + }, + "returnedRange": { + "from": 1759881600000, + "to": 1760227200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "0a1a6086138594227fe6a0af581db39a209a497ad6a34cd22d6701a7e98d27c5", + "decodedReportSha256": "3d0ce4e88579d98a6df8bd0ca3424e6d0e98f5dd451e22618128b8d41c5a3dec", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:30.699Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/strategy.pine new file mode 100644 index 00000000..949073a4 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7 under the bar magnifier): a leveraged long (margin 20, 4 x equity) +// placed on the signal bar 2025-10-10 21:00 UTC, filled at the next bar's open, whose 1-minute lows breach. +// Magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-mag-1010", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 10, 10, 21, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/tv_trades.csv new file mode 100644 index 00000000..2cd47d59 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1010/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-10-11 05:15,Margin call,3428.11,0.3096,1152.65628,-91.31342,-7.92,0,20.687471,1.79,-91.31342,-7.92,-91.31342,-9.13,0 +1,Entry long,2025-10-11 05:15,L,3723.05,0.3096,1152.65628,-91.31342,-7.92,0,20.687471,1.79,-91.31342,-7.92,-91.31342,-9.13,0 +2,Exit long,2025-10-11 06:15,timeout,3895,0.7644,2845.89942,131.43858,4.62,0,196.93237,6.92,-246.93942,-8.68,40.125156,4.01,4 +2,Entry long,2025-10-11 05:15,L,3723.05,0.7644,2845.89942,131.43858,4.62,0,196.93237,6.92,-246.93942,-8.68,40.125156,4.01,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/meta.json new file mode 100644 index 00000000..52dcbac9 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-mag-1121", + "slug": "pm2-m7-mag-1121", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:34.573Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-11-19", + "to": "2025-11-23" + }, + "pine_sha256": "2807b5124369a1814bba54deba23070585511ba3428aa8e6ad4fcc8f3a2f67fc", + "tv_trades_sha256": "d2ebdc4cff50a62ccc7809a1cc4cd58bf3d32f5a9ebbfdd60e118e6786e3251e", + "tv_trades_rows": 2 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/metrics.json new file mode 100644 index 00000000..1d9c585e --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-mag-1121", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-11-19", + "to": "2025-11-23", + "deepBacktesting": true, + "trades": 1, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "2807b5124369a1814bba54deba23070585511ba3428aa8e6ad4fcc8f3a2f67fc", + "tvTradesCsvHash": "d2ebdc4cff50a62ccc7809a1cc4cd58bf3d32f5a9ebbfdd60e118e6786e3251e", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75270", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1763510400000, + "to": 1763856000000 + }, + "returnedRange": { + "from": 1763510400000, + "to": 1763856000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "c08a98e904ccf93d5329cb50b9cf215626259c5c1ec081357aa3456652b9f7cb", + "decodedReportSha256": "a6099144010bf66c60bf619b85c99472c12f932b6a1c9adad7e0729df7e5b376", + "closedTrades": 1, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:34.572Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/strategy.pine new file mode 100644 index 00000000..d93bdcb2 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7 under the bar magnifier): a leveraged long (margin 5, 16 x equity) +// placed on the signal bar 2025-11-21 07:15 UTC, filled at the next bar's open, whose 1-minute lows breach. +// Magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-mag-1121", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 11, 21, 7, 15) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(16 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/tv_trades.csv new file mode 100644 index 00000000..6cfbbbac --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-mag-1121/tv_trades.csv @@ -0,0 +1,3 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-11-21 15:30,Margin call,2680.37,5.806,15998.20076,-435.97253,-2.73,0,31.46852,0.20,-435.97253,-2.73,-435.97253,-43.60,0 +1,Entry long,2025-11-21 15:30,L,2755.46,5.806,15998.20076,-435.97253,-2.73,0,31.46852,0.20,-435.97253,-2.73,-435.97253,-43.60,0 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/meta.json new file mode 100644 index 00000000..42967756 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-poocl-0402-1945", + "slug": "pm2-m7-poocl-0402-1945", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:38.671Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05" + }, + "pine_sha256": "13d12069e5ea2c83087ed228460d971e0246c5805d710496290e5f36cc1587dc", + "tv_trades_sha256": "e7852d9c3904147c6c4dea9f4e5b5c08458eb42b3a3c6fc77b0027f1eb846974", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/metrics.json new file mode 100644 index 00000000..d325d87b --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-poocl-0402-1945", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "13d12069e5ea2c83087ed228460d971e0246c5805d710496290e5f36cc1587dc", + "tvTradesCsvHash": "e7852d9c3904147c6c4dea9f4e5b5c08458eb42b3a3c6fc77b0027f1eb846974", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75271", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "returnedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "aa181ac0bab99fa0a44b3782eb82d551c8847b927486a017fcd6a4ecbb87f7c7", + "decodedReportSha256": "19a2c69bdb7c7a14ccfb286a4192e79458153877b6510e65ff80a603db86bb24", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:38.670Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/strategy.pine new file mode 100644 index 00000000..3a004127 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (M7, a limit opening under process_orders_on_close): a leveraged long LIMIT +// entry (margin 5, 17.93 x equity, limit 0.997 x close) placed on the signal bar 2025-04-02 19:45 UTC and +// filled on the next bar on its way down; that bar's low then breaches and its close recovers. +// process_orders_on_close ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-poocl-0402-1945", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=true) +if time == timestamp("UTC", 2025, 4, 2, 19, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(17.93 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 0.997, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/tv_trades.csv new file mode 100644 index 00000000..824a13ec --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-1945/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-03 04:00,Margin call,1881,4.516,8602.21228,-107.61628,-1.25,0,0,0.00,-107.61628,-1.25,-107.61628,-10.76,0 +1,Entry long,2025-04-03 04:00,L,1904.83,4.516,8602.21228,-107.61628,-1.25,0,0,0.00,-107.61628,-1.25,-107.61628,-10.76,0 +2,Exit long,2025-04-03 04:45,timeout,1880,4.868,9272.71244,-120.87244,-1.30,0,258.68552,2.79,-193.89244,-2.09,-228.48872,-22.85,3 +2,Entry long,2025-04-03 04:00,L,1904.83,4.868,9272.71244,-120.87244,-1.30,0,258.68552,2.79,-193.89244,-2.09,-228.48872,-22.85,3 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/meta.json new file mode 100644 index 00000000..7b3b284d --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-poocl-0402-2000", + "slug": "pm2-m7-poocl-0402-2000", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:42.722Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05" + }, + "pine_sha256": "52c0eb5a2451844c835f094b65b782d18189b551bee9969ac5b9aff7bc9ad5b7", + "tv_trades_sha256": "0d4698a024f7cbf8ff19a2558ff06ec09b8421021a51c9dd0c24215e52c548a3", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/metrics.json new file mode 100644 index 00000000..b8f424fe --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-poocl-0402-2000", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "52c0eb5a2451844c835f094b65b782d18189b551bee9969ac5b9aff7bc9ad5b7", + "tvTradesCsvHash": "0d4698a024f7cbf8ff19a2558ff06ec09b8421021a51c9dd0c24215e52c548a3", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75272", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "returnedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "3b111237b1ae8ed19de2b082643aa386c3a768e71c8b00680bdc58e245b336a5", + "decodedReportSha256": "1f603cc28965ca7a789a7fff5d6de32b66a35714109fad06d217bebadc3d0e66", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:42.722Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/strategy.pine new file mode 100644 index 00000000..907c8411 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (M7, a limit opening under process_orders_on_close): a leveraged long LIMIT +// entry (margin 5, 14.17 x equity, limit 0.997 x close) placed on the signal bar 2025-04-02 20:00 UTC and +// filled on the next bar on its way down; that bar's low then breaches and its close recovers. +// process_orders_on_close ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-poocl-0402-2000", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=true) +if time == timestamp("UTC", 2025, 4, 2, 20, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(14.17 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 0.997, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/tv_trades.csv new file mode 100644 index 00000000..23d8ba3d --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0402-2000/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-03 04:15,Margin call,1872.48,3.7668,7259.791308,-206.53365,-2.84,0,0,0.00,-206.53365,-2.84,-206.53365,-20.65,0 +1,Entry long,2025-04-03 04:15,L,1927.31,3.7668,7259.791308,-206.53365,-2.84,0,0,0.00,-206.53365,-2.84,-206.53365,-20.65,0 +2,Exit long,2025-04-03 05:00,timeout,1859.77,3.5632,6867.390992,-240.65852,-3.50,0,0,0.00,-261.46762,-3.81,-447.19217,-44.72,3 +2,Entry long,2025-04-03 04:15,L,1927.31,3.5632,6867.390992,-240.65852,-3.50,0,0,0.00,-261.46762,-3.81,-447.19217,-44.72,3 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/meta.json new file mode 100644 index 00000000..61b12a57 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-poocl-0801-0030", + "slug": "pm2-m7-poocl-0801-0030", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:46.605Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-29", + "to": "2025-08-04" + }, + "pine_sha256": "9d772a370542a272ea2f070fb3065516941ac9730a68eb896ac58905d204cbef", + "tv_trades_sha256": "816a9c1fda6dff83a08796c68eb094bb33d4684e0b968bff207ee990e73f6021", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/metrics.json new file mode 100644 index 00000000..7f0a5ea1 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-poocl-0801-0030", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-29", + "to": "2025-08-04", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "9d772a370542a272ea2f070fb3065516941ac9730a68eb896ac58905d204cbef", + "tvTradesCsvHash": "816a9c1fda6dff83a08796c68eb094bb33d4684e0b968bff207ee990e73f6021", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75273", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1753747200000, + "to": 1754265600000 + }, + "returnedRange": { + "from": 1753747200000, + "to": 1754265600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "a0994897548a008ffd1ed5c2a0876f5599a9ba308a0069e20819130d42009fcf", + "decodedReportSha256": "33cfe4ce93896fcbaded29426bcf2fa47801d38ddbe9fda720ed91ac2f580be1", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:46.604Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/strategy.pine new file mode 100644 index 00000000..36347696 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (M7, a limit opening under process_orders_on_close): a leveraged long LIMIT +// entry (margin 5, 18.055 x equity, limit 0.997 x close) placed on the signal bar 2025-08-01 00:30 UTC and +// filled on the next bar on its way down; that bar's low then breaches and its close recovers. +// process_orders_on_close ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-poocl-0801-0030", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=true) +if time == timestamp("UTC", 2025, 8, 1, 0, 30) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(18.055 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 0.997, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/tv_trades.csv new file mode 100644 index 00000000..15af0864 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-0801-0030/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-08-01 08:45,Margin call,3613.67,2.22,8117.4966,-95.1492,-1.17,0,0,0.00,-95.1492,-1.17,-95.1492,-9.51,0 +1,Entry long,2025-08-01 08:45,L,3656.53,2.22,8117.4966,-95.1492,-1.17,0,0,0.00,-95.1492,-1.17,-95.1492,-9.51,0 +2,Exit long,2025-08-01 09:30,timeout,3690.36,2.702,9879.94406,91.40866,0.93,0,103.94594,1.05,-115.80772,-1.17,-3.74054,-0.37,3 +2,Entry long,2025-08-01 08:45,L,3656.53,2.702,9879.94406,91.40866,0.93,0,103.94594,1.05,-115.80772,-1.17,-3.74054,-0.37,3 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/meta.json new file mode 100644 index 00000000..0f00fba1 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-poocl-1201-0215", + "slug": "pm2-m7-poocl-1201-0215", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:50.364Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-11-28", + "to": "2025-12-04" + }, + "pine_sha256": "17912e6bccf06952c69f35c5277ceb25921425628990e0cf193b9a612589077a", + "tv_trades_sha256": "b6332c9af3571acfe98a67256ac3ee2f91f7be0aae8538f939631ae192816e94", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/metrics.json new file mode 100644 index 00000000..7594d251 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-poocl-1201-0215", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-11-28", + "to": "2025-12-04", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "17912e6bccf06952c69f35c5277ceb25921425628990e0cf193b9a612589077a", + "tvTradesCsvHash": "b6332c9af3571acfe98a67256ac3ee2f91f7be0aae8538f939631ae192816e94", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75274", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1764288000000, + "to": 1764806400000 + }, + "returnedRange": { + "from": 1764288000000, + "to": 1764806400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "b1a0b8cfbda723c4d4b0485ff929ee8c4b82eb02584598260193580f3034cf18", + "decodedReportSha256": "3b5c523707c627f5629a8aafb4eef4b47c5dabc682f9e3aaf2a66e55998abc80", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:50.363Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/strategy.pine new file mode 100644 index 00000000..2bea17eb --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (M7, a limit opening under process_orders_on_close): a leveraged long LIMIT +// entry (margin 5, 18.855 x equity, limit 0.997 x close) placed on the signal bar 2025-12-01 02:15 UTC and +// filled on the next bar on its way down; that bar's low then breaches and its close recovers. +// process_orders_on_close ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-poocl-1201-0215", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=true) +if time == timestamp("UTC", 2025, 12, 1, 2, 15) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(18.855 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 0.997, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/tv_trades.csv new file mode 100644 index 00000000..7e1b4a5e --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocl-1201-0215/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-12-01 10:30,Margin call,2824.6,1.7024,4841.2,-32.60096,-0.67,0,0,0.00,-32.60096,-0.67,-32.60096,-3.26,0 +1,Entry long,2025-12-01 10:30,L,2843.75,1.7024,4841.2,-32.60096,-0.67,0,0,0.00,-32.60096,-0.67,-32.60096,-3.26,0 +2,Exit long,2025-12-01 11:15,timeout,2826.4,4.9076,13955.9875,-85.14686,-0.61,0,37.248684,0.27,-152.08652,-1.09,-117.74782,-11.77,3 +2,Entry long,2025-12-01 10:30,L,2843.75,4.9076,13955.9875,-85.14686,-0.61,0,37.248684,0.27,-152.08652,-1.09,-117.74782,-11.77,3 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/meta.json new file mode 100644 index 00000000..5f457169 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-poocm-0105-1415", + "slug": "pm2-m7-poocm-0105-1415", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:54.608Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-02", + "to": "2026-01-08" + }, + "pine_sha256": "e9f4966f8e81d2b9a2c5e68034fa6786dd9f473d6f6397a614952a90e23a5a96", + "tv_trades_sha256": "b0ae1e3b89dcf62c12301bff10c574a36006e611cb2feb2a4fad525511c314c8", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/metrics.json new file mode 100644 index 00000000..1bd6f1a5 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-poocm-0105-1415", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-02", + "to": "2026-01-08", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "e9f4966f8e81d2b9a2c5e68034fa6786dd9f473d6f6397a614952a90e23a5a96", + "tvTradesCsvHash": "b0ae1e3b89dcf62c12301bff10c574a36006e611cb2feb2a4fad525511c314c8", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75275", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1767312000000, + "to": 1767830400000 + }, + "returnedRange": { + "from": 1767312000000, + "to": 1767830400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "89e507d078a77a4fdf51681ac969ee62518b283b6831ec5803088666f44e1f1d", + "decodedReportSha256": "7c62c76b521491801d0458dd05e84aa2bfc867a76297daadb4297adfb8410cac", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:54.607Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/strategy.pine new file mode 100644 index 00000000..c5d59e57 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7 under process_orders_on_close, market): a leveraged long (margin 5, 18.835 x equity) +// placed and filled at the close of the signal bar 2026-01-05 14:15 UTC; the next bar's low breaches the +// maintenance line and its close recovers. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-poocm-0105-1415", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=true) +if time == timestamp("UTC", 2026, 1, 5, 14, 15) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(18.835 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/tv_trades.csv new file mode 100644 index 00000000..c1f8d991 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0105-1415/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2026-01-05 22:30,Margin call,3132.58,1.496,4717.15728,-30.8176,-0.65,0,26.13512,0.55,-30.8176,-0.65,-30.8176,-3.08,1 +1,Entry long,2026-01-05 22:15,L,3153.18,1.496,4717.15728,-30.8176,-0.65,0,26.13512,0.55,-30.8176,-0.65,-30.8176,-3.08,1 +2,Exit long,2026-01-05 23:00,timeout,3161.08,4.477,14116.78686,35.3683,0.25,0,104.40364,0.74,-92.2262,-0.65,4.5507,0.46,3 +2,Entry long,2026-01-05 22:15,L,3153.18,4.477,14116.78686,35.3683,0.25,0,104.40364,0.74,-92.2262,-0.65,4.5507,0.46,3 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/meta.json new file mode 100644 index 00000000..582cd594 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-poocm-0402-1945", + "slug": "pm2-m7-poocm-0402-1945", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:40:59.500Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05" + }, + "pine_sha256": "bdf9318ad0c727bc94be5d728bfde18446adfce1c291f68dac477f58ce1010fa", + "tv_trades_sha256": "b7e708608a5cea3777433c0f6bd64f790e1a5993833dccb1b3f90720910a9d56", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/metrics.json new file mode 100644 index 00000000..cf153b76 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-poocm-0402-1945", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "bdf9318ad0c727bc94be5d728bfde18446adfce1c291f68dac477f58ce1010fa", + "tvTradesCsvHash": "b7e708608a5cea3777433c0f6bd64f790e1a5993833dccb1b3f90720910a9d56", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75276", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "returnedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f4583d366adac6c12dedb7401cc1545a6121ec91ab744594a056d0f864181c19", + "decodedReportSha256": "3f2856b42351bba014215626b06e0c52dd1c86f4168eca78735bf4e431b0f5d1", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:40:59.499Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/strategy.pine new file mode 100644 index 00000000..f791e43b --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7 under process_orders_on_close, market): a leveraged long (margin 5, 17.44 x equity) +// placed and filled at the close of the signal bar 2025-04-02 19:45 UTC; the next bar's low breaches the +// maintenance line and its close recovers. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-poocm-0402-1945", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=true) +if time == timestamp("UTC", 2025, 4, 2, 19, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(17.44 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/tv_trades.csv new file mode 100644 index 00000000..101028ba --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0402-1945/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-03 04:00,Margin call,1881,5.4568,10425.543808,-161.30301,-1.55,0,0.163704,0.00,-161.30301,-1.55,-161.30301,-16.13,1 +1,Entry long,2025-04-03 03:45,L,1910.56,5.4568,10425.543808,-161.30301,-1.55,0,0.163704,0.00,-161.30301,-1.55,-161.30301,-16.13,1 +2,Exit long,2025-04-03 04:30,timeout,1870.06,3.6712,7014.047872,-148.6836,-2.12,0,174.05159,2.48,-167.25987,-2.38,-309.9866,-31.00,3 +2,Entry long,2025-04-03 03:45,L,1910.56,3.6712,7014.047872,-148.6836,-2.12,0,174.05159,2.48,-167.25987,-2.38,-309.9866,-31.00,3 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/meta.json new file mode 100644 index 00000000..13280ef3 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-poocm-0702-0100", + "slug": "pm2-m7-poocm-0702-0100", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:41:03.702Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-29", + "to": "2025-07-05" + }, + "pine_sha256": "4b2f754a2d9b7155df0d65ba03b427781613d2664b06c1025e5528f951f0ef4e", + "tv_trades_sha256": "78ad71bf244a719e326979fe4a26c763b6bdb72c61853939e0c34d92e0a8d7d5", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/metrics.json new file mode 100644 index 00000000..e88342f0 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-poocm-0702-0100", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-29", + "to": "2025-07-05", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "4b2f754a2d9b7155df0d65ba03b427781613d2664b06c1025e5528f951f0ef4e", + "tvTradesCsvHash": "78ad71bf244a719e326979fe4a26c763b6bdb72c61853939e0c34d92e0a8d7d5", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75277", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1751155200000, + "to": 1751673600000 + }, + "returnedRange": { + "from": 1751155200000, + "to": 1751673600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "ccd3c6792a612e30231fbf1d99df4eebb0b52bbe84d5cc756fe2b9b31fb993d2", + "decodedReportSha256": "56a960a2baeb0fbb6bdac42a3f7c42667a561e61ddeab5819b3ed23ad37d5af6", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:41:03.701Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/strategy.pine new file mode 100644 index 00000000..af5403d6 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7 under process_orders_on_close, market): a leveraged long (margin 5, 17.66 x equity) +// placed and filled at the close of the signal bar 2025-07-02 01:00 UTC; the next bar's low breaches the +// maintenance line and its close recovers. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-poocm-0702-0100", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=true) +if time == timestamp("UTC", 2025, 7, 2, 1, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(17.66 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/tv_trades.csv new file mode 100644 index 00000000..4cc719d6 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-0702-0100/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 09:15,Margin call,2367.53,3.9452,9472.385748,-132.0064,-1.39,0,16.924908,0.18,-132.0064,-1.39,-132.0064,-13.20,1 +1,Entry long,2025-07-02 09:00,L,2400.99,3.9452,9472.385748,-132.0064,-1.39,0,16.924908,0.18,-132.0064,-1.39,-132.0064,-13.20,1 +2,Exit long,2025-07-02 09:45,timeout,2410.03,3.4098,8186.895702,30.824593,0.38,0,32.69998,0.40,-114.09191,-1.39,-101.1818,-10.12,3 +2,Entry long,2025-07-02 09:00,L,2400.99,3.4098,8186.895702,30.824593,0.38,0,32.69998,0.40,-114.09191,-1.39,-101.1818,-10.12,3 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/meta.json new file mode 100644 index 00000000..a46f5d11 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7-poocm-1002-1445", + "slug": "pm2-m7-poocm-1002-1445", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:41:07.446Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-09-29", + "to": "2025-10-05" + }, + "pine_sha256": "d1b8bb7215855919aa11562b8d557fc990093904dd8052518b17c8605eeaa252", + "tv_trades_sha256": "3c75ca7c5cd23668176bd01e84fe8cf6743b766414bee3a92f65d67ee173203f", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/metrics.json new file mode 100644 index 00000000..71a642ad --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7-poocm-1002-1445", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-09-29", + "to": "2025-10-05", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "d1b8bb7215855919aa11562b8d557fc990093904dd8052518b17c8605eeaa252", + "tvTradesCsvHash": "3c75ca7c5cd23668176bd01e84fe8cf6743b766414bee3a92f65d67ee173203f", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75278", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1759104000000, + "to": 1759622400000 + }, + "returnedRange": { + "from": 1759104000000, + "to": 1759622400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "41d2dca260757b24ea0ab3238b88a824d29fce668fdcf1e807c57f2b37e89cbc", + "decodedReportSha256": "692091d6e41e5742f87988ff3b5217e33b2af20b3fb7dd4657ef3a3264efc0bb", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:41:07.445Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/strategy.pine new file mode 100644 index 00000000..fd2174d0 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/strategy.pine @@ -0,0 +1,12 @@ +// R5 lane PAR-MARGIN-2 (M7 under process_orders_on_close, market): a leveraged long (margin 5, 18.39 x equity) +// placed and filled at the close of the signal bar 2025-10-02 14:45 UTC; the next bar's low breaches the +// maintenance line and its close recovers. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-m7-poocm-1002-1445", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=true) +if time == timestamp("UTC", 2025, 10, 2, 14, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(18.39 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/tv_trades.csv new file mode 100644 index 00000000..724be69e --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7-poocm-1002-1445/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-10-02 23:00,Margin call,4332.23,1.4844,6490.61322,-59.85101,-0.92,0,32.315388,0.50,-59.85101,-0.92,-59.85101,-5.99,1 +1,Entry long,2025-10-02 22:45,L,4372.55,1.4844,6490.61322,-59.85101,-0.92,0,32.315388,0.50,-59.85101,-0.92,-59.85101,-5.99,1 +2,Exit long,2025-10-02 23:30,timeout,4408.26,2.7206,11895.95953,97.152626,0.82,0,123.54244,1.04,-109.694595,-0.92,37.301617,3.73,3 +2,Entry long,2025-10-02 22:45,L,4372.55,2.7206,11895.95953,97.152626,0.82,0,123.54244,1.04,-109.694595,-0.92,37.301617,3.73,3 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/meta.json new file mode 100644 index 00000000..f03c5b9c --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7b-lim-0402-1930", + "slug": "pm2-m7b-lim-0402-1930", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:41:11.232Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05" + }, + "pine_sha256": "f997f7b12e3e45aa05b36c65e5d51368bea9254a03ff3e5d048c29ba47454614", + "tv_trades_sha256": "e6aaeb8a522871f633a6c7af9bdfd8e732c712bd3bccaffc554a078d5f9e7ed9", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/metrics.json new file mode 100644 index 00000000..08390c55 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7b-lim-0402-1930", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "f997f7b12e3e45aa05b36c65e5d51368bea9254a03ff3e5d048c29ba47454614", + "tvTradesCsvHash": "e6aaeb8a522871f633a6c7af9bdfd8e732c712bd3bccaffc554a078d5f9e7ed9", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75279", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "returnedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "bfaea7c72e40457071d886b66bc4ec1b47f9edac8286f6085a05ca8f8a54efe1", + "decodedReportSha256": "119e21d32dd5ac06f527e9234ca4c563a8848c73696bb960e136f3c0d594fe25", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:41:11.231Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/strategy.pine new file mode 100644 index 00000000..7767674f --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN-2 (M7-B, a limit add): a carried long L1 (margin 5, 4 x equity) and a LIMIT add L2 +// (13.75 x equity, limit 0.997 x close) placed on L1's entry bar 2025-04-02 19:45 UTC and filled on the next bar +// on its way down; that bar's low breaches the combined line (L1 alone never does), its close recovers. +// Timeout close_all 4 bars after L1's entry bar. +//@version=6 +strategy("pm2-m7b-lim-0402-1930", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=2, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 4, 2, 19, 30) and strategy.position_size == 0 + strategy.entry("L1", strategy.long, qty = math.floor(4 * strategy.equity / close * 1000) / 1000) +if time == timestamp("UTC", 2025, 4, 2, 19, 45) and strategy.position_size > 0 + strategy.entry("L2", strategy.long, qty = math.floor(13.75 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 0.997, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 4 + strategy.close_all(comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/tv_trades.csv new file mode 100644 index 00000000..de1ade34 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1930/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-03 04:00,Margin call,1881,2.099,3999.11975,-50.90075,-1.27,0,21.05297,0.53,-50.90075,-1.27,-50.90075,-5.09,1 +1,Entry long,2025-04-03 03:45,L1,1905.25,2.099,3999.11975,-50.90075,-1.27,0,21.05297,0.53,-50.90075,-1.27,-50.90075,-5.09,1 +2,Exit long,2025-04-03 04:00,Margin call,1881,2.4142,4598.640586,-57.530388,-1.25,0,0,0.00,-57.530388,-1.25,-108.43114,-10.84,0 +2,Entry long,2025-04-03 04:00,L2,1904.83,2.4142,4598.640586,-57.530388,-1.25,0,0,0.00,-57.530388,-1.25,-108.43114,-10.84,0 +3,Exit long,2025-04-03 05:15,timeout,1859.76,4.8628,9262.807324,-219.1664,-2.37,0,258.40918,2.79,-247.51653,-2.67,-327.59753,-32.76,5 +3,Entry long,2025-04-03 04:00,L2,1904.83,4.8628,9262.807324,-219.1664,-2.37,0,258.40918,2.79,-247.51653,-2.67,-327.59753,-32.76,5 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/meta.json new file mode 100644 index 00000000..f7df170e --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7b-lim-0402-1945", + "slug": "pm2-m7b-lim-0402-1945", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:41:14.937Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05" + }, + "pine_sha256": "818c08cc5879b34d304a8641a855dac774983121b757127eb1cc5d7ab4551513", + "tv_trades_sha256": "785ea45bf94a33ee1bdc7d1f8303d60e0de26d8703d26d4f68cf399e8de1075a", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/metrics.json new file mode 100644 index 00000000..3b625faa --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7b-lim-0402-1945", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "818c08cc5879b34d304a8641a855dac774983121b757127eb1cc5d7ab4551513", + "tvTradesCsvHash": "785ea45bf94a33ee1bdc7d1f8303d60e0de26d8703d26d4f68cf399e8de1075a", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75280", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "returnedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f243fa202fbe89faeb9d6f3b3b228c9ca9c12c31be8a5820bb847ce91c14f5ac", + "decodedReportSha256": "0e38c494ef3fb188fedbb3f75ea1c6812433a0ee68c2923a92cb5cfe422b54ce", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:41:14.936Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/strategy.pine new file mode 100644 index 00000000..5bdbe7c0 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN-2 (M7-B, a limit add): a carried long L1 (margin 5, 4 x equity) and a LIMIT add L2 +// (10.15 x equity, limit 0.997 x close) placed on L1's entry bar 2025-04-02 20:00 UTC and filled on the next bar +// on its way down; that bar's low breaches the combined line (L1 alone never does), its close recovers. +// Timeout close_all 4 bars after L1's entry bar. +//@version=6 +strategy("pm2-m7b-lim-0402-1945", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=2, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 4, 2, 19, 45) and strategy.position_size == 0 + strategy.entry("L1", strategy.long, qty = math.floor(4 * strategy.equity / close * 1000) / 1000) +if time == timestamp("UTC", 2025, 4, 2, 20, 0) and strategy.position_size > 0 + strategy.entry("L2", strategy.long, qty = math.floor(10.15 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 0.997, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 4 + strategy.close_all(comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/tv_trades.csv new file mode 100644 index 00000000..21ba6f7b --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0402-1945/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-03 04:15,Margin call,1872.48,2.093,3998.86487,-79.76423,-1.99,0,99.16634,2.48,-79.76423,-1.99,-79.76423,-7.98,1 +1,Entry long,2025-04-03 04:00,L1,1910.59,2.093,3998.86487,-79.76423,-1.99,0,99.16634,2.48,-79.76423,-1.99,-79.76423,-7.98,1 +2,Exit long,2025-04-03 04:15,Margin call,1872.48,1.8338,3534.301078,-100.54726,-2.84,0,0,0.00,-100.54726,-2.84,-180.31148,-18.03,0 +2,Entry long,2025-04-03 04:15,L2,1927.31,1.8338,3534.301078,-100.54726,-2.84,0,0,0.00,-100.54726,-2.84,-180.31148,-18.03,0 +3,Exit long,2025-04-03 05:30,timeout,1862.71,3.6642,7062.049302,-236.70732,-3.35,0,0,0.00,-268.879,-3.81,-417.0188,-41.70,5 +3,Entry long,2025-04-03 04:15,L2,1927.31,3.6642,7062.049302,-236.70732,-3.35,0,0,0.00,-268.879,-3.81,-417.0188,-41.70,5 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/meta.json new file mode 100644 index 00000000..202d0b8c --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7b-lim-0801-0015", + "slug": "pm2-m7b-lim-0801-0015", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:41:18.955Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-29", + "to": "2025-08-04" + }, + "pine_sha256": "b34d6fb8cc1971728c7f432497aaeb600d51722653233cd7d9db1b7b750de05f", + "tv_trades_sha256": "0fcd4f3a0df2f3cb325a965c5837dc19e608fa1ae07bd30520599d2379ddda18", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/metrics.json new file mode 100644 index 00000000..90387956 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7b-lim-0801-0015", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-29", + "to": "2025-08-04", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "b34d6fb8cc1971728c7f432497aaeb600d51722653233cd7d9db1b7b750de05f", + "tvTradesCsvHash": "0fcd4f3a0df2f3cb325a965c5837dc19e608fa1ae07bd30520599d2379ddda18", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75281", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1753747200000, + "to": 1754265600000 + }, + "returnedRange": { + "from": 1753747200000, + "to": 1754265600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "0364cf8acec5961dc062dc1d0f4ad38d259c6eb983fab372a211775d111fd706", + "decodedReportSha256": "c155e7b8adb3fdf232787e9e5df6d4de88765bc8ef6813753a9f1325ef0706b8", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:41:18.953Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/strategy.pine new file mode 100644 index 00000000..7d2ac74a --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN-2 (M7-B, a limit add): a carried long L1 (margin 5, 4 x equity) and a LIMIT add L2 +// (13.75 x equity, limit 0.997 x close) placed on L1's entry bar 2025-08-01 00:30 UTC and filled on the next bar +// on its way down; that bar's low breaches the combined line (L1 alone never does), its close recovers. +// Timeout close_all 4 bars after L1's entry bar. +//@version=6 +strategy("pm2-m7b-lim-0801-0015", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=2, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 8, 1, 0, 15) and strategy.position_size == 0 + strategy.entry("L1", strategy.long, qty = math.floor(4 * strategy.equity / close * 1000) / 1000) +if time == timestamp("UTC", 2025, 8, 1, 0, 30) and strategy.position_size > 0 + strategy.entry("L2", strategy.long, qty = math.floor(13.75 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 0.997, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 4 + strategy.close_all(comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/tv_trades.csv new file mode 100644 index 00000000..bd09ef52 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-0801-0015/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-08-01 08:45,Margin call,3613.67,1.084,3998.56164,-81.34336,-2.03,0,2.0596,0.05,-81.34336,-2.03,-81.34336,-8.13,1 +1,Entry long,2025-08-01 08:30,L1,3688.71,1.084,3998.56164,-81.34336,-2.03,0,2.0596,0.05,-81.34336,-2.03,-81.34336,-8.13,1 +2,Exit long,2025-08-01 08:45,Margin call,3613.67,1.042,3810.10426,-44.66012,-1.17,0,0,0.00,-44.66012,-1.17,-126.00348,-12.60,0 +2,Entry long,2025-08-01 08:45,L2,3656.53,1.042,3810.10426,-44.66012,-1.17,0,0,0.00,-44.66012,-1.17,-126.00348,-12.60,0 +3,Exit long,2025-08-01 10:00,timeout,3687.11,2.621,9583.76513,80.15018,0.84,0,100.82987,1.05,-112.33606,-1.17,-45.8533,-4.59,5 +3,Entry long,2025-08-01 08:45,L2,3656.53,2.621,9583.76513,80.15018,0.84,0,100.82987,1.05,-112.33606,-1.17,-45.8533,-4.59,5 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/meta.json new file mode 100644 index 00000000..f93f08f6 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7b-lim-1207-1345", + "slug": "pm2-m7b-lim-1207-1345", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:41:23.191Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-12-04", + "to": "2025-12-10" + }, + "pine_sha256": "cc0960643e18eb2af55dfbc58fa6abda524c7407febe62e38dc3a4f29a974327", + "tv_trades_sha256": "b0e782a34b812dd256f2b5d1d4f7c8d2b85285e40d2ede02a7ffad94940e909b", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/metrics.json new file mode 100644 index 00000000..622ee402 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7b-lim-1207-1345", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-12-04", + "to": "2025-12-10", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "cc0960643e18eb2af55dfbc58fa6abda524c7407febe62e38dc3a4f29a974327", + "tvTradesCsvHash": "b0e782a34b812dd256f2b5d1d4f7c8d2b85285e40d2ede02a7ffad94940e909b", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75282", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1764806400000, + "to": 1765324800000 + }, + "returnedRange": { + "from": 1764806400000, + "to": 1765324800000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f3d8f5f718cfedcad9fecc57995e6a457c6ab75ed83b1a771d39bee04e187500", + "decodedReportSha256": "1e617bd38478da53cc81ccbb92ea6b7cad3a259b672394a80048d399ef068e6e", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:41:23.190Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/strategy.pine new file mode 100644 index 00000000..0f3006a1 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/strategy.pine @@ -0,0 +1,15 @@ +// R5 lane PAR-MARGIN-2 (M7-B, a limit add): a carried long L1 (margin 5, 4 x equity) and a LIMIT add L2 +// (8.425 x equity, limit 0.997 x close) placed on L1's entry bar 2025-12-07 14:00 UTC and filled on the next bar +// on its way down; that bar's low breaches the combined line (L1 alone never does), its close recovers. +// Timeout close_all 4 bars after L1's entry bar. +//@version=6 +strategy("pm2-m7b-lim-1207-1345", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=2, + margin_long=5, margin_short=5, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 12, 7, 13, 45) and strategy.position_size == 0 + strategy.entry("L1", strategy.long, qty = math.floor(4 * strategy.equity / close * 1000) / 1000) +if time == timestamp("UTC", 2025, 12, 7, 14, 0) and strategy.position_size > 0 + strategy.entry("L2", strategy.long, qty = math.floor(8.425 * strategy.equity / close * 1000) / 1000, limit = math.round(close * 0.997, 2)) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 4 + strategy.close_all(comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/tv_trades.csv new file mode 100644 index 00000000..001439bc --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-lim-1207-1345/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-12-07 22:15,Margin call,2903.75,1.32,3999.8508,-166.9008,-4.17,0,5.8608,0.15,-166.9008,-4.17,-166.9008,-16.69,1 +1,Entry long,2025-12-07 22:00,L1,3030.19,1.32,3999.8508,-166.9008,-4.17,0,5.8608,0.15,-166.9008,-4.17,-166.9008,-16.69,1 +2,Exit long,2025-12-07 22:15,Margin call,2903.75,0.5228,1576.205404,-58.124905,-3.69,0,0,0.00,-58.124905,-3.69,-225.02571,-22.50,0 +2,Entry long,2025-12-07 22:15,L2,3014.93,0.5228,1576.205404,-58.124905,-3.69,0,0,0.00,-58.124905,-3.69,-225.02571,-22.50,0 +3,Exit long,2025-12-07 23:30,timeout,2953.2,2.2402,6754.046186,-138.28755,-2.05,0,0,0.00,-249.06543,-3.69,-363.31326,-36.33,5 +3,Entry long,2025-12-07 22:15,L2,3014.93,2.2402,6754.046186,-138.28755,-2.05,0,0,0.00,-249.06543,-3.69,-363.31326,-36.33,5 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/meta.json new file mode 100644 index 00000000..602c73b6 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7b-mkt-0105-1400", + "slug": "pm2-m7b-mkt-0105-1400", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:41:26.843Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-02", + "to": "2026-01-08" + }, + "pine_sha256": "9333acf753ec6c09028c6cdf9a69811a7e34b599bcc7d1ec83b2f09ddbbd3853", + "tv_trades_sha256": "080f04615748c3354701805a2eda3e024887041f081583e125aa2cc14cf2a1b4", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/metrics.json new file mode 100644 index 00000000..72251f28 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7b-mkt-0105-1400", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-02", + "to": "2026-01-08", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "9333acf753ec6c09028c6cdf9a69811a7e34b599bcc7d1ec83b2f09ddbbd3853", + "tvTradesCsvHash": "080f04615748c3354701805a2eda3e024887041f081583e125aa2cc14cf2a1b4", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75283", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1767312000000, + "to": 1767830400000 + }, + "returnedRange": { + "from": 1767312000000, + "to": 1767830400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "a4be3548b37421bd334f63fac8328620ede3ade9b54795e4963fb44791add552", + "decodedReportSha256": "c6ccc273bf910f4725ea42df1a7c28f234d276c810479946b4fc02fd5455d0a5", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:41:26.842Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/strategy.pine new file mode 100644 index 00000000..613537de --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/strategy.pine @@ -0,0 +1,14 @@ +// R5 lane PAR-MARGIN-2 (M7-B, a market add): a carried long L1 (margin 20, 1 x equity) and an add L2 +// (3.94 x equity) placed on L1's entry bar 2026-01-05 14:15 UTC and filled at the next bar's open, whose low +// breaches the combined book's line (L1 alone never does). Timeout close_all 4 bars after L1's entry bar. +//@version=6 +strategy("pm2-m7b-mkt-0105-1400", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=2, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2026, 1, 5, 14, 0) and strategy.position_size == 0 + strategy.entry("L1", strategy.long, qty = math.floor(1 * strategy.equity / close * 1000) / 1000) +if time == timestamp("UTC", 2026, 1, 5, 14, 15) and strategy.position_size > 0 + strategy.entry("L2", strategy.long, qty = math.floor(3.94 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 4 + strategy.close_all(comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/tv_trades.csv new file mode 100644 index 00000000..7b61fd40 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0105-1400/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2026-01-05 22:30,Margin call,3132.58,0.084,264.64788,-1.51116,-0.57,0,1.68672,0.64,-1.51116,-0.57,-1.51116,-0.15,1 +1,Entry long,2026-01-05 22:15,L1,3150.57,0.084,264.64788,-1.51116,-0.57,0,1.68672,0.64,-1.51116,-0.57,-1.51116,-0.15,1 +2,Exit long,2026-01-05 23:30,timeout,3163.22,0.233,734.08281,2.94745,0.40,0,6.04169,0.82,-4.19167,-0.57,1.43629,0.14,5 +2,Entry long,2026-01-05 22:15,L1,3150.57,0.233,734.08281,2.94745,0.40,0,6.04169,0.82,-4.19167,-0.57,1.43629,0.14,5 +3,Exit long,2026-01-05 23:30,timeout,3163.22,1.25,3941.475,12.55,0.32,0,29.15,0.74,-25.75,-0.65,13.98629,1.40,4 +3,Entry long,2026-01-05 22:30,L2,3153.18,1.25,3941.475,12.55,0.32,0,29.15,0.74,-25.75,-0.65,13.98629,1.40,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/meta.json new file mode 100644 index 00000000..438f3e88 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7b-mkt-0402-1930", + "slug": "pm2-m7b-mkt-0402-1930", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:41:30.777Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05" + }, + "pine_sha256": "4bbdad728989f4b83a853c84936eff2d866a1cecd30a7ebd0ca6f2f9c385e648", + "tv_trades_sha256": "a5f4d89f44b0fa938d2a2e8c150a83feba37cfd6b2b2bad61721f9dc1ead87a8", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/metrics.json new file mode 100644 index 00000000..9cfdfb16 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7b-mkt-0402-1930", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-30", + "to": "2025-04-05", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "4bbdad728989f4b83a853c84936eff2d866a1cecd30a7ebd0ca6f2f9c385e648", + "tvTradesCsvHash": "a5f4d89f44b0fa938d2a2e8c150a83feba37cfd6b2b2bad61721f9dc1ead87a8", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75284", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "returnedRange": { + "from": 1743292800000, + "to": 1743811200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "20bf6613968110f7141dc5acb6a80db08c7a13be15195074df399620908192f4", + "decodedReportSha256": "acb98e671eea041777afdc0438f088f3f2eae0700803fd8e32572f1f558722db", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:41:30.775Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/strategy.pine new file mode 100644 index 00000000..5e83f6f8 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/strategy.pine @@ -0,0 +1,14 @@ +// R5 lane PAR-MARGIN-2 (M7-B, a market add): a carried long L1 (margin 20, 1 x equity) and an add L2 +// (3.85 x equity) placed on L1's entry bar 2025-04-02 19:45 UTC and filled at the next bar's open, whose low +// breaches the combined book's line (L1 alone never does). Timeout close_all 4 bars after L1's entry bar. +//@version=6 +strategy("pm2-m7b-mkt-0402-1930", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=2, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 4, 2, 19, 30) and strategy.position_size == 0 + strategy.entry("L1", strategy.long, qty = math.floor(1 * strategy.equity / close * 1000) / 1000) +if time == timestamp("UTC", 2025, 4, 2, 19, 45) and strategy.position_size > 0 + strategy.entry("L2", strategy.long, qty = math.floor(3.85 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 4 + strategy.close_all(comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/tv_trades.csv new file mode 100644 index 00000000..80621319 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0402-1930/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-03 04:00,Margin call,1881,0.3136,597.4864,-7.6048,-1.27,0,3.145408,0.53,-7.6048,-1.27,-7.6048,-0.76,1 +1,Entry long,2025-04-03 03:45,L1,1905.25,0.3136,597.4864,-7.6048,-1.27,0,3.145408,0.53,-7.6048,-1.27,-7.6048,-0.76,1 +2,Exit long,2025-04-03 05:00,timeout,1880.16,0.2104,400.8646,-5.278936,-1.32,0,11.092288,2.77,-8.4686,-2.11,-12.883736,-1.29,5 +2,Entry long,2025-04-03 03:45,L1,1905.25,0.2104,400.8646,-5.278936,-1.32,0,11.092288,2.77,-8.4686,-2.11,-12.883736,-1.29,5 +3,Exit long,2025-04-03 05:00,timeout,1880.16,2.02,3859.3918,-61.4686,-1.59,0,95.7076,2.48,-92.0918,-2.39,-74.35233,-7.44,4 +3,Entry long,2025-04-03 04:00,L2,1910.59,2.02,3859.3918,-61.4686,-1.59,0,95.7076,2.48,-92.0918,-2.39,-74.35233,-7.44,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/meta.json new file mode 100644 index 00000000..04c53d97 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7b-mkt-0702-0045", + "slug": "pm2-m7b-mkt-0702-0045", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:41:34.890Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-29", + "to": "2025-07-05" + }, + "pine_sha256": "6ef94404e6b77936c8fbce3e1703c28b506bddaf9f04abb8b3097ee4d96e73bb", + "tv_trades_sha256": "cf8b4bacde9150a970f64a4de32bfe9259492f8188035629d0a74d448d1312c4", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/metrics.json new file mode 100644 index 00000000..6331fe3e --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7b-mkt-0702-0045", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-29", + "to": "2025-07-05", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "6ef94404e6b77936c8fbce3e1703c28b506bddaf9f04abb8b3097ee4d96e73bb", + "tvTradesCsvHash": "cf8b4bacde9150a970f64a4de32bfe9259492f8188035629d0a74d448d1312c4", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75285", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1751155200000, + "to": 1751673600000 + }, + "returnedRange": { + "from": 1751155200000, + "to": 1751673600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "2b4f461aacba5352fd24c60eea7ce0bb477431c5a50faa7f2ec7f0601db4e558", + "decodedReportSha256": "76c5607005142b8f4cf263721735e7ab7a1920041688c32d014d487d804111a1", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:41:34.889Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/strategy.pine new file mode 100644 index 00000000..be361468 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/strategy.pine @@ -0,0 +1,14 @@ +// R5 lane PAR-MARGIN-2 (M7-B, a market add): a carried long L1 (margin 20, 1 x equity) and an add L2 +// (3.865 x equity) placed on L1's entry bar 2025-07-02 01:00 UTC and filled at the next bar's open, whose low +// breaches the combined book's line (L1 alone never does). Timeout close_all 4 bars after L1's entry bar. +//@version=6 +strategy("pm2-m7b-mkt-0702-0045", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=2, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 7, 2, 0, 45) and strategy.position_size == 0 + strategy.entry("L1", strategy.long, qty = math.floor(1 * strategy.equity / close * 1000) / 1000) +if time == timestamp("UTC", 2025, 7, 2, 1, 0) and strategy.position_size > 0 + strategy.entry("L2", strategy.long, qty = math.floor(3.865 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 4 + strategy.close_all(comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/tv_trades.csv new file mode 100644 index 00000000..05550dff --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-0702-0045/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-02 09:15,Margin call,2367.53,0.2288,548.52512,-6.834256,-1.25,0,1.802944,0.33,-6.834256,-1.25,-6.834256,-0.68,1 +1,Entry long,2025-07-02 09:00,L1,2397.4,0.2288,548.52512,-6.834256,-1.25,0,1.802944,0.33,-6.834256,-1.25,-6.834256,-0.68,1 +2,Exit long,2025-07-02 10:15,timeout,2407.51,0.1882,451.19068,1.902702,0.42,0,3.718832,0.82,-5.621534,-1.25,-4.931554,-0.49,5 +2,Entry long,2025-07-02 09:00,L1,2397.4,0.1882,451.19068,1.902702,0.42,0,3.718832,0.82,-5.621534,-1.25,-4.931554,-0.49,5 +3,Exit long,2025-07-02 10:15,timeout,2407.51,1.612,3870.37976,10.52636,0.27,0,26.08216,0.67,-53.9214,-1.39,5.594806,0.56,4 +3,Entry long,2025-07-02 09:15,L2,2400.98,1.612,3870.37976,10.52636,0.27,0,26.08216,0.67,-53.9214,-1.39,5.594806,0.56,4 \ No newline at end of file diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/meta.json b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/meta.json new file mode 100644 index 00000000..d4efe862 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-m7b-mkt-1002-1430", + "slug": "pm2-m7b-mkt-1002-1430", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:41:38.550Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-09-29", + "to": "2025-10-05" + }, + "pine_sha256": "7dd639b999500d57e290d5bc41dee932065eadffec69f58b8149d60f7b38e777", + "tv_trades_sha256": "c2c01658ce472bb6d0447415eab522b56e0a7aac81775ac649843729916a0dec", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/metrics.json b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/metrics.json new file mode 100644 index 00000000..eb5ff69a --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-m7b-mkt-1002-1430", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-09-29", + "to": "2025-10-05", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "7dd639b999500d57e290d5bc41dee932065eadffec69f58b8149d60f7b38e777", + "tvTradesCsvHash": "c2c01658ce472bb6d0447415eab522b56e0a7aac81775ac649843729916a0dec", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75286", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1759104000000, + "to": 1759622400000 + }, + "returnedRange": { + "from": 1759104000000, + "to": 1759622400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "a977bdde470f5a3f66ac27641d97517090c8bee19caff33c9f2ed7eb893f69d8", + "decodedReportSha256": "834cfe67216b155436d14ca3d82a3224ce15d2faa6b24b163dbb22686637d4dd", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:41:38.549Z" +} diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/strategy.pine b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/strategy.pine new file mode 100644 index 00000000..258fa35a --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/strategy.pine @@ -0,0 +1,14 @@ +// R5 lane PAR-MARGIN-2 (M7-B, a market add): a carried long L1 (margin 20, 1 x equity) and an add L2 +// (3.915 x equity) placed on L1's entry bar 2025-10-02 14:45 UTC and filled at the next bar's open, whose low +// breaches the combined book's line (L1 alone never does). Timeout close_all 4 bars after L1's entry bar. +//@version=6 +strategy("pm2-m7b-mkt-1002-1430", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=2, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 10, 2, 14, 30) and strategy.position_size == 0 + strategy.entry("L1", strategy.long, qty = math.floor(1 * strategy.equity / close * 1000) / 1000) +if time == timestamp("UTC", 2025, 10, 2, 14, 45) and strategy.position_size > 0 + strategy.entry("L2", strategy.long, qty = math.floor(3.915 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 4 + strategy.close_all(comment="timeout") diff --git a/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/tv_trades.csv b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/tv_trades.csv new file mode 100644 index 00000000..93331e85 --- /dev/null +++ b/tests/fixtures/margin_entry_bar/pm2-m7b-mkt-1002-1430/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-10-02 23:00,Margin call,4332.23,0.084,368.27952,-4.3722,-1.19,0,0.84336,0.23,-4.3722,-1.19,-4.3722,-0.44,1 +1,Entry long,2025-10-02 22:45,L1,4384.28,0.084,368.27952,-4.3722,-1.19,0,0.84336,0.23,-4.3722,-1.19,-4.3722,-0.44,1 +2,Exit long,2025-10-03 00:00,timeout,4435.5,0.144,631.33632,7.37568,1.17,0,8.26416,1.31,-7.4952,-1.19,3.00348,0.30,5 +2,Entry long,2025-10-02 22:45,L1,4384.28,0.144,631.33632,7.37568,1.17,0,8.26416,1.31,-7.4952,-1.19,3.00348,0.30,5 +3,Exit long,2025-10-03 00:00,timeout,4435.5,0.892,3900.30568,56.16032,1.44,0,61.66396,1.58,-35.95652,-0.92,59.1638,5.92,4 +3,Entry long,2025-10-02 23:00,L2,4372.54,0.892,3900.30568,56.16032,1.44,0,61.66396,1.58,-35.95652,-0.92,59.1638,5.92,4 \ No newline at end of file diff --git a/tests/test_adapter_live_state_equivalence_data.hpp b/tests/test_adapter_live_state_equivalence_data.hpp index 5495b4ec..9096d146 100644 --- a/tests/test_adapter_live_state_equivalence_data.hpp +++ b/tests/test_adapter_live_state_equivalence_data.hpp @@ -29,12 +29,18 @@ // margin call on the entry bar instead of a bar later; the other 105 values // are unchanged. // -// R5 lane PAR-MARGIN-2 re-harvested one more the same way, on its tree: chains -// seed 1570937, whose limit long filled on its way down is now called on its -// own bar at the low (46 @106 on bar 25, a bar earlier) -- the kernel's -// post-fill point measures from the fill's own waypoint, TradingView's rule on -// its tapes (tests/fixtures/margin_entry_bar/pm2-m7-lim-*). The other 107 -// values are unchanged. +// R5 lane PAR-MARGIN-2 re-harvested five more the same way, on its tree, each +// a leveraged configuration whose margin call moves onto the bar its fill was +// on, or is re-sized there on the book that fill left -- TradingView's rule on +// its tapes (tests/fixtures/margin_entry_bar/pm2-*): +// chains seed 1570937: a limit long filled on its way down is called on its +// own bar at the low (46 @106 on bar 25, a bar earlier) -- the kernel's +// post-fill point now measures from the fill's own waypoint; +// chains seed 2199311 and reversals seed 2199309 (process_orders_on_close), +// reversals seed 1570935 (adds to a carried book) and reversals seed 314187 +// (calc_on_order_fills; its trades are unchanged, one more request placed): +// the adapter now admits the post-fill point on those fills' bars. +// The other 103 values are unchanged. // R5 lane PAR-ORDERS re-harvested it the same way on its tree (29 of 108 digests // move; see the lane's hash commit for why each moves). @@ -60,7 +66,7 @@ constexpr std::uint64_t kTranscriptDigests[] = { 17062111550189209216ull, // reversals seed 104729, 131 trades, 397 commands, 431 rows placed 3937583110602879389ull, // reversals seed 209458, 126 trades, 382 commands, 409 rows placed - 4920319354091947109ull, // reversals seed 314187, 208 trades, 684 commands, 651 rows placed + 9508064597864072295ull, // reversals seed 314187, 208 trades, 684 commands, 652 rows placed 14691229822702022375ull, // reversals seed 418916, 105 trades, 378 commands, 361 rows placed 201369272286234124ull, // reversals seed 523645, 104 trades, 417 commands, 357 rows placed 12651613190033426865ull, // reversals seed 628374, 138 trades, 410 commands, 399 rows placed @@ -72,13 +78,13 @@ constexpr std::uint64_t kTranscriptDigests[] = { 9826242374101611623ull, // reversals seed 1256748, 118 trades, 402 commands, 362 rows placed 4883135135199369800ull, // reversals seed 1361477, 105 trades, 413 commands, 349 rows placed 6980972530369447172ull, // reversals seed 1466206, 151 trades, 372 commands, 393 rows placed - 11252195188350860763ull, // reversals seed 1570935, 92 trades, 362 commands, 328 rows placed + 17000758005854428354ull, // reversals seed 1570935, 92 trades, 362 commands, 329 rows placed 15023244533711012002ull, // reversals seed 1675664, 124 trades, 398 commands, 405 rows placed 3425020382181293193ull, // reversals seed 1780393, 196 trades, 702 commands, 629 rows placed 4513324373166417315ull, // reversals seed 1885122, 113 trades, 393 commands, 388 rows placed 18393465585117555058ull, // reversals seed 1989851, 180 trades, 430 commands, 448 rows placed 6120235323624511436ull, // reversals seed 2094580, 92 trades, 384 commands, 340 rows placed - 12203651668804466516ull, // reversals seed 2199309, 127 trades, 398 commands, 375 rows placed + 9639971099840596709ull, // reversals seed 2199309, 127 trades, 398 commands, 375 rows placed 7979846979225294549ull, // reversals seed 2304038, 149 trades, 412 commands, 418 rows placed 16092912242308174488ull, // reversals seed 2408767, 139 trades, 415 commands, 404 rows placed 1517737495207721277ull, // reversals seed 2513496, 110 trades, 390 commands, 394 rows placed @@ -150,7 +156,7 @@ constexpr std::uint64_t kTranscriptDigests[] = { 8033768331525115464ull, // chains seed 1885124, 66 trades, 360 commands, 304 rows placed 8033691744657508662ull, // chains seed 1989853, 73 trades, 353 commands, 373 rows placed 10675979410645833515ull, // chains seed 2094582, 75 trades, 321 commands, 355 rows placed - 11831052386180063129ull, // chains seed 2199311, 56 trades, 327 commands, 278 rows placed + 13103985213974940416ull, // chains seed 2199311, 54 trades, 326 commands, 285 rows placed 9463870180619581974ull, // chains seed 2304040, 81 trades, 342 commands, 353 rows placed 1057624106564667840ull, // chains seed 2408769, 67 trades, 347 commands, 374 rows placed 1646636678506043505ull, // chains seed 2513498, 66 trades, 336 commands, 332 rows placed diff --git a/tests/test_adapter_margin_schedule_differential.cpp b/tests/test_adapter_margin_schedule_differential.cpp index 896365e5..e2f8bcd2 100644 --- a/tests/test_adapter_margin_schedule_differential.cpp +++ b/tests/test_adapter_margin_schedule_differential.cpp @@ -21,10 +21,10 @@ * * M7 (G2-09) scheduling. The kernel's AfterApplied point after a * leveraged opening is admitted on the opening's own entry bar since - * R5 lane PAR-MARGIN (TradingView's tapes below), so the adapter books - * what the kernel books there. Its AfterApplied point after an add to - * a carried book, and its BarOpen point on a carried POOC short with - * fees, are points the adapter still refuses; the kernel books there. + * R5 lane PAR-MARGIN (TradingView's tapes below), and after an add to + * a carried book since R5 lane PAR-MARGIN-2, so the adapter books what + * the kernel books there. Its BarOpen point on a carried POOC short + * with fees is a point the adapter still refuses; the kernel books there. * M8 (G2-10) checkpoints as market executions. A gap-open breach: the * adapter executes at the open, sized at the open; the kernel has no * check kind that does -- its mark check rests at the adverse extreme, @@ -58,10 +58,12 @@ * pinned); it now books TradingView's rows on all four tapes. * R5 lane PAR-MARGIN-2, each section replaying the lane's `lab tv` tapes from * the fixture's own strategy.pine: - * M7 shapes: limit openings, long and short, leveraged and 1x -- 12 tapes, - * all TradingView's rows (the kernel's post-fill point now measures from - * the fill's own waypoint: a limit filled on its way down faces the - * bar's low, a short filled on its way up its high); + * M7 shapes: calc_on_order_fills openings, limit openings with and without + * process_orders_on_close, short limit openings (leveraged and 1x), POOC + * market openings, limit and market adds to a carried book, magnified + * openings -- 36 tapes, all TradingView's rows (the kernel's post-fill + * point now measures from the fill's own waypoint: a limit filled on its + * way down faces the bar's low, a short filled on its way up its high); * * Source-bound (includes pineforge/source): release profile only. */ @@ -430,14 +432,16 @@ void m7_leveraged_opening_entry_bar() { // B. A carried 10 @100 long (50 %, capital 1050) adds 10 @95 on a buy limit // filled on the H->L leg of bar 2 (O100 H101 L80 C81; high-first). After the // add the rest of the path runs down to the low 80: 700 against 800. -// kernel: AfterApplied at the add's point books 4 x 100 / 40 = 10 at 80 -- -// ab9714be's answer too (the post-add book at the bar's own low). -// Until R5 lane PAR-MARGIN-2 the kernel's post-fill mark scanned only -// the waypoints after the fill's own -- the fill is presented at the -// low it was falling toward -- so it booked 4 x 90 / 40.5 = 8.888.. -// at the close 81; -// adapter: the point is refused (no scheduling after an add to a carried -// book), and bar 3 (O100) is solvent -- no margin row at all. +// kernel: AfterApplied at the add's point books 4 x 100 / 40 = 10 at 80; +// adapter: admits that point for an add to a carried leveraged book, so it +// books the same 10 @80. +// That is ab9714be's answer too (the post-add book at the bar's own low), and +// TradingView's on the class (tests/fixtures/margin_entry_bar/pm2-m7b-lim-*, +// the "M7 shapes" section below). Until R5 lane PAR-MARGIN-2 the kernel's +// post-fill mark scanned only the waypoints after the fill's own -- the fill +// is presented at the low it was falling toward -- so it booked 4 x 90 / 40.5 +// = 8.888.. at the close 81, and the adapter refused the point: no margin row +// at all (bar 3, O100, is solvent). Both pins flipped with the fix. void m7_mid_bar_add() { std::printf("-- M7-B mid-bar pyramid add: the add's AfterApplied point\n"); Config c; @@ -463,11 +467,13 @@ void m7_mid_bar_add() { const auto native = margin_fills(kernel); print_side("kernel", native, kernel.position(), kernel.balance()); - CHECK(adapter.empty()); - CHECK(pine.position() == 20.0); REQUIRE(native.size() == 1); check_fill(native[0], 2, NativePathPhase::Low, 10.0, 80.0, true); CHECK(same_value(kernel.position(), 10.0)); + REQUIRE(adapter.size() == 1); + check_fill(adapter[0], 2, NativePathPhase::Low, 10.0, 80.0, true); + CHECK(adapter[0].label == "__margin_call__"); + CHECK(same_value(pine.position(), 10.0)); } // C. A carried POOC short 9.5 @100 (100 %, 0.1 % fee) whose bar 1 reaches @@ -1515,9 +1521,10 @@ void print_exits(const char* side, const std::vector& rows) { } // ---- the M7 shapes (tests/fixtures/margin_entry_bar/pm2-*) -// A leveraged opening, on the bar it fills. TradingView books the +// A leveraged opening or add, on the bar it fills. TradingView books the // margin call on that bar over the rest of its path, sized on the book the // fill left: +// pm2-m7-coof-* calc_on_order_fills, M7-A's events: the M7-A rows, 4 of 4; // pm2-m7-lim-* a limit opening filled on its way down: the call at that // bar's LOW, 4 of 4 (the kernel's post-fill point scanned the // waypoints after the fill's own, so it missed the low -- the @@ -1525,6 +1532,14 @@ void print_exits(const char* side, const std::vector& rows) { // pm2-m7-slim-*, pm2-m7-s1lim-* a SHORT limit opening filled on its way up, // leveraged (margin 5) and at full margin (1x): the call at // that bar's HIGH, 8 of 8 (none was booked before); +// pm2-m7-poocl-* the same limits under process_orders_on_close, 4 of 4; +// pm2-m7b-lim-* a limit ADD to a carried book, 4 of 4, on the combined book +// (the first-in lot first); +// pm2-m7b-mkt-* a market add at the open, 4 of 4 (booked before the lane); +// pm2-m7-poocm-* a POOC market opening at the signal close, the call on the +// next bar, 4 of 4 (booked before the lane); +// pm2-m7-mag-* M7-A's events magnified: the call at the first crossing +// 1-minute low of the entry bar, 4 of 4 (booked before). struct ShapeTape { const char* slug; const FeedBar* bars; @@ -1534,12 +1549,24 @@ struct ShapeTape { void m7_shapes_on_tapes() { const std::string dir = PINEFORGE_HM_M7A_FIXTURE_DIR; const ShapeTape tapes[] = { + {"pm2-m7-coof-0621", kM7a_m5_k16_0621, 6}, {"pm2-m7-coof-0922", kM7a_m5_k16_0922, 6}, + {"pm2-m7-coof-1121", kM7a_m5_k16_1121, 6}, {"pm2-m7-coof-1010", kM7a_m20_k4_1010, 6}, {"pm2-m7-lim-0402-1945", kPm2Chart_0402_1945, 6}, {"pm2-m7-lim-0402-2000", kPm2Chart_0402_2000, 6}, {"pm2-m7-lim-0801-0030", kPm2Chart_0801_0030, 6}, {"pm2-m7-lim-1201-0215", kPm2Chart_1201_0215, 6}, {"pm2-m7-slim-0402-1330", kPm2Chart_0402_1330, 6}, {"pm2-m7-slim-0509-1100", kPm2Chart_0509_1100, 6}, {"pm2-m7-slim-0709-1930", kPm2Chart_0709_1930, 6}, {"pm2-m7-slim-1001-0830", kPm2Chart_1001_0830, 6}, {"pm2-m7-s1lim-0402-1330", kPm2Chart_0402_1330, 6}, {"pm2-m7-s1lim-0509-1100", kPm2Chart_0509_1100, 6}, {"pm2-m7-s1lim-0709-1930", kPm2Chart_0709_1930, 6}, {"pm2-m7-s1lim-1001-0830", kPm2Chart_1001_0830, 6}, + {"pm2-m7-poocl-0402-1945", kPm2Chart_0402_1945, 6}, {"pm2-m7-poocl-0402-2000", kPm2Chart_0402_2000, 6}, + {"pm2-m7-poocl-0801-0030", kPm2Chart_0801_0030, 6}, {"pm2-m7-poocl-1201-0215", kPm2Chart_1201_0215, 6}, + {"pm2-m7-poocm-0105-1415", kPm2Chart_0105_1415, 6}, {"pm2-m7-poocm-0402-1945", kPm2Chart_0402_1945, 6}, + {"pm2-m7-poocm-0702-0100", kPm2Chart_0702_0100, 6}, {"pm2-m7-poocm-1002-1445", kPm2Chart_1002_1445, 6}, + {"pm2-m7b-lim-0402-1930", kPm2Add_0402_1930, 9}, {"pm2-m7b-lim-0402-1945", kPm2Add_0402_1945, 9}, + {"pm2-m7b-lim-0801-0015", kPm2Add_0801_0015, 9}, {"pm2-m7b-lim-1207-1345", kPm2Add_1207_1345, 9}, + {"pm2-m7b-mkt-0105-1400", kPm2Add_0105_1400, 9}, {"pm2-m7b-mkt-0402-1930", kPm2Add_0402_1930, 9}, + {"pm2-m7b-mkt-0702-0045", kPm2Add_0702_0045, 9}, {"pm2-m7b-mkt-1002-1430", kPm2Add_1002_1430, 9}, + {"pm2-m7-mag-0621", kPm2Mag_0621, 90}, {"pm2-m7-mag-0922", kPm2Mag_0922, 90}, + {"pm2-m7-mag-1010", kPm2Mag_1010, 90}, {"pm2-m7-mag-1121", kPm2Mag_1121, 90}, }; for (const ShapeTape& tape : tapes) { std::printf("-- M7 shape %s\n", tape.slug); From ce71fccb654906513b48e693af856c93c9518e76 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:34:54 +0800 Subject: [PATCH 44/83] Adapter: the intrabar margin check under POOC and COOF and on a full-margin short; TradingView samples 2-minute intrabars (R5 lane PAR-MARGIN-2, item 3 shapes) PAR-MARGIN admitted the kernel's IntrabarSample point only on a plain magnified run's leveraged book. Twenty lab tv exports (tests/fixtures/intrabar_margin/ pm2-i3-*, BINANCE:ETHUSDT.P 15m, ws-report-v1, rangeProof covered; the lane's own scripts) measure the shapes it left open: - pm2-i3-coof-*: PAR-MARGIN's four magnified scripts under calc_on_order_fills book the SAME rows as without it (the same tape hashes); - pm2-i3-pooc-{mag,chart}-*: a leveraged long filled at the signal close under process_orders_on_close; - pm2-i3-s1x-{mag,chart}-*: a 1x short (margin 100); and the eight chart controls book the one call at the chart bar's extreme the adapter already booked. With the magnifier on, TradingView checks the book at each of its intrabars and books the call at the first whose extreme crosses, sized there, then at each later crossing of the reduced book's line. intrabar_sample_checked now admits a leveraged long plain, under POOC or under COOF, and a short at any margin plain or under COOF (a full-margin long keeps its one-contract money call; a POOC short, unmeasured, keeps the post-script checkpoint). TradingView's intrabars are not the feed's minutes: its bar magnifier table maps a 15-minute chart to 2-minute intrabars ("15 / 2 yields 7.5 bars, which is rounded down to 7"), and six request.security_lower_tf("2") probes (tests/fixtures/intrabar_margin/pm2-ltf-*) print them: epoch-aligned minute pairs, each owned by the chart bar holding its LAST minute ([14..28] for a :15 bar, [30..42] for a :30 one). This is PAR-MARGIN's open 0130 skip: 01:42-01:43 is ONE intrabar, its low 2673.33. A model of the check at those intrabars books every call of all 20 magnified tapes (PAR-MARGIN's 4, COOF 4, POOC 4, 1x 4, the M7 magnifier 4); at the one-minute samples the corpus host feeds, the same model books exactly what the adapter books. The grids agree on 14 of the 20 (the adapter books the tapes' rows) and part on six -- 0130 plain and COOF, pooc 0109, 0402 (its first call, 1908.17, is the low of the straddling 20:14-20:16 intrabar, a minute before the fill), 0707 and s1x 0623 -- which stay recorded: fixing them means sampling the magnifier at TradingView's intrabar timeframe, which moves every magnified fill, not only margin calls. Adapter vs tape before -> after: COOF 0 -> 3 of 4, POOC magnified 0 -> 1 of 4, 1x magnified 0 -> 3 of 4; on every residual tape the call now lands on TradingView's bar (before, a bar late or never). tests/test_adapter_margin_schedule_differential.cpp: "intrabar shapes" (each tape from its own strategy.pine; TradingView's calls == the 2-minute model on all 16, the adapter's == the 1-minute model on all 16, full rows where the grids agree) and "TradingView intrabars" (the six printed arrays == the feed's minute pairs under the last-minute ownership rule). Docs: native-engine.md, ADR-0001 (margin bullet, IntrabarSample row), design MG15, the fixture README (0130 explained); anchors carried by an exact line map. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit bd73072ca6c1aa136c1d41cd3b2de486926b7478) INT26 pick note: the ADR row this commit rewrites (IntrabarSample) keeps its place after K-OCA-KEEP's two rows; the docs merged on anchor digits otherwise. No pinned behaviour moves on this tree (quiet-bar, publication and live-state harvested with their own switches). --- docs/adr/0001-kernel-adapter-boundary.md | 24 +- docs/design/native-feature-parity.md | 42 +- docs/pages/native-engine.md | 27 +- src/source/pine_adapter.cpp | 25 +- tests/fixtures/intrabar_margin/README.md | 75 +- .../pm2-i3-coof-0130/meta.json | 16 + .../pm2-i3-coof-0130/metrics.json | 40 + .../pm2-i3-coof-0130/strategy.pine | 13 + .../pm2-i3-coof-0130/tv_trades.csv | 7 + .../pm2-i3-coof-0406/meta.json | 16 + .../pm2-i3-coof-0406/metrics.json | 40 + .../pm2-i3-coof-0406/strategy.pine | 13 + .../pm2-i3-coof-0406/tv_trades.csv | 5 + .../pm2-i3-coof-0622/meta.json | 16 + .../pm2-i3-coof-0622/metrics.json | 40 + .../pm2-i3-coof-0622/strategy.pine | 13 + .../pm2-i3-coof-0622/tv_trades.csv | 9 + .../pm2-i3-coof-0824/meta.json | 16 + .../pm2-i3-coof-0824/metrics.json | 40 + .../pm2-i3-coof-0824/strategy.pine | 13 + .../pm2-i3-coof-0824/tv_trades.csv | 5 + .../pm2-i3-pooc-chart-0109-0115/meta.json | 16 + .../pm2-i3-pooc-chart-0109-0115/metrics.json | 40 + .../pm2-i3-pooc-chart-0109-0115/strategy.pine | 13 + .../pm2-i3-pooc-chart-0109-0115/tv_trades.csv | 5 + .../pm2-i3-pooc-chart-0402-2000/meta.json | 16 + .../pm2-i3-pooc-chart-0402-2000/metrics.json | 40 + .../pm2-i3-pooc-chart-0402-2000/strategy.pine | 13 + .../pm2-i3-pooc-chart-0402-2000/tv_trades.csv | 5 + .../pm2-i3-pooc-chart-0707-1600/meta.json | 16 + .../pm2-i3-pooc-chart-0707-1600/metrics.json | 40 + .../pm2-i3-pooc-chart-0707-1600/strategy.pine | 13 + .../pm2-i3-pooc-chart-0707-1600/tv_trades.csv | 5 + .../pm2-i3-pooc-chart-1010-1900/meta.json | 16 + .../pm2-i3-pooc-chart-1010-1900/metrics.json | 40 + .../pm2-i3-pooc-chart-1010-1900/strategy.pine | 13 + .../pm2-i3-pooc-chart-1010-1900/tv_trades.csv | 5 + .../pm2-i3-pooc-mag-0109-0115/meta.json | 16 + .../pm2-i3-pooc-mag-0109-0115/metrics.json | 40 + .../pm2-i3-pooc-mag-0109-0115/strategy.pine | 13 + .../pm2-i3-pooc-mag-0109-0115/tv_trades.csv | 5 + .../pm2-i3-pooc-mag-0402-2000/meta.json | 16 + .../pm2-i3-pooc-mag-0402-2000/metrics.json | 40 + .../pm2-i3-pooc-mag-0402-2000/strategy.pine | 13 + .../pm2-i3-pooc-mag-0402-2000/tv_trades.csv | 7 + .../pm2-i3-pooc-mag-0707-1600/meta.json | 16 + .../pm2-i3-pooc-mag-0707-1600/metrics.json | 40 + .../pm2-i3-pooc-mag-0707-1600/strategy.pine | 13 + .../pm2-i3-pooc-mag-0707-1600/tv_trades.csv | 5 + .../pm2-i3-pooc-mag-1010-1900/meta.json | 16 + .../pm2-i3-pooc-mag-1010-1900/metrics.json | 40 + .../pm2-i3-pooc-mag-1010-1900/strategy.pine | 13 + .../pm2-i3-pooc-mag-1010-1900/tv_trades.csv | 7 + .../pm2-i3-s1x-chart-0201-1100/meta.json | 16 + .../pm2-i3-s1x-chart-0201-1100/metrics.json | 40 + .../pm2-i3-s1x-chart-0201-1100/strategy.pine | 13 + .../pm2-i3-s1x-chart-0201-1100/tv_trades.csv | 5 + .../pm2-i3-s1x-chart-0407-1145/meta.json | 16 + .../pm2-i3-s1x-chart-0407-1145/metrics.json | 40 + .../pm2-i3-s1x-chart-0407-1145/strategy.pine | 13 + .../pm2-i3-s1x-chart-0407-1145/tv_trades.csv | 5 + .../pm2-i3-s1x-chart-0623-2130/meta.json | 16 + .../pm2-i3-s1x-chart-0623-2130/metrics.json | 40 + .../pm2-i3-s1x-chart-0623-2130/strategy.pine | 13 + .../pm2-i3-s1x-chart-0623-2130/tv_trades.csv | 5 + .../pm2-i3-s1x-chart-1104-2130/meta.json | 16 + .../pm2-i3-s1x-chart-1104-2130/metrics.json | 40 + .../pm2-i3-s1x-chart-1104-2130/strategy.pine | 13 + .../pm2-i3-s1x-chart-1104-2130/tv_trades.csv | 5 + .../pm2-i3-s1x-mag-0201-1100/meta.json | 16 + .../pm2-i3-s1x-mag-0201-1100/metrics.json | 40 + .../pm2-i3-s1x-mag-0201-1100/strategy.pine | 13 + .../pm2-i3-s1x-mag-0201-1100/tv_trades.csv | 7 + .../pm2-i3-s1x-mag-0407-1145/meta.json | 16 + .../pm2-i3-s1x-mag-0407-1145/metrics.json | 40 + .../pm2-i3-s1x-mag-0407-1145/strategy.pine | 13 + .../pm2-i3-s1x-mag-0407-1145/tv_trades.csv | 11 + .../pm2-i3-s1x-mag-0623-2130/meta.json | 16 + .../pm2-i3-s1x-mag-0623-2130/metrics.json | 40 + .../pm2-i3-s1x-mag-0623-2130/strategy.pine | 13 + .../pm2-i3-s1x-mag-0623-2130/tv_trades.csv | 7 + .../pm2-i3-s1x-mag-1104-2130/meta.json | 16 + .../pm2-i3-s1x-mag-1104-2130/metrics.json | 40 + .../pm2-i3-s1x-mag-1104-2130/strategy.pine | 13 + .../pm2-i3-s1x-mag-1104-2130/tv_trades.csv | 7 + .../intrabar_margin/pm2-ltf-0109/meta.json | 16 + .../intrabar_margin/pm2-ltf-0109/metrics.json | 40 + .../pm2-ltf-0109/strategy.pine | 30 + .../pm2-ltf-0109/tv_trades.csv | 19 + .../intrabar_margin/pm2-ltf-0130/meta.json | 16 + .../intrabar_margin/pm2-ltf-0130/metrics.json | 40 + .../pm2-ltf-0130/strategy.pine | 30 + .../pm2-ltf-0130/tv_trades.csv | 19 + .../intrabar_margin/pm2-ltf-0402/meta.json | 16 + .../intrabar_margin/pm2-ltf-0402/metrics.json | 40 + .../pm2-ltf-0402/strategy.pine | 30 + .../pm2-ltf-0402/tv_trades.csv | 19 + .../intrabar_margin/pm2-ltf-0623/meta.json | 16 + .../intrabar_margin/pm2-ltf-0623/metrics.json | 40 + .../pm2-ltf-0623/strategy.pine | 30 + .../pm2-ltf-0623/tv_trades.csv | 19 + .../intrabar_margin/pm2-ltf-0707/meta.json | 16 + .../intrabar_margin/pm2-ltf-0707/metrics.json | 40 + .../pm2-ltf-0707/strategy.pine | 30 + .../pm2-ltf-0707/tv_trades.csv | 19 + .../intrabar_margin/pm2-ltf-1010/meta.json | 16 + .../intrabar_margin/pm2-ltf-1010/metrics.json | 40 + .../pm2-ltf-1010/strategy.pine | 30 + .../pm2-ltf-1010/tv_trades.csv | 19 + .../fixtures/intrabar_margin/pm2_bars_1m.inc | 739 ++++++++++++++++++ ...t_adapter_margin_schedule_differential.cpp | 252 ++++++ 111 files changed, 3266 insertions(+), 50 deletions(-) create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0130/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0130/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0130/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0130/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0406/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0406/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0406/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0406/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0622/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0622/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0622/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0622/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0824/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0824/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0824/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-coof-0824/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0109/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0109/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0109/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0109/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0130/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0130/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0130/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0130/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0402/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0402/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0402/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0402/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0623/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0623/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0623/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0623/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0707/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0707/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0707/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-0707/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-1010/meta.json create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-1010/metrics.json create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-1010/strategy.pine create mode 100644 tests/fixtures/intrabar_margin/pm2-ltf-1010/tv_trades.csv create mode 100644 tests/fixtures/intrabar_margin/pm2_bars_1m.inc diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index c4f3b84d..c1f0d26b 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -250,7 +250,7 @@ kernel and own these quirks, each at its site: - **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:14103`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14167`) for the ten-significant-digit money, + `resolve_margin_requirement` (`pine_adapter.cpp:14172`) 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 @@ -261,7 +261,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:16871`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16876`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -290,12 +290,18 @@ kernel and own these quirks, each at its site: matched on the way down to it (the rule-2 row below). A magnified or timestamped-FX run keeps its own route, and the default-percent stop entry the pre-open slice answers for keeps that slice's verdict (`preopen_slice_class`). Under the bar magnifier the adapter admits the kernel's - `IntrabarSample` points on a leveraged book (`intrabar_sample_checked`): TradingView's magnified - broker books its call at the first lower-timeframe low that crosses and again at each later one - that crosses the reduced book's line (`tests/fixtures/intrabar_margin`: every row on 3 of 4 - tapes; on the fourth it books its second call a minute after the low this check books it at -- - a recorded divergence, open). A full-margin book, `process_orders_on_close` and - `calc_on_order_fills` keep their routes there, unmeasured. + `IntrabarSample` points (`intrabar_sample_checked`) on a leveraged long -- plain, under + `process_orders_on_close` or under `calc_on_order_fills` -- and on a short at any margin, plain + or under `calc_on_order_fills`: TradingView's magnified broker books its call at the first + lower-timeframe low that crosses and again at each later one that crosses the reduced book's + line (`tests/fixtures/intrabar_margin`, 20 tapes). TradingView samples its own 2-minute + intrabars on a 15-minute chart (its bar magnifier table; `request.security_lower_tf` prints + them, each owned by the chart bar holding its last minute), and a model of the check at those + intrabars books all 20 tapes' calls; the adapter checks the samples its host feeds (one minute + on the corpus), so six tapes part where the two grids resolve a crossing differently -- a + recorded divergence whose fix is the magnifier's sampling, every magnified fill with it, not + this check. A full-margin long (its one-contract money call) and a short under + `process_orders_on_close` keep their routes there. - **Calculation timing.** The cadence itself is the kernel's: a `calc_on_order_fills` strategy projects `NativeCalculationTrigger::BarCloseAndFills` with TradingView's guard literal as `max_recalculations_per_point`. What stays are the specifics COOF adds on top — @@ -553,7 +559,7 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | an OCA-Reduce sibling's deduction binary64 cannot take -- at most half an ulp of what it is taken from, so that fl(units - d) is the units again -- absorbed where the request core deducts a member's fill (`absorbed_deduction`, `pending_sum` and `absorbed_pending_delta` in `src/native_order.cpp`, read by `WorkingRequestCore::prepare_group_effect` / `apply_group_effect`, by `prepare_terms` / `apply_terms` through `effective_host_units`, and by `prepare_owner_applied` / `apply_owner_applied`), and recorded as a `ReservationReducedEvent` with `actual_deduction` 0, a `DeferredGroupAdjustmentEvent` with `deferred_delta` 0 that joins no chain, or a `TermsResolvedEvent` / `QuantityBoundEvent` with `effective_deduction` 0 beside a positive `pending_total` (`include/pineforge/native_order.hpp`; C: `closed_units` 0 on a `PF_NATIVE_EVENT_RESERVATION_REDUCED` or `PF_NATIVE_EVENT_DEFERRED_GROUP` row) | any host, by the OCA-Reduce groups it submits (C: `PF_NATIVE_GROUP_REDUCE`), and the Pine adapter by the `strategy.oca.reduce` groups it maps (`PineExecutionAdapter::group_for`, `src/source/pine_adapter.cpp`) | **absorbed as a no-op** (R5 lane K-ULP5, the owner-level ruling on K-ULP4's finding 2). Rule 2 (a), mechanism: binary64 bookkeeping is venue-neutral -- a deduction too small to move the sibling's remaining units (or its pending total) leaves them where binary64 subtraction puts them, unchanged, for a host of any venue, and the member's fill stands; a pending total below half an ulp of a later fill rounds into it, as the addition does. Rule 2 (b), knob: none -- it replaces a whole-run `SettlementFailure` (code 6, discriminator 7, after the fill was booked) that no host could want, a deduction binary64 can take is taken as before, bit for bit, and only a pending total that overflows binary64 still fails the run with that discriminator. What moved: runs that stopped on such a deduction now complete -- K-ULP4's p5b probe on the C++ and the C host, and the 914 of the 3,000 runs of the seeded OCA-Reduce battery that stopped on f1af50dc -- and the rows that pinned the old contract now pin the absorption (the G5 rows of `tests/test_native_resting_acceptance.cpp` and `tests/test_native_resting_replay_contract.cpp`, `checked_reservation_arithmetic` of `tests/test_native_order_resting_core.cpp`, A-T4d of `tests/test_native_execution_terms.cpp`, `numeric_absorption_and_chain_authentication` of `tests/test_native_order_terms_core.cpp`); the overflow rows still pin the failure. No completed run's deductions move -- each was one binary64 could take -- and `inspect_current_execution`, which threw `std::overflow_error` for a host-sized request whose pending total its units absorb, now answers it. Pinned by `tests/test_native_group_absorption.cpp` (kernel-only) and `check_reservation_absorbed` of `tests/test_native_c_api.c`. | | the order of one cause's group-effect receipts -- the queue order `WorkingRequestCore::group_recipients` walks, which a `ReplaceOptions::keep_handle` re-price parts from incarnation order: `receipt_lookup` bisects the receipts by cause and walks that cause's, and `prepare_group_effect` / `apply_group_effect` refuse only a cause older than the newest receipt's (`src/native_order.cpp`; `WorkingRequestCore::group_effect_receipt`, `include/pineforge/native_order.hpp`) | any C++ host that re-prices a group member with `keep_handle` and then fills a sibling (`NativeStrategyHost::replace(handle, request, options)`; no C spelling, the C replace takes no options); the Pine adapter never sets `keep_handle` | **generic: a drain applies a group's effect in queue order, whatever the handles' numbers** (R5 lane K-OCA-KEEP, K-ULP5's finding 1). Rule 2 (a), mechanism: which live siblings a member's fill reaches, and in what order, is the book's own queue for a host of any venue, and the receipts are that drain's idempotence record, in the order it applied them; a request re-priced with its handle kept is reached where a plain replace's successor is, so the two runs record one timeline, as V19-D's contract already said. Rule 2 (b), knob: none -- it replaces a whole-run `Contract` failure (code 2, discriminator 1, `CoreFailure::InvalidCause`, after the member's fill was booked) that no host could want: the receipt check held one cause's receipts to incarnation order, and the re-priced member, drained behind a younger sibling, was refused; a cause older than the newest receipt's is still refused. What moved: runs that stopped on such a drain now complete -- K-ULP5's `probe_keep_handle_oca`, staged and direct, under `Cancel` and `Reduce`, and all 180 runs of the seeded battery under a keep_handle option set (of its 360), which stopped on 2a03c658 -- each recording the plain replace's run event for event. No completed run moves and no hash value moved: a completed run's receipts were already in incarnation order within each cause, and the lookup answers such a store as before (the K-ULP4 and K-ULP5 batteries' per-run hashes unchanged; corpus parity unchanged by construction, the Pine adapter never setting `keep_handle`). Pinned by `tests/test_native_group_keep_handle.cpp` (kernel-only). | | a request on a quantity grid whose pending group deduction its units absorb -- it settles them all in one fill (`settles_in_one_fill`, `src/native_execution_consumer.cpp`, which asks `WorkingRequestCore::effective_host_units` whether the pending total takes any), so a whole-scope `ScopeFraction` resolves to its scope's held total unfloored and a host-sized close answered with that total is its own quantity, at the match and at the current execution alike | any host, by the OCA-Reduce groups and the quantity grid it declares (C: a fraction, and a close sized by `on_close_units`); the Pine adapter does not reach it: it emits no `ScopeFraction`, and it answers a close of its scope's whole total with `ExecutionGridPolicy::ExplicitUnits`, whose grid check never asks whether the request settles in one fill | **one definition of settling in one fill** (R5 lane K-OCA-KEEP, K-ULP5's finding 2). Rule 2 (a), mechanism: K-ULP4's grid rule admits the book's own quantity for a request that settles it in one fill, and K-ULP5's absorption makes a sub-ulp pending total take nothing, for a host of any venue; the two now agree. Rule 2 (b), knob: none. What moved: a run in which such a request closes its scope's total now closes it whole -- a whole-scope fraction was floored onto the grid and closed short, leaving a dust lot (before K-ULP5 such a run stopped wherever the floored units absorbed the total too, code 6, discriminator 7), and a host-sized close answered with its scope's total was refused `InvalidTerms` and left the lot open, before K-ULP5 as after: the `grid-absorbed-*` rows of `tests/test_native_group_keep_handle.cpp`, whose fraction closed 1.1000000000000001 of a 1.1000000000000227 lot on 2a03c658. K-ULP4's own battery answers every scenario's hash as before, K-ULP5's absorption and C-host batteries are unchanged (no grid), `grid-fraction-oca-pending` of `tests/test_native_unrepresentable_refusal.cpp` (a pending total that takes 0.3) is still floored, and no pinned hash moved. Pinned by `tests/test_native_group_keep_handle.cpp` (kernel-only). | -| the margin model's check points on an intrabar path -- `NativeMarginCheckKind::IntrabarSample` (`include/pineforge/native_host.hpp`; C `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE`), offered at every delivered sample after the script bar's first of a `lower_tf` path matched as `ContinuousSegments`, at that sample's price, immediately before it is matched (`NativeExecutionConsumer::deliver_intrabar_script`, `src/native_execution_consumer.cpp`) | every host with a margin model and such a path, through `margin_check_allowed`; the Pine adapter admits it on a magnified run's leveraged book (`PineExecutionAdapter::intrabar_sample_checked`, `src/source/pine_adapter.cpp`) | **a check point of the kernel's own** (R5 lane PAR-MARGIN, item 3; H-MEASURE's acid finding A). Rule 2 (a), mechanism: a run with no whole-bar waypoint model measures its maintenance requirement where its path is -- at each delivered sample -- the rule the docs and `native_run_spec.hpp` stated and the kernel had not kept (it checked the first sample and after fills only); nothing in it knows a venue, and a host that checks somewhere else suppresses the points it does not share, as for every other kind. Rule 2 (b), knob: none -- the path is the run's own declaration, and a one-price distribution path (`synthesized`, `DistributionSamples`) is not offered the point, since a request armed at a discrete point is matched only at a later one. What moved: a margin run on a continuous intrabar path whose book crosses its line inside a bar now books that call at the crossing sample (H-MEASURE's intrabar probe: 55 points offered, a slice at 105 where none was booked). TradingView's magnified broker does the same (`tests/fixtures/intrabar_margin`: the first call at the first lower-timeframe low that crosses on 4 of 4 tapes, every row on 3). No pinned value moved. Pinned by `tests/test_native_margin_intrabar_samples.cpp` (kernel-only) and the "intrabar margin on tapes" section of `tests/test_adapter_margin_schedule_differential.cpp`. | +| the margin model's check points on an intrabar path -- `NativeMarginCheckKind::IntrabarSample` (`include/pineforge/native_host.hpp`; C `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE`), offered at every delivered sample after the script bar's first of a `lower_tf` path matched as `ContinuousSegments`, at that sample's price, immediately before it is matched (`NativeExecutionConsumer::deliver_intrabar_script`, `src/native_execution_consumer.cpp`) | every host with a margin model and such a path, through `margin_check_allowed`; the Pine adapter admits it on a magnified run's leveraged long and, outside `process_orders_on_close`, its short at any margin (R5 lane PAR-MARGIN-2) (`PineExecutionAdapter::intrabar_sample_checked`, `src/source/pine_adapter.cpp`) | **a check point of the kernel's own** (R5 lane PAR-MARGIN, item 3; H-MEASURE's acid finding A). Rule 2 (a), mechanism: a run with no whole-bar waypoint model measures its maintenance requirement where its path is -- at each delivered sample -- the rule the docs and `native_run_spec.hpp` stated and the kernel had not kept (it checked the first sample and after fills only); nothing in it knows a venue, and a host that checks somewhere else suppresses the points it does not share, as for every other kind. Rule 2 (b), knob: none -- the path is the run's own declaration, and a one-price distribution path (`synthesized`, `DistributionSamples`) is not offered the point, since a request armed at a discrete point is matched only at a later one. What moved: a margin run on a continuous intrabar path whose book crosses its line inside a bar now books that call at the crossing sample (H-MEASURE's intrabar probe: 55 points offered, a slice at 105 where none was booked). TradingView's magnified broker does the same at its own intrabars, 2 minutes on a 15-minute chart (`tests/fixtures/intrabar_margin`: the first call at the first lower-timeframe low that crosses on 4 of 4 tapes; a model of the check at TradingView's 2-minute intrabars books every row of all 20 magnified tapes, R5 lane PAR-MARGIN-2). No pinned value moved. Pinned by `tests/test_native_margin_intrabar_samples.cpp` (kernel-only) and the "intrabar margin on tapes" section of `tests/test_adapter_margin_schedule_differential.cpp`. | | the path a post-fill margin check measures -- `NativeExecutionConsumer::margin_segment_origin` (`src/native_execution_consumer.cpp`), the phase the `AfterApplied` re-arm hands `margin_sizing_price`: the waypoint the segment into the fill's driver point starts from, with the fill price as the point's own mark | every host with a margin model on a path walked by its modeled waypoints (no intrabar path), through the `AfterApplied` point `margin_check_allowed` admits | **a mechanism correction** (R5 lane PAR-MARGIN-2). Rule 2 (a), mechanism: a request matched at a driver point was reached on the segment into it, so the book its fill leaves still faces that point's waypoint -- a limit filled on its way down to the bar's low faces the low, a short filled on its way up faces the high -- and the scan that started strictly after the point's phase measured a path the book had not finished; the waypoints the path already passed stay out, as `fx_roll_margin_check_at`'s continuous roll measures from its segment's origin. Nothing in it knows a venue. Rule 2 (b), knob: none. What moved: a post-fill point after a fill matched on a segment now measures that segment's destination waypoint -- on a high-first bar O100 H101 L80 C81 a buy limit at 95 measures 80 where it measured the close 81, and a low-first bar or a short filled on its way up is called where nothing was (`tests/test_native_margin_post_fill_path.cpp`, kernel-only); a fill at a waypoint's own price, the open's included, measures what it did. TradingView books that call on the fill's bar (lab tv tapes `tests/fixtures/margin_entry_bar/pm2-m7-lim-*` at the low, `pm2-m7-slim-*` and `pm2-m7-s1lim-*` at the high, 12 of 12; with the adapter's admissions `pm2-m7-poocl-*` and `pm2-m7b-lim-*` too), as `ab9714be`'s entry-bar suffix did; the differential's M7-B pin moved to that answer, 10 @80. One v19 witness value moved with it: `tests/test_publication_witness.cpp` Config03, a short filled on its way up a bar, now called at that bar's high (461a919ebdcd41e9 -> 4593ef08ab33e045); the corpus is 312/312, moved 0. | | the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:1825` and `:1934`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:295`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | | the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1478`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 199eaadf..ef1a80f0 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -50,16 +50,16 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2738-2749; `entry()` pine_adapter.cpp:5314, `order()` pine_adapter.cpp:10515) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5698-5745; `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:8417 → `Flatten{}` pine_adapter.cpp:8446-8447; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2708-3915 → `host.replace` pine_adapter.cpp:2724-2804) | **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:4723-4769, `cancel_all` pine_adapter.cpp:18099-18114; `live_by_source_key_` pine_adapter.hpp:1999; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **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. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4723-4769, `cancel_all` pine_adapter.cpp:18104-18119; `live_by_source_key_` pine_adapter.hpp:1999; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5538-5573, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7508-7525) | **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:2747-2753) | **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:13038; lowering `submit_one` pine_adapter.cpp:9075-9087, `BindCohort` pine_adapter.cpp:9576-9582, `submit_leg` pine_adapter.cpp:9600-9624; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8680-8683; `materialize_relative_exits` pine_adapter.cpp:17389-17430, pine_adapter.hpp:1824-1836) | **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:5624-5658. `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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19173-19202) | **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:2001-2019); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10915-10939), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1585-1608, `CloseCallsiteState` pine_adapter.hpp:1679-1694; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15698, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15932) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8680-8683; `materialize_relative_exits` pine_adapter.cpp:17394-17435, pine_adapter.hpp:1824-1836) | **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:5624-5658. `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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19178-19207) | **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:2001-2019); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10915-10939), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1585-1608, `CloseCallsiteState` pine_adapter.hpp:1679-1694; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15703, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15937) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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 @@ -86,7 +86,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:12513-12541, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:9002-9312) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4444-4459) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6741), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7948), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12769), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13365, pine_adapter.cpp:13418) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15746) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18769) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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). | +| 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:6741), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7948), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12769), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13365, pine_adapter.cpp:13418) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15751) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18774) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7210-7561; ledger `close_batch_bar_` pine_adapter.hpp:2042-2053; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | @@ -99,17 +99,17 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6230-6279, cap left unset `max_open_lots` pine_adapter.cpp:2361-2363) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | | MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13314-13379 and leaves the kernel gate unset pine_adapter.cpp:13313-13315) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | | MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13969-13970, `margin_pct` pine_adapter.cpp:13377) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:14069-14275, `submit_margin_call_units` pine_adapter.cpp:14236-14282, `schedule_margin_call_path` pine_adapter.cpp:18252-18353) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **Closed:** `maintenance_long` / `maintenance_short` with `NativeLiquidationCheck::{PathAdverseExtreme, PathAdverseExtremeMark, CalculationOnly}`; the kernel solves the level, rests its own `Reduce`/`Flatten` bound to the live book, re-prices it with `CancelReason::Superseded` and books a `MarginCallEvent` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | F:D2 O:D3 S:M3 | TV parts → MG15 | +| MG6 | Maintenance margin + forced liquidation (`submit_margin_call_slice` pine_adapter.cpp:14069-14280, `submit_margin_call_units` pine_adapter.cpp:14241-14287, `schedule_margin_call_path` pine_adapter.cpp:18257-18358) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14000-14036) | **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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16350-16366, `apply_fx_opening_margin_slice` pine_adapter.cpp:18354-18382) | **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:16841-16880) | **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 | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16355-16371, `apply_fx_opening_margin_slice` pine_adapter.cpp:18359-18387) | **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:16846-16885) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13349-13352) | **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:13930-13944, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14989-15019) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15032-15266) | **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 | +| MG12 | Max intraday loss → flatten + block (`intraday_loss_breached` pine_adapter.cpp:13930-13944, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14994-15024) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15037-15271) | **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:13740-13819) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1323 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:13958, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14017, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14753-14793, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14277-14404, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14277) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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, `tests/fixtures/margin_entry_bar/pm2-*`). Under the magnifier the kernel's `IntrabarSample` point (R5 lane PAR-MARGIN) is admitted on a leveraged book, where TradingView books its call at the first lower-timeframe low that crosses (`tests/fixtures/intrabar_margin`, every row on 3 of 4 tapes, the fourth a recorded divergence). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15270-15328) | n/a | A | — | S:R2 | (single-source) | +| MG15 | *(quirk)* TV margin call: money rounding `source_margin_money` pine_adapter.cpp:13958, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14017, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14758-14798, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14282-14409, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14282) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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, `tests/fixtures/margin_entry_bar/pm2-*`). 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 (`tests/fixtures/intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15275-15333) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -117,10 +117,10 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7542-7546 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16185) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16190) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `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:7027 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7078 (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:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5635, `flush_coof_tail` pine_adapter.cpp:16328, `end_coof_recalc` pine_adapter.cpp:5656) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5635, `flush_coof_tail` pine_adapter.cpp:16333, `end_coof_recalc` pine_adapter.cpp:5656) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7166-7170; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | | 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:7536-7655 **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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19173-19202 | 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:19178-19207 | 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:523, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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:604. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1274 | 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) | @@ -298,7 +298,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3005-3073) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3014-3082) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:45, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -523,9 +523,9 @@ line numbers are this tree's (683a82f); kernel semantics are |---|---|---|---|---|---| | `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2362); 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:13260-13261; 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:16841, `update_risk_state` pine_adapter.cpp:13901-13928): 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:13253), 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_drawdown` | `update_risk_state(bar.close)` once per script bar at the close mark (`update_risk_state(bar.close)` pine_adapter.cpp:16846, `update_risk_state` pine_adapter.cpp:13901-13928): 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:13253), 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:1200-1208): +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:17724-17733); 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:16344); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16456), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15019-15028, `submit_intraday_loss_close` pine_adapter.cpp:14989-15017), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17198); 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:5986, `:7655`, `:8379`, `:8507`, `:11484`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18097-18100) | `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_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16349); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16461), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15024-15033, `submit_intraday_loss_close` pine_adapter.cpp:14994-15022), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17203); 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:5986, `:7655`, `:8379`, `:8507`, `:11484`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18102-18105) | `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:13892-13894); 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:13798-13819, `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 @@ -1300,7 +1300,7 @@ unchanged after it. | 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5782) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6290-6303); 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:16185-16214) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:16190-16219) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | @@ -1345,7 +1345,7 @@ unchanged after it. | **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:16350-16366) 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:2646) 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:16355-16371) 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:2646) 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. | @@ -1443,7 +1443,7 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1684-1685) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6995-7007) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1693-1694) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6995-7007) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). 2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5629-5633) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1660-1667) is calibrated against it. 3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6848-6855). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2553); valid in bar-open, applied and tick callbacks. diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index f6fce543..527c45c5 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1312,7 +1312,10 @@ and after fills only: a request armed at a discrete point is matched only at a later one, so a reduction re-armed at every sample would never be. The same money under the magnifier on TradingView books its call at the first lower-timeframe low that crosses, sized there -(`tests/fixtures/intrabar_margin`). +(`tests/fixtures/intrabar_margin`). TradingView's lower timeframe is its own: +on a 15-minute chart its bar magnifier walks 2-minute intrabars, each owned by +the chart bar that holds its last minute, so the samples a run declares decide +where its calls land. `basis` chooses the equity side of that comparison, at every check point. `MarkedEquity` (the default) is `marked_equity(mark)` itself, which the open @@ -1531,14 +1534,20 @@ bar, on the combined book: twenty more tapes book the call on that bar at its low (`tests/fixtures/margin_entry_bar`: `pm2-m7-coof-*`, `pm2-m7-lim-*`, `pm2-m7-poocl-*`, `pm2-m7b-*`). A magnified or timestamped-FX run keeps its own route, and a default-percent stop entry taken at its bar's open keeps -the pre-open slice's verdict. Under the bar magnifier the adapter admits the kernel's -`IntrabarSample` points on a leveraged book: TradingView's magnified broker -books its call at the first lower-timeframe low that crosses and again at each -later one that crosses the reduced book's line (`tests/fixtures/intrabar_margin`, -every row on 3 of 4 tapes; on the fourth TradingView books its second call a -minute after the low the check books it at, a recorded divergence). A -full-margin book and the `process_orders_on_close` and `calc_on_order_fills` -runs keep their routes there, unmeasured. +the pre-open slice's verdict. Under the bar magnifier the adapter admits the +kernel's `IntrabarSample` points: TradingView's magnified broker books its call +at the first lower-timeframe low that crosses and again at each later one that +crosses the reduced book's line, on a leveraged long (plain, under +`process_orders_on_close` or under `calc_on_order_fills`) and on a short at any +margin, plain or under `calc_on_order_fills` (`tests/fixtures/intrabar_margin`, +20 magnified tapes). It checks at its own 2-minute intrabars (above), which +TradingView's `request.security_lower_tf` prints, and a model of that check +books all 20 tapes' calls; the adapter checks the samples its host feeds, one +minute on the corpus, so six tapes part where the two grids resolve a crossing +differently -- recorded, not fixed here: sampling the magnifier at +TradingView's intrabar timeframe moves every magnified fill, not only margin +calls. A full-margin long (its one-contract money call) and a short under +`process_orders_on_close` keep their routes there. ### Risk limits diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 608b09bb..a311996c 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -14228,20 +14228,25 @@ bool PineExecutionAdapter::margin_check_allowed( return !source_margin_rounded_tie_veto(); } -// Where the per-sample check is TradingView's as measured: a plain magnified -// run (the tapes run neither process_orders_on_close nor calc_on_order_fills) -// holding a leveraged book -- a full-margin book keeps its own checkpoints, -// unmeasured under the magnifier -- and no slice of the adapter's own resting -// (the pre-open slice keeps its verdict for its bar, as on the chart path). -// A flat book is admitted: the kernel then withdraws what it had resting. +// Where the per-sample check is TradingView's as measured, on a magnified run +// with no slice of the adapter's own resting (the pre-open slice keeps its +// verdict for its bar, as on the chart path): a leveraged long, plain, under +// process_orders_on_close or under calc_on_order_fills, and a short at any +// margin, plain or under calc_on_order_fills (lab tv tapes +// tests/fixtures/intrabar_margin: pm-i3-eth-mag-* for the plain leveraged long, +// R5 lane PAR-MARGIN; pm2-i3-pooc-mag-*, pm2-i3-coof-* and the full-margin +// short pm2-i3-s1x-mag-*, R5 lane PAR-MARGIN-2). Not a full-margin long, whose +// one-contract money call owns the book (resolve_margin_call_units refuses +// it), nor a short under process_orders_on_close, whose carried slice is the +// post-script checkpoint's (schedule_margin_call_path), unmeasured here. A +// flat book is admitted: the kernel then withdraws what it had resting. bool PineExecutionAdapter::intrabar_sample_checked( const NativeMarginCheckPoint& point) const { - if (!bar_magnifier_ || config_.process_orders_on_close || config_.calc_on_order_fills) - return false; + if (!bar_magnifier_) return false; const double held = point.position.signed_units; if (held == 0.0) return true; - const double margin = held > 0.0 ? config_.margin_long : config_.margin_short; - if (!(margin < 100.0)) return false; + if (held > 0.0 ? !(config_.margin_long < 100.0) : config_.process_orders_on_close) + return false; return !any_live_row(live_handles_, placement_, [](const PlacementSnapshot& row) { return row.family == PineOrderFamily::Margin; }); diff --git a/tests/fixtures/intrabar_margin/README.md b/tests/fixtures/intrabar_margin/README.md index 9dc47243..c3993635 100644 --- a/tests/fixtures/intrabar_margin/README.md +++ b/tests/fixtures/intrabar_margin/README.md @@ -17,7 +17,9 @@ TradingView dates a magnified fill at its chart bar's open. books its second call at the 01:43 low (2673.33) on the book the first call left, where a check at the 01:42 low (2690, which crosses that book's line) would have booked one a minute earlier; the export repeats over three other - windows. Recorded, open. + windows. Explained by R5 lane PAR-MARGIN-2 (below): TradingView's magnifier + walks 2-minute intrabars on a 15-minute chart, and 01:42-01:43 is ONE of them, + its low 2673.33. Recorded: the adapter samples the one-minute feed. `bars_1m.inc` holds each event's 1-minute bars from the corpus feed; `pm-i3-eth-ltf-lows` prints TradingView's own 1-minute lows inside the two @@ -36,3 +38,74 @@ on tapes") replays every pair. Campaign notes: `tv-tape--`. | `pm-i3-eth-mag-0406` | 2025-04-04 .. 2025-04-08 | margin calls 0.418 @1574.55 | `40208b4aa509bd58` | | `pm-i3-eth-chart-0406` | 2025-04-04 .. 2025-04-08 | margin calls 1.0776 @1552.5 | `ea61c13201510514` | | `pm-i3-eth-ltf-lows` | 2025-06-20 .. 2026-02-01 | the 1m lows inside 2025-06-22 13:15 and 2026-01-30 01:30 (UTC), printed as entry comments | `ad312f3e1202becf` | + +## PAR-MARGIN-2: item 3 under `process_orders_on_close`, `calc_on_order_fills` and on a full-margin book + +Twenty more `lab tv` exports on BINANCE:ETHUSDT.P 15m, the lane's own scripts: + +- `pm2-i3-coof-*`: this directory's four magnified scripts under + `calc_on_order_fills` -- the same rows as `pm-i3-eth-mag-*` (the same tape + hashes), 0130's second call included. +- `pm2-i3-pooc-{mag,chart}-*`: a leveraged long (margin 20) filled at the + signal close under `process_orders_on_close`, carried into a bar that first + crosses at an intrabar low and reaches a deeper low later; magnifier on and + off. +- `pm2-i3-s1x-{mag,chart}-*`: a 1 x short (margin 100, 0.93-1.00 x equity) + filled at the next open, carried into a bar that first crosses at an + intrabar high and reaches a higher one later; magnifier on and off. + +Magnifier off: one call at the chart bar's extreme, as the adapter books it (8 +of 8). Magnifier on: TradingView checks the book at each of ITS intrabars -- +2 minutes on a 15-minute chart (TradingView's bar magnifier table: 15 -> 2; +"15 / 2 yields 7.5 bars, which is rounded down to 7") -- and books the call at +the first one whose extreme crosses, sized there, and again at each later one +that crosses the reduced book's line. The `pm2-ltf-*` probes print TradingView's +own 2-minute intrabars inside six chart bars through +`request.security_lower_tf(syminfo.tickerid, "2", ...)`: epoch-aligned pairs of +the feed's minutes, each owned by the chart bar that holds its LAST minute +([14, 16, .., 28] for a :15 bar, [30, .., 42] for a :30 one), so a :15 bar's +first intrabar reaches back into the previous bar's last minute (0402's first +call, 1908.17, is that minute's low: it precedes the fill at the signal close). + +`tests/test_adapter_margin_schedule_differential.cpp` ("intrabar shapes") +replays every tape from its own `strategy.pine` and models the check: at +TradingView's 2-minute intrabars the model books every call of all 16 +magnified tapes here (and of the four `pm2-m7-mag-*`), and at the one-minute +samples the adapter's host feeds it books exactly what the adapter books. The +two grids agree on 10 of the 16, where the adapter books the tape's rows; they +part on six (0130 plain and COOF, pooc 0109, 0402 and 0707, s1x 0623), which +stay recorded. `pm2-ltf-*` is checked against the feed ("TradingView +intrabars"). `pm2_bars_1m.inc` holds each new event's one-minute bars from the +corpus feed. + +| probe | window | TradingView's margin calls | tvTradesCsvHash | +|---|---|---|---| +| `pm2-i3-coof-0130` | 2026-01-28 .. 2026-02-01 | 0.1088 @2760, 0.4824 @2673.33 | `ac0fb36013b01d07` | +| `pm2-i3-coof-0406` | 2025-04-04 .. 2025-04-08 | 0.418 @1574.55 | `40208b4aa509bd58` | +| `pm2-i3-coof-0622` | 2025-06-20 .. 2025-06-24 | 0.1052 @2223.05, 0.0276 @2198.19, 0.1688 @2180 | `2488da65fe9bd341` | +| `pm2-i3-coof-0824` | 2025-08-22 .. 2025-08-26 | 0.1224 @4820 | `f0e1b58462313b44` | +| `pm2-i3-pooc-chart-0109-0115` | 2026-01-07 .. 2026-01-11 | 0.2864 @3077.46 | `4dea9978d6d5e886` | +| `pm2-i3-pooc-chart-0402-2000` | 2025-03-31 .. 2025-04-04 | 0.8216 @1872.48 | `c592ff0981fbb444` | +| `pm2-i3-pooc-chart-0707-1600` | 2025-07-05 .. 2025-07-09 | 0.2712 @2505.37 | `7093ef952abe4bdc` | +| `pm2-i3-pooc-chart-1010-1900` | 2025-10-08 .. 2025-10-12 | 0.2436 @3950 | `34e625565aa35031` | +| `pm2-i3-pooc-mag-0109-0115` | 2026-01-07 .. 2026-01-11 | 0.1332 @3096.44 | `a0d512ec52485a02` | +| `pm2-i3-pooc-mag-0402-2000` | 2025-03-31 .. 2025-04-04 | 0.0656 @1908.17, 0.1992 @1888.67 | `70d257b985aec02f` | +| `pm2-i3-pooc-mag-0707-1600` | 2025-07-05 .. 2025-07-09 | 0.2712 @2505.37 | `7093ef952abe4bdc` | +| `pm2-i3-pooc-mag-1010-1900` | 2025-10-08 .. 2025-10-12 | 0.0392 @3991.74, 0.0788 @3950 | `c9aa3967619f2f8a` | +| `pm2-i3-s1x-chart-0201-1100` | 2026-01-30 .. 2026-02-03 | 0.0352 @2423.66 | `50f0f64a37926674` | +| `pm2-i3-s1x-chart-0407-1145` | 2025-04-05 .. 2025-04-09 | 0.0872 @1519.12 | `7f3c307947114fba` | +| `pm2-i3-s1x-chart-0623-2130` | 2025-06-21 .. 2025-06-25 | 0.0796 @2409.52 | `8f7d4ee9bdb9ff4b` | +| `pm2-i3-s1x-chart-1104-2130` | 2025-11-02 .. 2025-11-06 | 0.0292 @3271.1 | `098b32d635fd010d` | +| `pm2-i3-s1x-mag-0201-1100` | 2026-01-30 .. 2026-02-03 | 0.0092 @2423.66, 0.0064 @2402.65 | `a72c25a00038c741` | +| `pm2-i3-s1x-mag-0407-1145` | 2025-04-05 .. 2025-04-09 | 0.0084 @1516, 0.0168 @1498.93, 0.0016 @1523.99, 0.0036 @1526.48 | `4a145425eba491a3` | +| `pm2-i3-s1x-mag-0623-2130` | 2025-06-21 .. 2025-06-25 | 0.0236 @2369.42, 0.0236 @2440 | `924d02a624c08979` | +| `pm2-i3-s1x-mag-1104-2130` | 2025-11-02 .. 2025-11-06 | 0.0064 @3271.1, 0.0056 @3238.56 | `5bcd717fe804192c` | + +| probe | the chart bar (UTC) | intrabars printed | tvTradesCsvHash | +|---|---|---|---| +| `pm2-ltf-0109` | 2026-01-09 01:30 | minutes [30, 32, 34, 36, 38, 40, 42] | `88eaca621366d3aa` | +| `pm2-ltf-0130` | 2026-01-30 01:30 | minutes [30, 32, 34, 36, 38, 40, 42] | `75e6678ceb46c597` | +| `pm2-ltf-0402` | 2025-04-02 20:15 | minutes [14, 16, 18, 20, 22, 24, 26, 28] | `24f3cae40c441e68` | +| `pm2-ltf-0623` | 2025-06-23 22:00 | minutes [0, 2, 4, 6, 8, 10, 12] | `3223f6f0c7a3462d` | +| `pm2-ltf-0707` | 2025-07-07 16:15 | minutes [14, 16, 18, 20, 22, 24, 26, 28] | `53d673f2e8c5317b` | +| `pm2-ltf-1010` | 2025-10-10 19:15 | minutes [14, 16, 18, 20, 22, 24, 26, 28] | `01b82dc91bf8bbed` | diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0130/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-coof-0130/meta.json new file mode 100644 index 00000000..45a9bc32 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0130/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-coof-0130", + "slug": "pm2-i3-coof-0130", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:37:17.195Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-28", + "to": "2026-02-01" + }, + "pine_sha256": "c8eb2ef38682af0750e84b15295b0d8805de862c62ded3e954c664f1c95a63ca", + "tv_trades_sha256": "ac0fb36013b01d07933abe1523bbc6fff5bb2ee008821cee24cd5102e056a2d4", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0130/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-coof-0130/metrics.json new file mode 100644 index 00000000..4e0ce428 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0130/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-coof-0130", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-28", + "to": "2026-02-01", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "c8eb2ef38682af0750e84b15295b0d8805de862c62ded3e954c664f1c95a63ca", + "tvTradesCsvHash": "ac0fb36013b01d07933abe1523bbc6fff5bb2ee008821cee24cd5102e056a2d4", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75186", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1769558400000, + "to": 1769904000000 + }, + "returnedRange": { + "from": 1769558400000, + "to": 1769904000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "a19fbb2fedbb25cffd0a252b44951c5ed5ad2077b70f30a11a3c197a0ccc643c", + "decodedReportSha256": "8ffdeb8c4040c8fc17f8a283db4176060a66716e4827823f8b038d84ac50c09f", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:37:17.194Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0130/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-coof-0130/strategy.pine new file mode 100644 index 00000000..b874ee69 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0130/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 under calc_on_order_fills): one leveraged long (margin 20, 4.743 x equity) +// placed on the signal bar 2026-01-30 01:00 UTC, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute low and reaches a deeper low later. +// Magnifier ON, calc_on_order_fills ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-coof-0130", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, calc_on_order_fills=true, use_bar_magnifier=true) +if time == timestamp("UTC", 2026, 1, 30, 1, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.743 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0130/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-coof-0130/tv_trades.csv new file mode 100644 index 00000000..89160d08 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0130/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2026-01-30 09:30,Margin call,2760,0.1088,305.656192,-5.368192,-1.76,0,0.303552,0.10,-5.368192,-1.76,-5.368192,-0.54,1 +1,Entry long,2026-01-30 09:15,L,2809.34,0.1088,305.656192,-5.368192,-1.76,0,0.303552,0.10,-5.368192,-1.76,-5.368192,-0.54,1 +2,Exit long,2026-01-30 09:30,Margin call,2673.33,0.4824,1355.225616,-65.61122,-4.84,0,1.345896,0.10,-65.61122,-4.84,-70.979416,-7.10,1 +2,Entry long,2026-01-30 09:15,L,2809.34,0.4824,1355.225616,-65.61122,-4.84,0,1.345896,0.10,-65.61122,-4.84,-70.979416,-7.10,1 +3,Exit long,2026-01-30 10:15,timeout,2728.59,1.0968,3081.284112,-88.5666,-2.87,0,3.060072,0.10,-149.17577,-4.84,-159.54602,-15.95,4 +3,Entry long,2026-01-30 09:15,L,2809.34,1.0968,3081.284112,-88.5666,-2.87,0,3.060072,0.10,-149.17577,-4.84,-159.54602,-15.95,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0406/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-coof-0406/meta.json new file mode 100644 index 00000000..bd7322f5 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0406/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-coof-0406", + "slug": "pm2-i3-coof-0406", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:37:24.067Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-04", + "to": "2025-04-08" + }, + "pine_sha256": "532b1c229603ae3be2d296fc3b8d8698dccc0c4f49d045d11d80c89dcdd3fdf2", + "tv_trades_sha256": "40208b4aa509bd587fee990e0f8490956242c0cc3e9cfc6d8dc387e606a38e8a", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0406/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-coof-0406/metrics.json new file mode 100644 index 00000000..8dfe2430 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0406/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-coof-0406", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-04", + "to": "2025-04-08", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "532b1c229603ae3be2d296fc3b8d8698dccc0c4f49d045d11d80c89dcdd3fdf2", + "tvTradesCsvHash": "40208b4aa509bd587fee990e0f8490956242c0cc3e9cfc6d8dc387e606a38e8a", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75190", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743724800000, + "to": 1744070400000 + }, + "returnedRange": { + "from": 1743724800000, + "to": 1744070400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "48e11d78784d0ac667e91fb0a5f4ed9667c48c72f8735f6834199f8e4e409377", + "decodedReportSha256": "5807b949257bc2eecc28b4834ddd4d52d66b80a45e944dee8f02b9cd7e7eab57", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:37:24.067Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0406/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-coof-0406/strategy.pine new file mode 100644 index 00000000..31cb280c --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0406/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 under calc_on_order_fills): one leveraged long (margin 20, 4.657 x equity) +// placed on the signal bar 2025-04-06 20:00 UTC, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute low and reaches a deeper low later. +// Magnifier ON, calc_on_order_fills ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-coof-0406", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, calc_on_order_fills=true, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 4, 6, 20, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.657 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0406/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-coof-0406/tv_trades.csv new file mode 100644 index 00000000..97db1d90 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0406/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-07 04:30,Margin call,1574.55,0.418,676.60406,-18.44216,-2.73,0,2.16106,0.32,-18.44216,-2.73,-18.44216,-1.84,1 +1,Entry long,2025-04-07 04:15,L,1618.67,0.418,676.60406,-18.44216,-2.73,0,2.16106,0.32,-18.44216,-2.73,-18.44216,-1.84,1 +2,Exit long,2025-04-07 05:15,timeout,1586.78,2.459,3980.30953,-78.41751,-1.97,0,12.71303,0.32,-162.71204,-4.09,-96.85967,-9.69,4 +2,Entry long,2025-04-07 04:15,L,1618.67,2.459,3980.30953,-78.41751,-1.97,0,12.71303,0.32,-162.71204,-4.09,-96.85967,-9.69,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0622/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-coof-0622/meta.json new file mode 100644 index 00000000..53a093cf --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0622/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-coof-0622", + "slug": "pm2-i3-coof-0622", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:37:31.351Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-20", + "to": "2025-06-24" + }, + "pine_sha256": "98bcd16edf2be79b5f04e0c7e14cace1b837815bb15264e34f3e29384259d7ce", + "tv_trades_sha256": "2488da65fe9bd3412b35cb19d3fc59f63ae07c3de39b6e1645598af94dd59d41", + "tv_trades_rows": 8 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0622/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-coof-0622/metrics.json new file mode 100644 index 00000000..0862efbe --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0622/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-coof-0622", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-20", + "to": "2025-06-24", + "deepBacktesting": true, + "trades": 4, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "98bcd16edf2be79b5f04e0c7e14cace1b837815bb15264e34f3e29384259d7ce", + "tvTradesCsvHash": "2488da65fe9bd3412b35cb19d3fc59f63ae07c3de39b6e1645598af94dd59d41", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75195", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1750377600000, + "to": 1750723200000 + }, + "returnedRange": { + "from": 1750377600000, + "to": 1750723200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "13a6001da6dfc52406e2917bda7d388c6269c48c0df94dc6929cfc39a454b09b", + "decodedReportSha256": "e8383f7d8bbeb0bbcb1bd575f9b5cd6a6b2b10c0d5432b307964f3ddb0eca121", + "closedTrades": 4, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:37:31.350Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0622/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-coof-0622/strategy.pine new file mode 100644 index 00000000..dcaaee9c --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0622/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 under calc_on_order_fills): one leveraged long (margin 20, 4.608 x equity) +// placed on the signal bar 2025-06-22 12:45 UTC, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute low and reaches a deeper low later. +// Magnifier ON, calc_on_order_fills ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-coof-0622", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, calc_on_order_fills=true, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 6, 22, 12, 45) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.608 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0622/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-coof-0622/tv_trades.csv new file mode 100644 index 00000000..82a812e3 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0622/tv_trades.csv @@ -0,0 +1,9 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-22 21:15,Margin call,2223.05,0.1052,239.72976,-5.8649,-2.45,0,0.564924,0.24,-5.8649,-2.45,-5.8649,-0.59,1 +1,Entry long,2025-06-22 21:00,L,2278.8,0.1052,239.72976,-5.8649,-2.45,0,0.564924,0.24,-5.8649,-2.45,-5.8649,-0.59,1 +2,Exit long,2025-06-22 21:15,Margin call,2198.19,0.0276,62.89488,-2.224836,-3.54,0,0.148212,0.24,-2.224836,-3.54,-8.089736,-0.81,1 +2,Entry long,2025-06-22 21:00,L,2278.8,0.0276,62.89488,-2.224836,-3.54,0,0.148212,0.24,-2.224836,-3.54,-8.089736,-0.81,1 +3,Exit long,2025-06-22 21:15,Margin call,2180,0.1688,384.66144,-16.67744,-4.34,0,0.906456,0.24,-16.67744,-4.34,-24.767176,-2.48,1 +3,Entry long,2025-06-22 21:00,L,2278.8,0.1688,384.66144,-16.67744,-4.34,0,0.906456,0.24,-16.67744,-4.34,-24.767176,-2.48,1 +4,Exit long,2025-06-22 22:00,timeout,2198.9,1.7204,3920.44752,-137.45996,-3.51,0,9.238548,0.24,-205.62221,-5.24,-162.22714,-16.22,4 +4,Entry long,2025-06-22 21:00,L,2278.8,1.7204,3920.44752,-137.45996,-3.51,0,9.238548,0.24,-205.62221,-5.24,-162.22714,-16.22,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0824/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-coof-0824/meta.json new file mode 100644 index 00000000..3e489cc5 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0824/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-coof-0824", + "slug": "pm2-i3-coof-0824", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:37:35.893Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-08-22", + "to": "2025-08-26" + }, + "pine_sha256": "7d2be5b6b0e9620e29b97b08cdc277c6a6dbaf075af5878431d4973ee9d90c96", + "tv_trades_sha256": "f0e1b58462313b44a5727e0d9ecc952238de677cf6db3ad3ecdc0192d04d8d4a", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0824/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-coof-0824/metrics.json new file mode 100644 index 00000000..c88756d1 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0824/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-coof-0824", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-08-22", + "to": "2025-08-26", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "7d2be5b6b0e9620e29b97b08cdc277c6a6dbaf075af5878431d4973ee9d90c96", + "tvTradesCsvHash": "f0e1b58462313b44a5727e0d9ecc952238de677cf6db3ad3ecdc0192d04d8d4a", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75199", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1755820800000, + "to": 1756166400000 + }, + "returnedRange": { + "from": 1755820800000, + "to": 1756166400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "dacfca534b7284b53ac0d3cb9094b706e34854070e74403acbb46deeb75f3182", + "decodedReportSha256": "1f6b35a0b6fbbbd1f0cb4932f4b1d77cd0628e37b578c1f4bbed6670e1ab0958", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:37:35.893Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0824/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-coof-0824/strategy.pine new file mode 100644 index 00000000..4b428cb0 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0824/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 under calc_on_order_fills): one leveraged long (margin 20, 4.694 x equity) +// placed on the signal bar 2025-08-24 19:00 UTC, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute low and reaches a deeper low later. +// Magnifier ON, calc_on_order_fills ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-coof-0824", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=false, calc_on_order_fills=true, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 8, 24, 19, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.694 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-coof-0824/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-coof-0824/tv_trades.csv new file mode 100644 index 00000000..71bdde8e --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-coof-0824/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-08-25 03:30,Margin call,4820,0.1224,604.620504,-14.652504,-2.42,0,1.867824,0.31,-14.652504,-2.42,-14.652504,-1.47,1 +1,Entry long,2025-08-25 03:15,L,4939.71,0.1224,604.620504,-14.652504,-2.42,0,1.867824,0.31,-14.652504,-2.42,-14.652504,-1.47,1 +2,Exit long,2025-08-25 04:15,timeout,4737.81,0.8276,4088.103996,-167.09244,-4.09,0,12.629176,0.31,-196.33983,-4.80,-181.74495,-18.17,4 +2,Entry long,2025-08-25 03:15,L,4939.71,0.8276,4088.103996,-167.09244,-4.09,0,12.629176,0.31,-196.33983,-4.80,-181.74495,-18.17,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/meta.json new file mode 100644 index 00000000..1aa6ca92 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-pooc-chart-0109-0115", + "slug": "pm2-i3-pooc-chart-0109-0115", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:37:40.264Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-07", + "to": "2026-01-11" + }, + "pine_sha256": "3f0be80a1ec4eb3bd1b6a76f7ecf9d15a7d949e00792c58b001f4fada24651bb", + "tv_trades_sha256": "4dea9978d6d5e8869ccd2b11e87e22ca57f7f466c99b58e5fa822532452f9a4e", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/metrics.json new file mode 100644 index 00000000..92273892 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-pooc-chart-0109-0115", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-07", + "to": "2026-01-11", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "3f0be80a1ec4eb3bd1b6a76f7ecf9d15a7d949e00792c58b001f4fada24651bb", + "tvTradesCsvHash": "4dea9978d6d5e8869ccd2b11e87e22ca57f7f466c99b58e5fa822532452f9a4e", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75204", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1767744000000, + "to": 1768089600000 + }, + "returnedRange": { + "from": 1767744000000, + "to": 1768089600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "5f10f34d7ffb493efc04e45cd8cbbc3eec5809ef511bf181d49231387db21123", + "decodedReportSha256": "300d2d719475250d52623f16685e25f6629996fb77a13c3a844cd50186a3061b", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:37:40.263Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/strategy.pine new file mode 100644 index 00000000..e7057c7f --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 under process_orders_on_close): one leveraged long (margin 20, 4.846 x equity) +// placed and filled at the close of the signal bar 2026-01-09 01:15 UTC and carried into the next bar, which +// first crosses the maintenance line at an early 1-minute low and reaches a deeper low later. +// process_orders_on_close ON, magnifier OFF. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-pooc-chart-0109-0115", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=true) +if time == timestamp("UTC", 2026, 1, 9, 1, 15) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.846 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/tv_trades.csv new file mode 100644 index 00000000..bf6e3a1e --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0109-0115/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2026-01-09 09:30,Margin call,3077.46,0.2864,898.777616,-17.393072,-1.94,0,2.809584,0.31,-17.393072,-1.94,-17.393072,-1.74,1 +1,Entry long,2026-01-09 09:15,L,3138.19,0.2864,898.777616,-17.393072,-1.94,0,2.809584,0.31,-17.393072,-1.94,-17.393072,-1.74,1 +2,Exit long,2026-01-09 10:00,timeout,3122.09,1.2576,3946.587744,-20.24736,-0.51,0,12.337056,0.31,-76.37405,-1.94,-37.64043,-3.76,3 +2,Entry long,2026-01-09 09:15,L,3138.19,1.2576,3946.587744,-20.24736,-0.51,0,12.337056,0.31,-76.37405,-1.94,-37.64043,-3.76,3 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/meta.json new file mode 100644 index 00000000..4342d45b --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-pooc-chart-0402-2000", + "slug": "pm2-i3-pooc-chart-0402-2000", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:37:47.985Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-31", + "to": "2025-04-04" + }, + "pine_sha256": "ca87071f208e260cb53c2657b2c06d9fbfe40c95aac9fba97800b8c8398fdb50", + "tv_trades_sha256": "c592ff0981fbb444f716e304bf41b7f7fd084b77dabf547ca16756f33b907109", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/metrics.json new file mode 100644 index 00000000..38369b83 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-pooc-chart-0402-2000", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-31", + "to": "2025-04-04", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "ca87071f208e260cb53c2657b2c06d9fbfe40c95aac9fba97800b8c8398fdb50", + "tvTradesCsvHash": "c592ff0981fbb444f716e304bf41b7f7fd084b77dabf547ca16756f33b907109", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75210", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743379200000, + "to": 1743724800000 + }, + "returnedRange": { + "from": 1743379200000, + "to": 1743724800000 + }, + "rangeProof": "covered", + "rawFrameSha256": "8b20db2d6bae2039c526fbaa7c1ff465d406a7d9b7cdc3c920fadf3570e6ee37", + "decodedReportSha256": "a04a36c94bf63177e421d7036d0c5c1fc95de162556d057611e19b3f660336fd", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:37:47.984Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/strategy.pine new file mode 100644 index 00000000..6081c127 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 under process_orders_on_close): one leveraged long (margin 20, 4.785 x equity) +// placed and filled at the close of the signal bar 2025-04-02 20:00 UTC and carried into the next bar, which +// first crosses the maintenance line at an early 1-minute low and reaches a deeper low later. +// process_orders_on_close ON, magnifier OFF. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-pooc-chart-0402-2000", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=true) +if time == timestamp("UTC", 2025, 4, 2, 20, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.785 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/tv_trades.csv new file mode 100644 index 00000000..dd719b53 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0402-2000/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-03 04:15,Margin call,1872.48,0.8216,1588.243176,-49.813606,-3.14,0,20.424976,1.29,-49.813606,-3.14,-49.813606,-4.98,1 +1,Entry long,2025-04-03 04:00,L,1933.11,0.8216,1588.243176,-49.813606,-3.14,0,20.424976,1.29,-49.813606,-3.14,-49.813606,-4.98,1 +2,Exit long,2025-04-03 04:45,timeout,1880,1.6534,3196.204074,-87.81207,-2.75,0,41.103523,1.29,-112.613075,-3.52,-137.62569,-13.76,3 +2,Entry long,2025-04-03 04:00,L,1933.11,1.6534,3196.204074,-87.81207,-2.75,0,41.103523,1.29,-112.613075,-3.52,-137.62569,-13.76,3 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/meta.json new file mode 100644 index 00000000..f23fe83b --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-pooc-chart-0707-1600", + "slug": "pm2-i3-pooc-chart-0707-1600", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:37:52.467Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-05", + "to": "2025-07-09" + }, + "pine_sha256": "81601358c06775c9fc71dadab39f3f55be13c0553d8ebca38f7f45b02944b308", + "tv_trades_sha256": "7093ef952abe4bdccc758fe993c5dec0821330b843b4c1f9543515bc3f9a4d0b", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/metrics.json new file mode 100644 index 00000000..8df8a66d --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-pooc-chart-0707-1600", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-05", + "to": "2025-07-09", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "81601358c06775c9fc71dadab39f3f55be13c0553d8ebca38f7f45b02944b308", + "tvTradesCsvHash": "7093ef952abe4bdccc758fe993c5dec0821330b843b4c1f9543515bc3f9a4d0b", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75214", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1751673600000, + "to": 1752019200000 + }, + "returnedRange": { + "from": 1751673600000, + "to": 1752019200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f22bbd798a8b4fda3fbf72b61bcc8f89e1fbac586d766f984f580ccbbcab81e5", + "decodedReportSha256": "c9c3adfbb4ca24288de6cb61528e4b9a88b654cda6ec0ac9b4b3db0504d3fcd7", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:37:52.467Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/strategy.pine new file mode 100644 index 00000000..c623573c --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 under process_orders_on_close): one leveraged long (margin 20, 4.943 x equity) +// placed and filled at the close of the signal bar 2025-07-07 16:00 UTC and carried into the next bar, which +// first crosses the maintenance line at an early 1-minute low and reaches a deeper low later. +// process_orders_on_close ON, magnifier OFF. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-pooc-chart-0707-1600", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=true) +if time == timestamp("UTC", 2025, 7, 7, 16, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.943 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/tv_trades.csv new file mode 100644 index 00000000..48020603 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-0707-1600/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-08 00:15,Margin call,2505.37,0.2712,687.36996,-7.913616,-1.15,0,1.827888,0.27,-7.913616,-1.15,-7.913616,-0.79,1 +1,Entry long,2025-07-08 00:00,L,2534.55,0.2712,687.36996,-7.913616,-1.15,0,1.827888,0.27,-7.913616,-1.15,-7.913616,-0.79,1 +2,Exit long,2025-07-08 00:45,timeout,2523.24,1.6788,4255.00254,-18.987228,-0.45,0,11.315112,0.27,-48.987385,-1.15,-26.900845,-2.69,3 +2,Entry long,2025-07-08 00:00,L,2534.55,1.6788,4255.00254,-18.987228,-0.45,0,11.315112,0.27,-48.987385,-1.15,-26.900845,-2.69,3 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/meta.json new file mode 100644 index 00000000..85d04cd0 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-pooc-chart-1010-1900", + "slug": "pm2-i3-pooc-chart-1010-1900", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:38:13.859Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-10-08", + "to": "2025-10-12" + }, + "pine_sha256": "945c138ec9482c64b46647d08cec8d16ead48920d71bfd49b3ee244cc6fb9a40", + "tv_trades_sha256": "34e625565aa350318e2552ddbb8a4abd69230f9a5be19daedff8d6577b6aba18", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/metrics.json new file mode 100644 index 00000000..568cea46 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-pooc-chart-1010-1900", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-10-08", + "to": "2025-10-12", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "945c138ec9482c64b46647d08cec8d16ead48920d71bfd49b3ee244cc6fb9a40", + "tvTradesCsvHash": "34e625565aa350318e2552ddbb8a4abd69230f9a5be19daedff8d6577b6aba18", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75221", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1759881600000, + "to": 1760227200000 + }, + "returnedRange": { + "from": 1759881600000, + "to": 1760227200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "6187ccea09026a7b2023d54a9add5ecbd9d679eb61966597dcfd6df1a8fe0c2b", + "decodedReportSha256": "18061d380d29596316edb448d86ec2258c08c4b32a2c6aa1ead67db5f70cf0fb", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:38:13.858Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/strategy.pine new file mode 100644 index 00000000..53fe25de --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 under process_orders_on_close): one leveraged long (margin 20, 4.86 x equity) +// placed and filled at the close of the signal bar 2025-10-10 19:00 UTC and carried into the next bar, which +// first crosses the maintenance line at an early 1-minute low and reaches a deeper low later. +// process_orders_on_close ON, magnifier OFF. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-pooc-chart-1010-1900", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=true) +if time == timestamp("UTC", 2025, 10, 10, 19, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.86 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/tv_trades.csv new file mode 100644 index 00000000..25ae2484 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-chart-1010-1900/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-10-11 03:15,Margin call,3950,0.2436,981.496068,-19.276068,-1.96,0,0.323988,0.03,-19.276068,-1.96,-19.276068,-1.93,1 +1,Entry long,2025-10-11 03:00,L,4029.13,0.2436,981.496068,-19.276068,-1.96,0,0.323988,0.03,-19.276068,-1.96,-19.276068,-1.93,1 +2,Exit long,2025-10-11 03:45,timeout,3994.99,0.9624,3877.634712,-32.856335,-0.85,0,6.765672,0.17,-78.300865,-2.02,-52.132404,-5.21,3 +2,Entry long,2025-10-11 03:00,L,4029.13,0.9624,3877.634712,-32.856335,-0.85,0,6.765672,0.17,-78.300865,-2.02,-52.132404,-5.21,3 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/meta.json new file mode 100644 index 00000000..cb6d00ce --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-pooc-mag-0109-0115", + "slug": "pm2-i3-pooc-mag-0109-0115", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:38:24.672Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-07", + "to": "2026-01-11" + }, + "pine_sha256": "ee2e43fe0466583bb2a3f99ba92431770774a660e7ad54e53631e2b1b4e2828a", + "tv_trades_sha256": "a0d512ec52485a023aff33857054a48f3b37840f9ba0562a8f9059dd72874996", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/metrics.json new file mode 100644 index 00000000..0023e7b8 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-pooc-mag-0109-0115", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-07", + "to": "2026-01-11", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "ee2e43fe0466583bb2a3f99ba92431770774a660e7ad54e53631e2b1b4e2828a", + "tvTradesCsvHash": "a0d512ec52485a023aff33857054a48f3b37840f9ba0562a8f9059dd72874996", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75227", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1767744000000, + "to": 1768089600000 + }, + "returnedRange": { + "from": 1767744000000, + "to": 1768089600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "530f5687ff1c351c7ae5a9415f082736db64aba392114d22e062c52f86192df1", + "decodedReportSha256": "1ea71ef3f5be5fb8ad135b14f2ae79832fd3aacfb4716ab98bb9b56b41e5f7d5", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:38:24.672Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/strategy.pine new file mode 100644 index 00000000..4da23ae7 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 under process_orders_on_close): one leveraged long (margin 20, 4.846 x equity) +// placed and filled at the close of the signal bar 2026-01-09 01:15 UTC and carried into the next bar, which +// first crosses the maintenance line at an early 1-minute low and reaches a deeper low later. +// process_orders_on_close ON, magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-pooc-mag-0109-0115", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=true, use_bar_magnifier=true) +if time == timestamp("UTC", 2026, 1, 9, 1, 15) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.846 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/tv_trades.csv new file mode 100644 index 00000000..3aa853ef --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0109-0115/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2026-01-09 09:30,Margin call,3096.44,0.1332,418.006908,-5.5611,-1.33,0,1.306692,0.31,-5.5611,-1.33,-5.5611,-0.56,1 +1,Entry long,2026-01-09 09:15,L,3138.19,0.1332,418.006908,-5.5611,-1.33,0,1.306692,0.31,-5.5611,-1.33,-5.5611,-0.56,1 +2,Exit long,2026-01-09 10:00,timeout,3122.09,1.4108,4427.358452,-22.71388,-0.51,0,13.839948,0.31,-85.67789,-1.94,-28.27498,-2.83,3 +2,Entry long,2026-01-09 09:15,L,3138.19,1.4108,4427.358452,-22.71388,-0.51,0,13.839948,0.31,-85.67789,-1.94,-28.27498,-2.83,3 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/meta.json new file mode 100644 index 00000000..a65c4d7c --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-pooc-mag-0402-2000", + "slug": "pm2-i3-pooc-mag-0402-2000", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:38:29.023Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-31", + "to": "2025-04-04" + }, + "pine_sha256": "d55d56d6b70bd3f99c5a4e6d3d4cafbaeb56b20a8e77c0932947ca5c21d3bdb1", + "tv_trades_sha256": "70d257b985aec02f4c74ae88cc1e2e1adfc2d28b42cf9a51a3a652958e1fb009", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/metrics.json new file mode 100644 index 00000000..1f060d88 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-pooc-mag-0402-2000", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-31", + "to": "2025-04-04", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "d55d56d6b70bd3f99c5a4e6d3d4cafbaeb56b20a8e77c0932947ca5c21d3bdb1", + "tvTradesCsvHash": "70d257b985aec02f4c74ae88cc1e2e1adfc2d28b42cf9a51a3a652958e1fb009", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75232", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743379200000, + "to": 1743724800000 + }, + "returnedRange": { + "from": 1743379200000, + "to": 1743724800000 + }, + "rangeProof": "covered", + "rawFrameSha256": "173f4b188be16ded259badbe14d08969fc886cee12c521a2ad071881e945df70", + "decodedReportSha256": "62ece371b24758ad5d7ed58ed9cffd56494f6ebbb80d7cc726ac94958f9a14a7", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:38:29.023Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/strategy.pine new file mode 100644 index 00000000..ac386d38 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 under process_orders_on_close): one leveraged long (margin 20, 4.785 x equity) +// placed and filled at the close of the signal bar 2025-04-02 20:00 UTC and carried into the next bar, which +// first crosses the maintenance line at an early 1-minute low and reaches a deeper low later. +// process_orders_on_close ON, magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-pooc-mag-0402-2000", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=true, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 4, 2, 20, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.785 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/tv_trades.csv new file mode 100644 index 00000000..e3a875d0 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0402-2000/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-03 04:15,Margin call,1908.17,0.0656,126.812016,-1.636064,-1.29,0,0,0.00,-1.636064,-1.29,-1.636064,-0.16,1 +1,Entry long,2025-04-03 04:00,L,1933.11,0.0656,126.812016,-1.636064,-1.29,0,0,0.00,-1.636064,-1.29,-1.636064,-0.16,1 +2,Exit long,2025-04-03 04:15,Margin call,1888.67,0.1992,385.075512,-8.852448,-2.30,0,4.952112,1.29,-8.852448,-2.30,-10.488512,-1.05,1 +2,Entry long,2025-04-03 04:00,L,1933.11,0.1992,385.075512,-8.852448,-2.30,0,4.952112,1.29,-8.852448,-2.30,-10.488512,-1.05,1 +3,Exit long,2025-04-03 04:45,timeout,1880,2.2102,4272.559722,-117.38372,-2.75,0,54.94557,1.29,-150.53673,-3.52,-127.87223,-12.79,3 +3,Entry long,2025-04-03 04:00,L,1933.11,2.2102,4272.559722,-117.38372,-2.75,0,54.94557,1.29,-150.53673,-3.52,-127.87223,-12.79,3 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/meta.json new file mode 100644 index 00000000..77736928 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-pooc-mag-0707-1600", + "slug": "pm2-i3-pooc-mag-0707-1600", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:38:38.248Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-05", + "to": "2025-07-09" + }, + "pine_sha256": "d72d51aab2412d7f851db7b066ef3ef6c1cbbcf4794a32390ce4a458b2f709ac", + "tv_trades_sha256": "7093ef952abe4bdccc758fe993c5dec0821330b843b4c1f9543515bc3f9a4d0b", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/metrics.json new file mode 100644 index 00000000..778872a1 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-pooc-mag-0707-1600", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-05", + "to": "2025-07-09", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "d72d51aab2412d7f851db7b066ef3ef6c1cbbcf4794a32390ce4a458b2f709ac", + "tvTradesCsvHash": "7093ef952abe4bdccc758fe993c5dec0821330b843b4c1f9543515bc3f9a4d0b", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75236", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1751673600000, + "to": 1752019200000 + }, + "returnedRange": { + "from": 1751673600000, + "to": 1752019200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "ed342f7338c0dd0cc2ea450395c55daa7558d3015a7b6148319cf6a3dc80e636", + "decodedReportSha256": "def8a435bfb90ff19f961a3fc4369118bea46857b63359312da5415994a64747", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:38:38.247Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/strategy.pine new file mode 100644 index 00000000..75a1c7e1 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 under process_orders_on_close): one leveraged long (margin 20, 4.943 x equity) +// placed and filled at the close of the signal bar 2025-07-07 16:00 UTC and carried into the next bar, which +// first crosses the maintenance line at an early 1-minute low and reaches a deeper low later. +// process_orders_on_close ON, magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-pooc-mag-0707-1600", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=true, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 7, 7, 16, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.943 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/tv_trades.csv new file mode 100644 index 00000000..48020603 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-0707-1600/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-08 00:15,Margin call,2505.37,0.2712,687.36996,-7.913616,-1.15,0,1.827888,0.27,-7.913616,-1.15,-7.913616,-0.79,1 +1,Entry long,2025-07-08 00:00,L,2534.55,0.2712,687.36996,-7.913616,-1.15,0,1.827888,0.27,-7.913616,-1.15,-7.913616,-0.79,1 +2,Exit long,2025-07-08 00:45,timeout,2523.24,1.6788,4255.00254,-18.987228,-0.45,0,11.315112,0.27,-48.987385,-1.15,-26.900845,-2.69,3 +2,Entry long,2025-07-08 00:00,L,2534.55,1.6788,4255.00254,-18.987228,-0.45,0,11.315112,0.27,-48.987385,-1.15,-26.900845,-2.69,3 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/meta.json new file mode 100644 index 00000000..a4710a9c --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-pooc-mag-1010-1900", + "slug": "pm2-i3-pooc-mag-1010-1900", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:38:42.132Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-10-08", + "to": "2025-10-12" + }, + "pine_sha256": "0d4699ff315e470804f4246c3d0c8d29843189ffdddc2396ad16034ba391df16", + "tv_trades_sha256": "c9aa3967619f2f8abc9a90c20140d47597f44c5a1097b4513b771dc550c53f9b", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/metrics.json new file mode 100644 index 00000000..de52d268 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-pooc-mag-1010-1900", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-10-08", + "to": "2025-10-12", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "0d4699ff315e470804f4246c3d0c8d29843189ffdddc2396ad16034ba391df16", + "tvTradesCsvHash": "c9aa3967619f2f8abc9a90c20140d47597f44c5a1097b4513b771dc550c53f9b", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75238", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1759881600000, + "to": 1760227200000 + }, + "returnedRange": { + "from": 1759881600000, + "to": 1760227200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "8e7a2a128c2e8d7325dd764e959cc1941bfe53d90c4b406f42d46ed1ed8f7f2c", + "decodedReportSha256": "444d0a67836d5ec587cd74e803e36046a90c22572d54d1a8eb3be2e4b9795d2f", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:38:42.131Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/strategy.pine new file mode 100644 index 00000000..ee4a296a --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 under process_orders_on_close): one leveraged long (margin 20, 4.86 x equity) +// placed and filled at the close of the signal bar 2025-10-10 19:00 UTC and carried into the next bar, which +// first crosses the maintenance line at an early 1-minute low and reaches a deeper low later. +// process_orders_on_close ON, magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-pooc-mag-1010-1900", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=20, margin_short=20, commission_value=0, slippage=0, + process_orders_on_close=true, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 10, 10, 19, 0) and strategy.position_size == 0 + strategy.entry("L", strategy.long, qty = math.floor(4.86 * strategy.equity / close * 1000) / 1000) +if strategy.position_size > 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("L", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/tv_trades.csv new file mode 100644 index 00000000..2fcdc674 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-pooc-mag-1010-1900/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-10-11 03:15,Margin call,3991.74,0.0392,157.941896,-1.465688,-0.93,0,0.323008,0.20,-1.465688,-0.93,-1.465688,-0.15,1 +1,Entry long,2025-10-11 03:00,L,4029.13,0.0392,157.941896,-1.465688,-0.93,0,0.323008,0.20,-1.465688,-0.93,-1.465688,-0.15,1 +2,Exit long,2025-10-11 03:15,Margin call,3950,0.0788,317.495444,-6.235444,-1.96,0,0.649312,0.20,-6.235444,-1.96,-7.701132,-0.77,1 +2,Entry long,2025-10-11 03:00,L,4029.13,0.0788,317.495444,-6.235444,-1.96,0,0.649312,0.20,-6.235444,-1.96,-7.701132,-0.77,1 +3,Exit long,2025-10-11 03:45,timeout,3994.99,1.088,4383.69344,-37.14432,-0.85,0,8.96512,0.20,-88.51968,-2.02,-44.84545,-4.48,3 +3,Entry long,2025-10-11 03:00,L,4029.13,1.088,4383.69344,-37.14432,-0.85,0,8.96512,0.20,-88.51968,-2.02,-44.84545,-4.48,3 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/meta.json new file mode 100644 index 00000000..bff5517e --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-s1x-chart-0201-1100", + "slug": "pm2-i3-s1x-chart-0201-1100", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:38:45.870Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-30", + "to": "2026-02-03" + }, + "pine_sha256": "0ab7d3a61fbbd6bdf1d7c31a5b89e8b29135bcebb29d8ca282da058c05703fc5", + "tv_trades_sha256": "50f0f64a37926674b3f89b74ef8d1767dc15d032056e18f7d866d7563acba600", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/metrics.json new file mode 100644 index 00000000..4c9b527b --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-s1x-chart-0201-1100", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-30", + "to": "2026-02-03", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "0ab7d3a61fbbd6bdf1d7c31a5b89e8b29135bcebb29d8ca282da058c05703fc5", + "tvTradesCsvHash": "50f0f64a37926674b3f89b74ef8d1767dc15d032056e18f7d866d7563acba600", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75240", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1769731200000, + "to": 1770076800000 + }, + "returnedRange": { + "from": 1769731200000, + "to": 1770076800000 + }, + "rangeProof": "covered", + "rawFrameSha256": "ddcf753c4094c6c406342e405c79d79f8e5db79eda86db1820218d04b70aa0c3", + "decodedReportSha256": "c5257f6a61f4d540f4501fcdaab0be9454826a4e4b77fd8125360fb66ce22218", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:38:45.870Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/strategy.pine new file mode 100644 index 00000000..1abf1bb2 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 for a full-margin book): one 1x short (margin 100, 0.996 x equity) +// placed on the signal bar 2026-02-01 11:00 UTC, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute high and reaches a higher high later. +// Magnifier OFF. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-s1x-chart-0201-1100", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=100, margin_short=100, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2026, 2, 1, 11, 0) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(0.996 * strategy.equity / close * 1000) / 1000) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/tv_trades.csv new file mode 100644 index 00000000..3a8f6f13 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0201-1100/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2026-02-01 19:30,Margin call,2423.66,0.0352,84.197344,-1.115488,-1.32,0,0.343552,0.41,-1.115488,-1.32,-1.115488,-0.11,1 +1,Entry short,2026-02-01 19:15,S,2391.97,0.0352,84.197344,-1.115488,-1.32,0,0.343552,0.41,-1.115488,-1.32,-1.115488,-0.11,1 +2,Exit short,2026-02-01 20:15,timeout,2405.51,0.3808,910.862176,-5.156032,-0.57,0,3.716608,0.41,-12.067552,-1.32,-6.27152,-0.63,4 +2,Entry short,2026-02-01 19:15,S,2391.97,0.3808,910.862176,-5.156032,-0.57,0,3.716608,0.41,-12.067552,-1.32,-6.27152,-0.63,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/meta.json new file mode 100644 index 00000000..71a9c26b --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-s1x-chart-0407-1145", + "slug": "pm2-i3-s1x-chart-0407-1145", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:39:18.065Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-05", + "to": "2025-04-09" + }, + "pine_sha256": "cbc43aa362a57cd7a85d9230c47acd44714a8314c62307999cc7d5c33dc7620a", + "tv_trades_sha256": "7f3c307947114fbada31dc60c3ac78d4458f5c787a3318bdb7f529166ad35808", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/metrics.json new file mode 100644 index 00000000..be90bd24 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-s1x-chart-0407-1145", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-05", + "to": "2025-04-09", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "cbc43aa362a57cd7a85d9230c47acd44714a8314c62307999cc7d5c33dc7620a", + "tvTradesCsvHash": "7f3c307947114fbada31dc60c3ac78d4458f5c787a3318bdb7f529166ad35808", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75250", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743811200000, + "to": 1744156800000 + }, + "returnedRange": { + "from": 1743811200000, + "to": 1744156800000 + }, + "rangeProof": "covered", + "rawFrameSha256": "44469cd4f789532074e9e02269f1d1d7361d4d59b404884b8b218192b8d38317", + "decodedReportSha256": "2143c6e40d49343425977784d2612b2dbe8dcc7f6cb01e6ba093c70574968376", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:39:18.064Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/strategy.pine new file mode 100644 index 00000000..344c2d53 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 for a full-margin book): one 1x short (margin 100, 0.985 x equity) +// placed on the signal bar 2025-04-07 11:45 UTC, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute high and reaches a higher high later. +// Magnifier OFF. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-s1x-chart-0407-1145", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=100, margin_short=100, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 4, 7, 11, 45) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(0.985 * strategy.equity / close * 1000) / 1000) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/tv_trades.csv new file mode 100644 index 00000000..933e4167 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0407-1145/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-04-07 20:15,Margin call,1519.12,0.0872,129.260048,-3.207216,-2.48,0,0.450824,0.35,-3.207216,-2.48,-3.207216,-0.32,1 +1,Entry short,2025-04-07 20:00,S,1482.34,0.0872,129.260048,-3.207216,-2.48,0,0.450824,0.35,-3.207216,-2.48,-3.207216,-0.32,1 +2,Exit short,2025-04-07 21:00,timeout,1520.01,0.5768,855.013712,-21.728056,-2.54,0,2.982056,0.35,-25.459951,-2.98,-24.935272,-2.49,4 +2,Entry short,2025-04-07 20:00,S,1482.34,0.5768,855.013712,-21.728056,-2.54,0,2.982056,0.35,-25.459951,-2.98,-24.935272,-2.49,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/meta.json new file mode 100644 index 00000000..c792f7fe --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-s1x-chart-0623-2130", + "slug": "pm2-i3-s1x-chart-0623-2130", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:39:27.888Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-21", + "to": "2025-06-25" + }, + "pine_sha256": "e9a04aff1052615a519b98dd0ed66c011d5e552d16aab896a96aa32e85c36707", + "tv_trades_sha256": "8f7d4ee9bdb9ff4bad472545df9cf0fa51fa684268c41edc8ebc6cc577adc2fa", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/metrics.json new file mode 100644 index 00000000..b338dab4 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-s1x-chart-0623-2130", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-21", + "to": "2025-06-25", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "e9a04aff1052615a519b98dd0ed66c011d5e552d16aab896a96aa32e85c36707", + "tvTradesCsvHash": "8f7d4ee9bdb9ff4bad472545df9cf0fa51fa684268c41edc8ebc6cc577adc2fa", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75253", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1750464000000, + "to": 1750809600000 + }, + "returnedRange": { + "from": 1750464000000, + "to": 1750809600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "df432facd14c9ab68625b52fdf778cdcfe4c25aff59cff90956799f5af632135", + "decodedReportSha256": "5e8a07a12b272c633190ebb9382bce8d7dced1e5d2c08668cf67f80d09e8f57c", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:39:27.888Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/strategy.pine new file mode 100644 index 00000000..35f3e40d --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 for a full-margin book): one 1x short (margin 100, 0.991 x equity) +// placed on the signal bar 2025-06-23 21:30 UTC, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute high and reaches a higher high later. +// Magnifier OFF. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-s1x-chart-0623-2130", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=100, margin_short=100, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 6, 23, 21, 30) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(0.991 * strategy.equity / close * 1000) / 1000) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/tv_trades.csv new file mode 100644 index 00000000..0311d667 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-0623-2130/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-06-24 06:00,Margin call,2409.52,0.0796,186.3834,-5.414392,-2.90,0,0.8358,0.45,-5.414392,-2.90,-5.414392,-0.54,1 +1,Entry short,2025-06-24 05:45,S,2341.5,0.0796,186.3834,-5.414392,-2.90,0,0.8358,0.45,-5.414392,-2.90,-5.414392,-0.54,1 +2,Exit short,2025-06-24 06:45,timeout,2394.2,0.3434,804.0711,-18.09718,-2.25,0,3.6057,0.45,-33.8249,-4.21,-23.511572,-2.35,4 +2,Entry short,2025-06-24 05:45,S,2341.5,0.3434,804.0711,-18.09718,-2.25,0,3.6057,0.45,-33.8249,-4.21,-23.511572,-2.35,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/meta.json new file mode 100644 index 00000000..ea9383bb --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-s1x-chart-1104-2130", + "slug": "pm2-i3-s1x-chart-1104-2130", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:39:31.566Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-11-02", + "to": "2025-11-06" + }, + "pine_sha256": "bf34f154872addd68f4186d991435afe0d6b839337bcd7e176a08f579c749e0d", + "tv_trades_sha256": "098b32d635fd010de1bee7404d8861fb0619ab95e920a06bf217b343b1f3bcfe", + "tv_trades_rows": 4 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/metrics.json new file mode 100644 index 00000000..333cbda6 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-s1x-chart-1104-2130", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-11-02", + "to": "2025-11-06", + "deepBacktesting": true, + "trades": 2, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "bf34f154872addd68f4186d991435afe0d6b839337bcd7e176a08f579c749e0d", + "tvTradesCsvHash": "098b32d635fd010de1bee7404d8861fb0619ab95e920a06bf217b343b1f3bcfe", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75255", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1762041600000, + "to": 1762387200000 + }, + "returnedRange": { + "from": 1762041600000, + "to": 1762387200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "25a81c98b4ba939e62cfc119d539d2dd25b6e41373fd4fccf49580343ec2f6df", + "decodedReportSha256": "101c65e81480e94c775041c31040cc9baa4df18571dad8ddd2d00c45dfec2410", + "closedTrades": 2, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:39:31.565Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/strategy.pine new file mode 100644 index 00000000..44113c1f --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 for a full-margin book): one 1x short (margin 100, 0.926 x equity) +// placed on the signal bar 2025-11-04 21:30 UTC, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute high and reaches a higher high later. +// Magnifier OFF. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-s1x-chart-1104-2130", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=100, margin_short=100, commission_value=0, slippage=0, + process_orders_on_close=false) +if time == timestamp("UTC", 2025, 11, 4, 21, 30) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(0.926 * strategy.equity / close * 1000) / 1000) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/tv_trades.csv new file mode 100644 index 00000000..63003d9a --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-chart-1104-2130/tv_trades.csv @@ -0,0 +1,5 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-11-05 06:00,Margin call,3271.1,0.0292,90.697244,-4.818876,-5.31,0,0.0438,0.05,-4.818876,-5.31,-4.818876,-0.48,1 +1,Entry short,2025-11-05 05:45,S,3106.07,0.0292,90.697244,-4.818876,-5.31,0,0.0438,0.05,-4.818876,-5.31,-4.818876,-0.48,1 +2,Exit short,2025-11-05 06:45,timeout,3250.87,0.2688,834.911616,-38.92224,-4.66,0,0.4032,0.05,-49.34899,-5.91,-43.741116,-4.37,4 +2,Entry short,2025-11-05 05:45,S,3106.07,0.2688,834.911616,-38.92224,-4.66,0,0.4032,0.05,-49.34899,-5.91,-43.741116,-4.37,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/meta.json new file mode 100644 index 00000000..d7c52b3a --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-s1x-mag-0201-1100", + "slug": "pm2-i3-s1x-mag-0201-1100", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:39:35.168Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-30", + "to": "2026-02-03" + }, + "pine_sha256": "bb7f4784d95ffbcfd03a2a1f246fcd6bd51765ccf6ca9a29b0520af7f9ef8380", + "tv_trades_sha256": "a72c25a00038c741f1d97023995059933962f0b65190da5f090a9c4b76ad40b0", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/metrics.json new file mode 100644 index 00000000..58379849 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-s1x-mag-0201-1100", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-30", + "to": "2026-02-03", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "bb7f4784d95ffbcfd03a2a1f246fcd6bd51765ccf6ca9a29b0520af7f9ef8380", + "tvTradesCsvHash": "a72c25a00038c741f1d97023995059933962f0b65190da5f090a9c4b76ad40b0", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75256", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1769731200000, + "to": 1770076800000 + }, + "returnedRange": { + "from": 1769731200000, + "to": 1770076800000 + }, + "rangeProof": "covered", + "rawFrameSha256": "31f62d4cb3fab9d868b5f3c5a0ca69fc66686e875dad0df7529665108d841a57", + "decodedReportSha256": "c2dfc47de9780b150ba846f8a3b160663d877a196b46a7e845e139e879068ff3", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:39:35.167Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/strategy.pine new file mode 100644 index 00000000..115c8c4d --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 for a full-margin book): one 1x short (margin 100, 0.996 x equity) +// placed on the signal bar 2026-02-01 11:00 UTC, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute high and reaches a higher high later. +// Magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-s1x-mag-0201-1100", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=100, margin_short=100, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=true) +if time == timestamp("UTC", 2026, 2, 1, 11, 0) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(0.996 * strategy.equity / close * 1000) / 1000) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/tv_trades.csv new file mode 100644 index 00000000..cd92521c --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0201-1100/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2026-02-01 19:30,Margin call,2402.65,0.0064,15.308608,-0.068352,-0.45,0,0.062464,0.41,-0.068352,-0.45,-0.068352,-0.01,1 +1,Entry short,2026-02-01 19:15,S,2391.97,0.0064,15.308608,-0.068352,-0.45,0,0.062464,0.41,-0.068352,-0.45,-0.068352,-0.01,1 +2,Exit short,2026-02-01 19:30,Margin call,2423.66,0.0092,22.006124,-0.291548,-1.32,0,0.089792,0.41,-0.291548,-1.32,-0.3599,-0.04,1 +2,Entry short,2026-02-01 19:15,S,2391.97,0.0092,22.006124,-0.291548,-1.32,0,0.089792,0.41,-0.291548,-1.32,-0.3599,-0.04,1 +3,Exit short,2026-02-01 20:15,timeout,2405.51,0.4004,957.744788,-5.421416,-0.57,0,3.907904,0.41,-12.688676,-1.32,-5.781316,-0.58,4 +3,Entry short,2026-02-01 19:15,S,2391.97,0.4004,957.744788,-5.421416,-0.57,0,3.907904,0.41,-12.688676,-1.32,-5.781316,-0.58,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/meta.json new file mode 100644 index 00000000..3caec5f2 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-s1x-mag-0407-1145", + "slug": "pm2-i3-s1x-mag-0407-1145", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:39:38.774Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-05", + "to": "2025-04-09" + }, + "pine_sha256": "5967e69c555ae7f84bbeca9c09a00e481195ef81d6db9f713e15d162f181fed1", + "tv_trades_sha256": "4a145425eba491a3245b051afb9fb7123ede123ac07b2d4cbf1881859c466a69", + "tv_trades_rows": 10 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/metrics.json new file mode 100644 index 00000000..857c2b0b --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-s1x-mag-0407-1145", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-05", + "to": "2025-04-09", + "deepBacktesting": true, + "trades": 5, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "5967e69c555ae7f84bbeca9c09a00e481195ef81d6db9f713e15d162f181fed1", + "tvTradesCsvHash": "4a145425eba491a3245b051afb9fb7123ede123ac07b2d4cbf1881859c466a69", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75257", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743811200000, + "to": 1744156800000 + }, + "returnedRange": { + "from": 1743811200000, + "to": 1744156800000 + }, + "rangeProof": "covered", + "rawFrameSha256": "d329eb9896a262a96ebcaa1acf44bfbdc7b147e4c26fa3430f096ebab2636071", + "decodedReportSha256": "516f1f30d205d51a5761a7f8456dd74d8eebf063a572a25411b43313172495a1", + "closedTrades": 5, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:39:38.773Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/strategy.pine new file mode 100644 index 00000000..188d6015 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 for a full-margin book): one 1x short (margin 100, 0.985 x equity) +// placed on the signal bar 2025-04-07 11:45 UTC, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute high and reaches a higher high later. +// Magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-s1x-mag-0407-1145", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=100, margin_short=100, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 4, 7, 11, 45) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(0.985 * strategy.equity / close * 1000) / 1000) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/tv_trades.csv new file mode 100644 index 00000000..b09fd973 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0407-1145/tv_trades.csv @@ -0,0 +1,11 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-04-07 20:15,Margin call,1498.93,0.0168,24.903312,-0.278712,-1.12,0,0.086856,0.35,-0.278712,-1.12,-0.278712,-0.03,1 +1,Entry short,2025-04-07 20:00,S,1482.34,0.0168,24.903312,-0.278712,-1.12,0,0.086856,0.35,-0.278712,-1.12,-0.278712,-0.03,1 +2,Exit short,2025-04-07 20:15,Margin call,1516,0.0084,12.451656,-0.282744,-2.27,0,0.043428,0.35,-0.282744,-2.27,-0.561456,-0.06,1 +2,Entry short,2025-04-07 20:00,S,1482.34,0.0084,12.451656,-0.282744,-2.27,0,0.043428,0.35,-0.282744,-2.27,-0.561456,-0.06,1 +3,Exit short,2025-04-07 20:30,Margin call,1523.99,0.0016,2.371744,-0.06664,-2.81,0,0.008272,0.35,-0.06664,-2.81,-0.628096,-0.06,2 +3,Entry short,2025-04-07 20:00,S,1482.34,0.0016,2.371744,-0.06664,-2.81,0,0.008272,0.35,-0.06664,-2.81,-0.628096,-0.06,2 +4,Exit short,2025-04-07 20:45,Margin call,1526.48,0.0036,5.336424,-0.158904,-2.98,0,0.018612,0.35,-0.158904,-2.98,-0.787,-0.08,3 +4,Entry short,2025-04-07 20:00,S,1482.34,0.0036,5.336424,-0.158904,-2.98,0,0.018612,0.35,-0.158904,-2.98,-0.787,-0.08,3 +5,Exit short,2025-04-07 21:00,timeout,1520.01,0.6336,939.210624,-23.867712,-2.54,0,3.275712,0.35,-27.967104,-2.98,-24.654713,-2.47,4 +5,Entry short,2025-04-07 20:00,S,1482.34,0.6336,939.210624,-23.867712,-2.54,0,3.275712,0.35,-27.967104,-2.98,-24.654713,-2.47,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/meta.json new file mode 100644 index 00000000..35fe46c6 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-s1x-mag-0623-2130", + "slug": "pm2-i3-s1x-mag-0623-2130", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:39:43.034Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-21", + "to": "2025-06-25" + }, + "pine_sha256": "befca40c1e4d043e4f44c8c664e9cf549c324db31b4452a7ddfacc227f61cace", + "tv_trades_sha256": "924d02a624c08979b9a6f4ad997f8bb2225fc4902ea053986513f792c06e791a", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/metrics.json new file mode 100644 index 00000000..ceba08d7 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-s1x-mag-0623-2130", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-21", + "to": "2025-06-25", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "befca40c1e4d043e4f44c8c664e9cf549c324db31b4452a7ddfacc227f61cace", + "tvTradesCsvHash": "924d02a624c08979b9a6f4ad997f8bb2225fc4902ea053986513f792c06e791a", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75258", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1750464000000, + "to": 1750809600000 + }, + "returnedRange": { + "from": 1750464000000, + "to": 1750809600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "34f740f47c4401f8436047b70b6461c0ebbf36800512309ac69c316e08331124", + "decodedReportSha256": "33d6f3b1c21968cbec73f09ecea0e738e5d82b9ef99392d3a4db5b652fb2ac85", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:39:43.034Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/strategy.pine new file mode 100644 index 00000000..26244a7f --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 for a full-margin book): one 1x short (margin 100, 0.991 x equity) +// placed on the signal bar 2025-06-23 21:30 UTC, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute high and reaches a higher high later. +// Magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-s1x-mag-0623-2130", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=100, margin_short=100, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 6, 23, 21, 30) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(0.991 * strategy.equity / close * 1000) / 1000) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/tv_trades.csv new file mode 100644 index 00000000..cec35d5d --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-0623-2130/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-06-24 06:00,Margin call,2369.42,0.0236,55.2594,-0.658912,-1.19,0,0.2478,0.45,-0.658912,-1.19,-0.658912,-0.07,1 +1,Entry short,2025-06-24 05:45,S,2341.5,0.0236,55.2594,-0.658912,-1.19,0,0.2478,0.45,-0.658912,-1.19,-0.658912,-0.07,1 +2,Exit short,2025-06-24 06:15,Margin call,2440,0.0236,55.2594,-2.3246,-4.21,0,0.2478,0.45,-2.3246,-4.21,-2.983512,-0.30,2 +2,Entry short,2025-06-24 05:45,S,2341.5,0.0236,55.2594,-2.3246,-4.21,0,0.2478,0.45,-2.3246,-4.21,-2.983512,-0.30,2 +3,Exit short,2025-06-24 06:45,timeout,2394.2,0.3758,879.9357,-19.80466,-2.25,0,3.9459,0.45,-37.0163,-4.21,-22.788172,-2.28,4 +3,Entry short,2025-06-24 05:45,S,2341.5,0.3758,879.9357,-19.80466,-2.25,0,3.9459,0.45,-37.0163,-4.21,-22.788172,-2.28,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/meta.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/meta.json new file mode 100644 index 00000000..b7dda75c --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-i3-s1x-mag-1104-2130", + "slug": "pm2-i3-s1x-mag-1104-2130", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:39:46.463Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-11-02", + "to": "2025-11-06" + }, + "pine_sha256": "e1f9153361439d66e0ef0cf881605e5cd23b3749dd49989d0a0dab1cfe90109b", + "tv_trades_sha256": "5bcd717fe804192c60ad5c62e159b37c18e7a908cc2bbd391659bf44ebfba097", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/metrics.json b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/metrics.json new file mode 100644 index 00000000..340a299b --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-i3-s1x-mag-1104-2130", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-11-02", + "to": "2025-11-06", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "e1f9153361439d66e0ef0cf881605e5cd23b3749dd49989d0a0dab1cfe90109b", + "tvTradesCsvHash": "5bcd717fe804192c60ad5c62e159b37c18e7a908cc2bbd391659bf44ebfba097", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75259", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1762041600000, + "to": 1762387200000 + }, + "returnedRange": { + "from": 1762041600000, + "to": 1762387200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f81b20d039301290fc1e74390fdb080e303b9be9cbc2f39ebada47a8121dc2a1", + "decodedReportSha256": "b12ef1e674cf86f4f59717ce118df758f606cac6a6f02a580b84975e00245ce5", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:39:46.462Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/strategy.pine b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/strategy.pine new file mode 100644 index 00000000..256b9134 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/strategy.pine @@ -0,0 +1,13 @@ +// R5 lane PAR-MARGIN-2 (item 3 for a full-margin book): one 1x short (margin 100, 0.926 x equity) +// placed on the signal bar 2025-11-04 21:30 UTC, filled at the next bar's open and carried into a bar that +// first crosses the maintenance line at an early 1-minute high and reaches a higher high later. +// Magnifier ON. Timeout close 3 bars after the entry bar. +//@version=6 +strategy("pm2-i3-s1x-mag-1104-2130", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + margin_long=100, margin_short=100, commission_value=0, slippage=0, + process_orders_on_close=false, use_bar_magnifier=true) +if time == timestamp("UTC", 2025, 11, 4, 21, 30) and strategy.position_size == 0 + strategy.entry("S", strategy.short, qty = math.floor(0.926 * strategy.equity / close * 1000) / 1000) +if strategy.position_size < 0 and bar_index - strategy.opentrades.entry_bar_index(0) >= 3 + strategy.close("S", comment="timeout") diff --git a/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/tv_trades.csv new file mode 100644 index 00000000..bd04c5cb --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-i3-s1x-mag-1104-2130/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit short,2025-11-05 06:00,Margin call,3238.56,0.0056,17.393992,-0.741944,-4.27,0,0.020272,0.12,-0.741944,-4.27,-0.741944,-0.07,1 +1,Entry short,2025-11-05 05:45,S,3106.07,0.0056,17.393992,-0.741944,-4.27,0,0.020272,0.12,-0.741944,-4.27,-0.741944,-0.07,1 +2,Exit short,2025-11-05 06:00,Margin call,3271.1,0.0064,19.878848,-1.056192,-5.31,0,0.023168,0.12,-1.056192,-5.31,-1.798136,-0.18,1 +2,Entry short,2025-11-05 05:45,S,3106.07,0.0064,19.878848,-1.056192,-5.31,0,0.023168,0.12,-1.056192,-5.31,-1.798136,-0.18,1 +3,Exit short,2025-11-05 06:45,timeout,3250.87,0.286,888.33602,-41.4128,-4.66,0,1.03532,0.12,-52.50674,-5.91,-43.210938,-4.32,4 +3,Entry short,2025-11-05 05:45,S,3106.07,0.286,888.33602,-41.4128,-4.66,0,1.03532,0.12,-52.50674,-5.91,-43.210938,-4.32,4 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0109/meta.json b/tests/fixtures/intrabar_margin/pm2-ltf-0109/meta.json new file mode 100644 index 00000000..688bdd92 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0109/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-ltf-0109", + "slug": "pm2-ltf-0109", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:50:56.791Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-07", + "to": "2026-01-11" + }, + "pine_sha256": "79d015b0046ce3a341a1270a7d25e3d935f0c357bbbac185f9636d4c32c7f0ef", + "tv_trades_sha256": "88eaca621366d3aa7eac60bb18b328f47299c84b08ef9bc60d2f84f3807e606d", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0109/metrics.json b/tests/fixtures/intrabar_margin/pm2-ltf-0109/metrics.json new file mode 100644 index 00000000..2c2dcf97 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0109/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-ltf-0109", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-07", + "to": "2026-01-11", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "79d015b0046ce3a341a1270a7d25e3d935f0c357bbbac185f9636d4c32c7f0ef", + "tvTradesCsvHash": "88eaca621366d3aa7eac60bb18b328f47299c84b08ef9bc60d2f84f3807e606d", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75311", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1767744000000, + "to": 1768089600000 + }, + "returnedRange": { + "from": 1767744000000, + "to": 1768089600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "a72c484898ff42863c6fff7c85f45d0c5c72f5e73c397fbb59946ea172eeb531", + "decodedReportSha256": "e72a9f1f43c797ca4607bf07036a90b264625cf1ea9fb1f1f45a181844d36f28", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:50:56.791Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0109/strategy.pine b/tests/fixtures/intrabar_margin/pm2-ltf-0109/strategy.pine new file mode 100644 index 00000000..7751c089 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0109/strategy.pine @@ -0,0 +1,30 @@ +// R5 lane PAR-MARGIN-2: TradingView's own intrabars inside the 15m bar 2026-01-09 01:30 UTC, +// read through request.security_lower_tf at "2" (the bar magnifier's intrabar timeframe for a +// 15-minute chart in TradingView's table) and at "1", printed as order comments (Signal column). +//@version=6 +strategy("pm2-ltf-0109", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=0.001, pyramiding=10) +t2 = request.security_lower_tf(syminfo.tickerid, "2", time) +o2 = request.security_lower_tf(syminfo.tickerid, "2", open) +h2 = request.security_lower_tf(syminfo.tickerid, "2", high) +l2 = request.security_lower_tf(syminfo.tickerid, "2", low) +c2 = request.security_lower_tf(syminfo.tickerid, "2", close) +l1 = request.security_lower_tf(syminfo.tickerid, "1", low) +h1 = request.security_lower_tf(syminfo.tickerid, "1", high) +mins(a) => + out = array.new_float() + for v in a + array.push(out, (v % 3600000) / 60000) + out +if time == timestamp("UTC", 2026, 1, 9, 1, 30) + strategy.entry("T", strategy.long, comment = "T" + str.tostring(mins(t2))) + strategy.entry("O", strategy.long, comment = "O" + str.tostring(o2)) + strategy.entry("H", strategy.long, comment = "H" + str.tostring(h2)) + strategy.entry("L", strategy.long, comment = "L" + str.tostring(l2)) + strategy.entry("C", strategy.long, comment = "C" + str.tostring(c2)) + strategy.entry("A", strategy.long, comment = "l1a" + str.tostring(array.slice(l1, 0, math.min(8, array.size(l1))))) + strategy.entry("B", strategy.long, comment = "l1b" + str.tostring(array.slice(l1, math.min(8, array.size(l1)), array.size(l1)))) + strategy.entry("D", strategy.long, comment = "h1a" + str.tostring(array.slice(h1, 0, math.min(8, array.size(h1))))) + strategy.entry("E", strategy.long, comment = "h1b" + str.tostring(array.slice(h1, math.min(8, array.size(h1)), array.size(h1)))) +if time == timestamp("UTC", 2026, 1, 9, 1, 45) + strategy.close_all(comment = "n2=" + str.tostring(array.size(l2)) + " n1=" + str.tostring(array.size(l1))) diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0109/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-ltf-0109/tv_trades.csv new file mode 100644 index 00000000..f016dc6b --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0109/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2026-01-09 10:00,n2=8 n1=15,3104.32,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.00651,0.00,1 +1,Entry long,2026-01-09 09:45,"T[30, 32, 34, 36, 38, 40, 42]",3097.81,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.00651,0.00,1 +2,Exit long,2026-01-09 10:00,n2=8 n1=15,3104.32,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.01302,0.00,1 +2,Entry long,2026-01-09 09:45,"O[3138.19, 3124.18, 3126.26, 3103.25, 3084.85, 3094.59, 3093.86]",3097.81,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.01302,0.00,1 +3,Exit long,2026-01-09 10:00,n2=8 n1=15,3104.32,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.01953,0.00,1 +3,Entry long,2026-01-09 09:45,"H[3148, 3127, 3126.27, 3104.22, 3096.47, 3096.3, 3097.63]",3097.81,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.01953,0.00,1 +4,Exit long,2026-01-09 10:00,n2=8 n1=15,3104.32,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.02604,0.00,1 +4,Entry long,2026-01-09 09:45,"L[3122.81, 3120, 3096.44, 3077.46, 3084.85, 3091.89, 3093.86]",3097.81,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.02604,0.00,1 +5,Exit long,2026-01-09 10:00,n2=8 n1=15,3104.32,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.03255,0.00,1 +5,Entry long,2026-01-09 09:45,"C[3124.18, 3126.26, 3103.25, 3084.86, 3094.6, 3093.87, 3096.5]",3097.81,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.03255,0.00,1 +6,Exit long,2026-01-09 10:00,n2=8 n1=15,3104.32,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.03906,0.00,1 +6,Entry long,2026-01-09 09:45,"l1a[3125.02, 3122.81, 3120, 3124.9, 3106.26, 3096.44, 3088.63, 3077.46]",3097.81,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.03906,0.00,1 +7,Exit long,2026-01-09 10:00,n2=8 n1=15,3104.32,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.04557,0.00,1 +7,Entry long,2026-01-09 09:45,"l1b[3084.85, 3092.08, 3094, 3091.89, 3093.86, 3094.13, 3095.43]",3097.81,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.04557,0.00,1 +8,Exit long,2026-01-09 10:00,n2=8 n1=15,3104.32,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.05208,0.01,1 +8,Entry long,2026-01-09 09:45,"h1a[3148, 3126.19, 3127, 3127, 3126.27, 3108.63, 3104.22, 3095.39]",3097.81,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.05208,0.01,1 +9,Exit long,2026-01-09 10:00,n2=8 n1=15,3104.32,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.05859,0.01,1 +9,Entry long,2026-01-09 09:45,"h1b[3094.83, 3096.47, 3096.3, 3094.31, 3097.63, 3096.64, 3097.91]",3097.81,0.001,3.09781,0.00651,0.21,0,0.00707,0.23,-0.00067,-0.02,0.05859,0.01,1 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0130/meta.json b/tests/fixtures/intrabar_margin/pm2-ltf-0130/meta.json new file mode 100644 index 00000000..781710b1 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0130/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-ltf-0130", + "slug": "pm2-ltf-0130", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:51:01.015Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-28", + "to": "2026-02-01" + }, + "pine_sha256": "fb12d944a1d3c04eb7e6b9c71f05352b9569df428f25aad4cf8d92bb4efad068", + "tv_trades_sha256": "75e6678ceb46c5970cc2d196f97a2eb4a185c82051af711455436d3cb2d43ca0", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0130/metrics.json b/tests/fixtures/intrabar_margin/pm2-ltf-0130/metrics.json new file mode 100644 index 00000000..e2a81a7d --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0130/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-ltf-0130", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2026-01-28", + "to": "2026-02-01", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "fb12d944a1d3c04eb7e6b9c71f05352b9569df428f25aad4cf8d92bb4efad068", + "tvTradesCsvHash": "75e6678ceb46c5970cc2d196f97a2eb4a185c82051af711455436d3cb2d43ca0", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75312", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1769558400000, + "to": 1769904000000 + }, + "returnedRange": { + "from": 1769558400000, + "to": 1769904000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "e55e9712db8b8d26cb20ebe15448d549ad03f516afa882430cf4fb5696762e0d", + "decodedReportSha256": "79de90b6fb6a88ccfe790ffe2c49f6bf4894be4292cd7ba8f1380666dd49b0b7", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:51:01.015Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0130/strategy.pine b/tests/fixtures/intrabar_margin/pm2-ltf-0130/strategy.pine new file mode 100644 index 00000000..e031a5c6 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0130/strategy.pine @@ -0,0 +1,30 @@ +// R5 lane PAR-MARGIN-2: TradingView's own intrabars inside the 15m bar 2026-01-30 01:30 UTC, +// read through request.security_lower_tf at "2" (the bar magnifier's intrabar timeframe for a +// 15-minute chart in TradingView's table) and at "1", printed as order comments (Signal column). +//@version=6 +strategy("pm2-ltf-0130", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=0.001, pyramiding=10) +t2 = request.security_lower_tf(syminfo.tickerid, "2", time) +o2 = request.security_lower_tf(syminfo.tickerid, "2", open) +h2 = request.security_lower_tf(syminfo.tickerid, "2", high) +l2 = request.security_lower_tf(syminfo.tickerid, "2", low) +c2 = request.security_lower_tf(syminfo.tickerid, "2", close) +l1 = request.security_lower_tf(syminfo.tickerid, "1", low) +h1 = request.security_lower_tf(syminfo.tickerid, "1", high) +mins(a) => + out = array.new_float() + for v in a + array.push(out, (v % 3600000) / 60000) + out +if time == timestamp("UTC", 2026, 1, 30, 1, 30) + strategy.entry("T", strategy.long, comment = "T" + str.tostring(mins(t2))) + strategy.entry("O", strategy.long, comment = "O" + str.tostring(o2)) + strategy.entry("H", strategy.long, comment = "H" + str.tostring(h2)) + strategy.entry("L", strategy.long, comment = "L" + str.tostring(l2)) + strategy.entry("C", strategy.long, comment = "C" + str.tostring(c2)) + strategy.entry("A", strategy.long, comment = "l1a" + str.tostring(array.slice(l1, 0, math.min(8, array.size(l1))))) + strategy.entry("B", strategy.long, comment = "l1b" + str.tostring(array.slice(l1, math.min(8, array.size(l1)), array.size(l1)))) + strategy.entry("D", strategy.long, comment = "h1a" + str.tostring(array.slice(h1, 0, math.min(8, array.size(h1))))) + strategy.entry("E", strategy.long, comment = "h1b" + str.tostring(array.slice(h1, math.min(8, array.size(h1)), array.size(h1)))) +if time == timestamp("UTC", 2026, 1, 30, 1, 45) + strategy.close_all(comment = "n2=" + str.tostring(array.size(l2)) + " n1=" + str.tostring(array.size(l1))) diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0130/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-ltf-0130/tv_trades.csv new file mode 100644 index 00000000..86cf2664 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0130/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2026-01-30 10:00,n2=8 n1=15,2745.65,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.02195,0.00,1 +1,Entry long,2026-01-30 09:45,"T[30, 32, 34, 36, 38, 40, 42]",2723.7,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.02195,0.00,1 +2,Exit long,2026-01-30 10:00,n2=8 n1=15,2745.65,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.0439,0.00,1 +2,Entry long,2026-01-30 09:45,"O[2799.17, 2804.4, 2805, 2798.84, 2783.58, 2774.33, 2742.75]",2723.7,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.0439,0.00,1 +3,Exit long,2026-01-30 10:00,n2=8 n1=15,2745.65,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.06585,0.01,1 +3,Entry long,2026-01-30 09:45,"H[2804.77, 2805, 2807.46, 2801.68, 2790.61, 2779.63, 2751.87]",2723.7,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.06585,0.01,1 +4,Exit long,2026-01-30 10:00,n2=8 n1=15,2745.65,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.0878,0.01,1 +4,Entry long,2026-01-30 09:45,"L[2793.76, 2801.12, 2796.95, 2782.69, 2760, 2727.51, 2673.33]",2723.7,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.0878,0.01,1 +5,Exit long,2026-01-30 10:00,n2=8 n1=15,2745.65,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.10975,0.01,1 +5,Entry long,2026-01-30 09:45,"C[2804.4, 2805, 2798.84, 2783.58, 2774.34, 2742.74, 2715.36]",2723.7,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.10975,0.01,1 +6,Exit long,2026-01-30 10:00,n2=8 n1=15,2745.65,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.1317,0.01,1 +6,Entry long,2026-01-30 09:45,"l1a[2793.76, 2796.67, 2801.12, 2802.47, 2802.72, 2796.95, 2796.78, 2782.69]",2723.7,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.1317,0.01,1 +7,Exit long,2026-01-30 10:00,n2=8 n1=15,2745.65,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.15365,0.02,1 +7,Entry long,2026-01-30 09:45,"l1b[2782.63, 2760, 2763.35, 2727.51, 2690, 2673.33, 2707.28]",2723.7,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.15365,0.02,1 +8,Exit long,2026-01-30 10:00,n2=8 n1=15,2745.65,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.1756,0.02,1 +8,Entry long,2026-01-30 09:45,"h1a[2799.18, 2804.77, 2804.41, 2805, 2807.46, 2803.4, 2801.68, 2797.31]",2723.7,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.1756,0.02,1 +9,Exit long,2026-01-30 10:00,n2=8 n1=15,2745.65,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.19755,0.02,1 +9,Entry long,2026-01-30 09:45,"h1b[2789.76, 2790.61, 2779.63, 2773.13, 2751.87, 2716.01, 2732.44]",2723.7,0.001,2.7237,0.02195,0.81,0,0.0273,1.00,-0.0087,-0.32,0.19755,0.02,1 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0402/meta.json b/tests/fixtures/intrabar_margin/pm2-ltf-0402/meta.json new file mode 100644 index 00000000..ac4b6b10 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0402/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-ltf-0402", + "slug": "pm2-ltf-0402", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:51:13.582Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-31", + "to": "2025-04-04" + }, + "pine_sha256": "fb973cb6e32055d5b4cdfa926d71ce4cca534178d1a92b2b82957536964d9e63", + "tv_trades_sha256": "24f3cae40c441e68ab2370ec3f2ddbef6e34808ab73aeccdeaa99d1289e71150", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0402/metrics.json b/tests/fixtures/intrabar_margin/pm2-ltf-0402/metrics.json new file mode 100644 index 00000000..48dd256d --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0402/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-ltf-0402", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-03-31", + "to": "2025-04-04", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "fb973cb6e32055d5b4cdfa926d71ce4cca534178d1a92b2b82957536964d9e63", + "tvTradesCsvHash": "24f3cae40c441e68ab2370ec3f2ddbef6e34808ab73aeccdeaa99d1289e71150", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75321", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743379200000, + "to": 1743724800000 + }, + "returnedRange": { + "from": 1743379200000, + "to": 1743724800000 + }, + "rangeProof": "covered", + "rawFrameSha256": "f5a8a76323ad5c6d06afe53f8902d1207becceace39e59d9615dbf5833fd624b", + "decodedReportSha256": "5107a11a5f75f64157d3692f63d7fc33d7be4142cb7ecf5488cdf2e1736d42ac", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:51:13.581Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0402/strategy.pine b/tests/fixtures/intrabar_margin/pm2-ltf-0402/strategy.pine new file mode 100644 index 00000000..cc069ce6 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0402/strategy.pine @@ -0,0 +1,30 @@ +// R5 lane PAR-MARGIN-2: TradingView's own intrabars inside the 15m bar 2025-04-02 20:15 UTC, +// read through request.security_lower_tf at "2" (the bar magnifier's intrabar timeframe for a +// 15-minute chart in TradingView's table) and at "1", printed as order comments (Signal column). +//@version=6 +strategy("pm2-ltf-0402", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=0.001, pyramiding=10) +t2 = request.security_lower_tf(syminfo.tickerid, "2", time) +o2 = request.security_lower_tf(syminfo.tickerid, "2", open) +h2 = request.security_lower_tf(syminfo.tickerid, "2", high) +l2 = request.security_lower_tf(syminfo.tickerid, "2", low) +c2 = request.security_lower_tf(syminfo.tickerid, "2", close) +l1 = request.security_lower_tf(syminfo.tickerid, "1", low) +h1 = request.security_lower_tf(syminfo.tickerid, "1", high) +mins(a) => + out = array.new_float() + for v in a + array.push(out, (v % 3600000) / 60000) + out +if time == timestamp("UTC", 2025, 4, 2, 20, 15) + strategy.entry("T", strategy.long, comment = "T" + str.tostring(mins(t2))) + strategy.entry("O", strategy.long, comment = "O" + str.tostring(o2)) + strategy.entry("H", strategy.long, comment = "H" + str.tostring(h2)) + strategy.entry("L", strategy.long, comment = "L" + str.tostring(l2)) + strategy.entry("C", strategy.long, comment = "C" + str.tostring(c2)) + strategy.entry("A", strategy.long, comment = "l1a" + str.tostring(array.slice(l1, 0, math.min(8, array.size(l1))))) + strategy.entry("B", strategy.long, comment = "l1b" + str.tostring(array.slice(l1, math.min(8, array.size(l1)), array.size(l1)))) + strategy.entry("D", strategy.long, comment = "h1a" + str.tostring(array.slice(h1, 0, math.min(8, array.size(h1))))) + strategy.entry("E", strategy.long, comment = "h1b" + str.tostring(array.slice(h1, math.min(8, array.size(h1)), array.size(h1)))) +if time == timestamp("UTC", 2025, 4, 2, 20, 30) + strategy.close_all(comment = "n2=" + str.tostring(array.size(l2)) + " n1=" + str.tostring(array.size(l1))) diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0402/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-ltf-0402/tv_trades.csv new file mode 100644 index 00000000..ba908f97 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0402/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-03 04:45,n2=7 n1=15,1870,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.02782,0.00,1 +1,Entry long,2025-04-03 04:30,"T[14, 16, 18, 20, 22, 24, 26, 28]",1897.82,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.02782,0.00,1 +2,Exit long,2025-04-03 04:45,n2=7 n1=15,1870,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.05564,-0.01,1 +2,Entry long,2025-04-03 04:30,"O[1908.17, 1919.98, 1938.25, 1941.27, 1929.33, 1922.6, 1922.35, 1891.85]",1897.82,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.05564,-0.01,1 +3,Exit long,2025-04-03 04:45,n2=7 n1=15,1870,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.08346,-0.01,1 +3,Entry long,2025-04-03 04:30,"H[1946.03, 1957.97, 1943.67, 1941.49, 1930.97, 1923.68, 1928.28, 1900.53]",1897.82,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.08346,-0.01,1 +4,Exit long,2025-04-03 04:45,n2=7 n1=15,1870,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.11128,-0.01,1 +4,Entry long,2025-04-03 04:30,"L[1908.17, 1919.12, 1931, 1923.36, 1919.97, 1917.68, 1888.67, 1872.48]",1897.82,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.11128,-0.01,1 +5,Exit long,2025-04-03 04:45,n2=7 n1=15,1870,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.1391,-0.01,1 +5,Entry long,2025-04-03 04:30,"C[1919.95, 1938.27, 1941.28, 1929.34, 1922.6, 1922.3, 1891.84, 1897.82]",1897.82,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.1391,-0.01,1 +6,Exit long,2025-04-03 04:45,n2=7 n1=15,1870,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.16692,-0.02,1 +6,Entry long,2025-04-03 04:30,"l1a[1916.85, 1919.12, 1930.35, 1933.53, 1931, 1930, 1923.36, 1919.97]",1897.82,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.16692,-0.02,1 +7,Exit long,2025-04-03 04:45,n2=7 n1=15,1870,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.19474,-0.02,1 +7,Entry long,2025-04-03 04:30,"l1b[1922.01, 1917.68, 1919.37, 1906, 1888.67, 1872.48, 1884.51]",1897.82,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.19474,-0.02,1 +8,Exit long,2025-04-03 04:45,n2=7 n1=15,1870,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.22256,-0.02,1 +8,Entry long,2025-04-03 04:30,"h1a[1936.36, 1957.97, 1952.68, 1943.67, 1943.49, 1941.49, 1932.11, 1930.97]",1897.82,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.22256,-0.02,1 +9,Exit long,2025-04-03 04:45,n2=7 n1=15,1870,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.25038,-0.03,1 +9,Entry long,2025-04-03 04:30,"h1b[1925.8, 1922.69, 1923.68, 1928.28, 1910, 1892.7, 1900.53]",1897.82,0.001,1.89782,-0.02782,-1.47,0,0.00673,0.35,-0.03282,-1.73,-0.25038,-0.03,1 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0623/meta.json b/tests/fixtures/intrabar_margin/pm2-ltf-0623/meta.json new file mode 100644 index 00000000..f7a104c4 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0623/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-ltf-0623", + "slug": "pm2-ltf-0623", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:51:19.307Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-21", + "to": "2025-06-25" + }, + "pine_sha256": "dd23d506516aacc9e5e8bdee7c8e23ad3ddb0e4c03597e961cb47c99031e6653", + "tv_trades_sha256": "3223f6f0c7a3462dc87c5592f8e70d2ea41f82a0d7f049510d3c149ac1f61081", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0623/metrics.json b/tests/fixtures/intrabar_margin/pm2-ltf-0623/metrics.json new file mode 100644 index 00000000..2ac0a6cc --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0623/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-ltf-0623", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-06-21", + "to": "2025-06-25", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "dd23d506516aacc9e5e8bdee7c8e23ad3ddb0e4c03597e961cb47c99031e6653", + "tvTradesCsvHash": "3223f6f0c7a3462dc87c5592f8e70d2ea41f82a0d7f049510d3c149ac1f61081", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75325", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1750464000000, + "to": 1750809600000 + }, + "returnedRange": { + "from": 1750464000000, + "to": 1750809600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "77c168f19130351a84f2095ddf92a0f055d5e221c6ed6f44a942326186cdb23d", + "decodedReportSha256": "60a1ab96164174f81ead4013c62acee9acc52d2c9ec64a6e9fcdd2fe0484f403", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:51:19.306Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0623/strategy.pine b/tests/fixtures/intrabar_margin/pm2-ltf-0623/strategy.pine new file mode 100644 index 00000000..dab4bc66 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0623/strategy.pine @@ -0,0 +1,30 @@ +// R5 lane PAR-MARGIN-2: TradingView's own intrabars inside the 15m bar 2025-06-23 22:00 UTC, +// read through request.security_lower_tf at "2" (the bar magnifier's intrabar timeframe for a +// 15-minute chart in TradingView's table) and at "1", printed as order comments (Signal column). +//@version=6 +strategy("pm2-ltf-0623", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=0.001, pyramiding=10) +t2 = request.security_lower_tf(syminfo.tickerid, "2", time) +o2 = request.security_lower_tf(syminfo.tickerid, "2", open) +h2 = request.security_lower_tf(syminfo.tickerid, "2", high) +l2 = request.security_lower_tf(syminfo.tickerid, "2", low) +c2 = request.security_lower_tf(syminfo.tickerid, "2", close) +l1 = request.security_lower_tf(syminfo.tickerid, "1", low) +h1 = request.security_lower_tf(syminfo.tickerid, "1", high) +mins(a) => + out = array.new_float() + for v in a + array.push(out, (v % 3600000) / 60000) + out +if time == timestamp("UTC", 2025, 6, 23, 22, 0) + strategy.entry("T", strategy.long, comment = "T" + str.tostring(mins(t2))) + strategy.entry("O", strategy.long, comment = "O" + str.tostring(o2)) + strategy.entry("H", strategy.long, comment = "H" + str.tostring(h2)) + strategy.entry("L", strategy.long, comment = "L" + str.tostring(l2)) + strategy.entry("C", strategy.long, comment = "C" + str.tostring(c2)) + strategy.entry("A", strategy.long, comment = "l1a" + str.tostring(array.slice(l1, 0, math.min(8, array.size(l1))))) + strategy.entry("B", strategy.long, comment = "l1b" + str.tostring(array.slice(l1, math.min(8, array.size(l1)), array.size(l1)))) + strategy.entry("D", strategy.long, comment = "h1a" + str.tostring(array.slice(h1, 0, math.min(8, array.size(h1))))) + strategy.entry("E", strategy.long, comment = "h1b" + str.tostring(array.slice(h1, math.min(8, array.size(h1)), array.size(h1)))) +if time == timestamp("UTC", 2025, 6, 23, 22, 15) + strategy.close_all(comment = "n2=" + str.tostring(array.size(l2)) + " n1=" + str.tostring(array.size(l1))) diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0623/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-ltf-0623/tv_trades.csv new file mode 100644 index 00000000..70abfd86 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0623/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-24 06:30,n2=8 n1=15,2425.27,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.02247,0.00,1 +1,Entry long,2025-06-24 06:15,"T[0, 2, 4, 6, 8, 10, 12]",2402.8,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.02247,0.00,1 +2,Exit long,2025-06-24 06:30,n2=8 n1=15,2425.27,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.04494,0.00,1 +2,Entry long,2025-06-24 06:15,"O[2339.88, 2341.05, 2362.71, 2372.21, 2374.47, 2388.93, 2405.61]",2402.8,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.04494,0.00,1 +3,Exit long,2025-06-24 06:30,n2=8 n1=15,2425.27,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.06741,0.01,1 +3,Entry long,2025-06-24 06:15,"H[2342.71, 2369.42, 2378, 2378.42, 2393.24, 2409.52, 2408.28]",2402.8,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.06741,0.01,1 +4,Exit long,2025-06-24 06:30,n2=8 n1=15,2425.27,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.08988,0.01,1 +4,Entry long,2025-06-24 06:15,"L[2339.51, 2339.82, 2359.28, 2370.3, 2374.47, 2386.71, 2391.07]",2402.8,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.08988,0.01,1 +5,Exit long,2025-06-24 06:30,n2=8 n1=15,2425.27,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.11235,0.01,1 +5,Entry long,2025-06-24 06:15,"C[2341.05, 2362.71, 2372.2, 2374.47, 2388.93, 2405.6, 2396.34]",2402.8,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.11235,0.01,1 +6,Exit long,2025-06-24 06:30,n2=8 n1=15,2425.27,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.13482,0.01,1 +6,Entry long,2025-06-24 06:15,"l1a[2339.63, 2339.51, 2339.82, 2352.51, 2359.28, 2366, 2370.3, 2372.04]",2402.8,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.13482,0.01,1 +7,Exit long,2025-06-24 06:30,n2=8 n1=15,2425.27,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.15729,0.02,1 +7,Entry long,2025-06-24 06:15,"l1b[2374.47, 2380.12, 2386.71, 2387.29, 2395.96, 2391.07, 2396.1]",2402.8,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.15729,0.02,1 +8,Exit long,2025-06-24 06:30,n2=8 n1=15,2425.27,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.17976,0.02,1 +8,Entry long,2025-06-24 06:15,"h1a[2341.68, 2342.71, 2360, 2369.42, 2372.63, 2378, 2378.42, 2375.97]",2402.8,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.17976,0.02,1 +9,Exit long,2025-06-24 06:30,n2=8 n1=15,2425.27,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.20223,0.02,1 +9,Entry long,2025-06-24 06:15,"h1b[2386.38, 2393.24, 2396.5, 2409.52, 2408.28, 2398.77, 2404.25]",2402.8,0.001,2.4028,0.02247,0.94,0,0.0372,1.55,-0.01305,-0.54,0.20223,0.02,1 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0707/meta.json b/tests/fixtures/intrabar_margin/pm2-ltf-0707/meta.json new file mode 100644 index 00000000..5dd86f4c --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0707/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-ltf-0707", + "slug": "pm2-ltf-0707", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:51:24.557Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-05", + "to": "2025-07-09" + }, + "pine_sha256": "bcd5ac609ab0e163afe5713ed470a89e80be71bab014457a217abc49bb1c38c5", + "tv_trades_sha256": "53d673f2e8c5317bd6d662cf8c0e2e51e5092b0f810ca456af5343995075a597", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0707/metrics.json b/tests/fixtures/intrabar_margin/pm2-ltf-0707/metrics.json new file mode 100644 index 00000000..7e1b5468 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0707/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-ltf-0707", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-07-05", + "to": "2025-07-09", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "bcd5ac609ab0e163afe5713ed470a89e80be71bab014457a217abc49bb1c38c5", + "tvTradesCsvHash": "53d673f2e8c5317bd6d662cf8c0e2e51e5092b0f810ca456af5343995075a597", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75329", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1751673600000, + "to": 1752019200000 + }, + "returnedRange": { + "from": 1751673600000, + "to": 1752019200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "3f2a77d0222ac1be5069a1d0dcd56a700d30f4b0c774cd298c4a794e154cd6ba", + "decodedReportSha256": "8d5acd3c7c49dff4d0f6a0351d17585b287ee070c7bb03488fab8a6a67753939", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:51:24.556Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0707/strategy.pine b/tests/fixtures/intrabar_margin/pm2-ltf-0707/strategy.pine new file mode 100644 index 00000000..e1934fa9 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0707/strategy.pine @@ -0,0 +1,30 @@ +// R5 lane PAR-MARGIN-2: TradingView's own intrabars inside the 15m bar 2025-07-07 16:15 UTC, +// read through request.security_lower_tf at "2" (the bar magnifier's intrabar timeframe for a +// 15-minute chart in TradingView's table) and at "1", printed as order comments (Signal column). +//@version=6 +strategy("pm2-ltf-0707", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=0.001, pyramiding=10) +t2 = request.security_lower_tf(syminfo.tickerid, "2", time) +o2 = request.security_lower_tf(syminfo.tickerid, "2", open) +h2 = request.security_lower_tf(syminfo.tickerid, "2", high) +l2 = request.security_lower_tf(syminfo.tickerid, "2", low) +c2 = request.security_lower_tf(syminfo.tickerid, "2", close) +l1 = request.security_lower_tf(syminfo.tickerid, "1", low) +h1 = request.security_lower_tf(syminfo.tickerid, "1", high) +mins(a) => + out = array.new_float() + for v in a + array.push(out, (v % 3600000) / 60000) + out +if time == timestamp("UTC", 2025, 7, 7, 16, 15) + strategy.entry("T", strategy.long, comment = "T" + str.tostring(mins(t2))) + strategy.entry("O", strategy.long, comment = "O" + str.tostring(o2)) + strategy.entry("H", strategy.long, comment = "H" + str.tostring(h2)) + strategy.entry("L", strategy.long, comment = "L" + str.tostring(l2)) + strategy.entry("C", strategy.long, comment = "C" + str.tostring(c2)) + strategy.entry("A", strategy.long, comment = "l1a" + str.tostring(array.slice(l1, 0, math.min(8, array.size(l1))))) + strategy.entry("B", strategy.long, comment = "l1b" + str.tostring(array.slice(l1, math.min(8, array.size(l1)), array.size(l1)))) + strategy.entry("D", strategy.long, comment = "h1a" + str.tostring(array.slice(h1, 0, math.min(8, array.size(h1))))) + strategy.entry("E", strategy.long, comment = "h1b" + str.tostring(array.slice(h1, math.min(8, array.size(h1)), array.size(h1)))) +if time == timestamp("UTC", 2025, 7, 7, 16, 30) + strategy.close_all(comment = "n2=" + str.tostring(array.size(l2)) + " n1=" + str.tostring(array.size(l1))) diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-0707/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-ltf-0707/tv_trades.csv new file mode 100644 index 00000000..a2c770f4 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-0707/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-08 00:45,n2=7 n1=15,2534.5,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.00427,0.00,1 +1,Entry long,2025-07-08 00:30,"T[14, 16, 18, 20, 22, 24, 26, 28]",2530.23,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.00427,0.00,1 +2,Exit long,2025-07-08 00:45,n2=7 n1=15,2534.5,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.00854,0.00,1 +2,Entry long,2025-07-08 00:30,"O[2536.65, 2535.38, 2539.24, 2532.32, 2519.59, 2526.29, 2527.52, 2535.29]",2530.23,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.00854,0.00,1 +3,Exit long,2025-07-08 00:45,n2=7 n1=15,2534.5,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.01281,0.00,1 +3,Entry long,2025-07-08 00:30,"H[2537.58, 2540.57, 2541.29, 2534.79, 2531.27, 2530.12, 2536.35, 2535.29]",2530.23,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.01281,0.00,1 +4,Exit long,2025-07-08 00:45,n2=7 n1=15,2534.5,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.01708,0.00,1 +4,Entry long,2025-07-08 00:30,"L[2533.91, 2535.37, 2532.32, 2505.37, 2517.1, 2523, 2527.14, 2529.31]",2530.23,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.01708,0.00,1 +5,Exit long,2025-07-08 00:45,n2=7 n1=15,2534.5,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.02135,0.00,1 +5,Entry long,2025-07-08 00:30,"C[2535.38, 2539.25, 2532.33, 2519.59, 2526.29, 2527.52, 2535.3, 2530.24]",2530.23,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.02135,0.00,1 +6,Exit long,2025-07-08 00:45,n2=7 n1=15,2534.5,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.02562,0.00,1 +6,Entry long,2025-07-08 00:30,"l1a[2533.91, 2535.37, 2538.27, 2537.35, 2532.32, 2522.02, 2505.37, 2517.1]",2530.23,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.02562,0.00,1 +7,Exit long,2025-07-08 00:45,n2=7 n1=15,2534.5,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.02989,0.00,1 +7,Entry long,2025-07-08 00:30,"l1b[2521.3, 2523, 2526.19, 2527.14, 2532.32, 2529.31, 2530]",2530.23,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.02989,0.00,1 +8,Exit long,2025-07-08 00:45,n2=7 n1=15,2534.5,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.03416,0.00,1 +8,Entry long,2025-07-08 00:30,"h1a[2535.97, 2538.82, 2540.57, 2540.72, 2541.29, 2534.79, 2524.92, 2522.51]",2530.23,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.03416,0.00,1 +9,Exit long,2025-07-08 00:45,n2=7 n1=15,2534.5,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.03843,0.00,1 +9,Entry long,2025-07-08 00:30,"h1b[2531.27, 2530.12, 2530.01, 2533, 2536.35, 2535.29, 2532.1]",2530.23,0.001,2.53023,0.00427,0.17,0,0.00551,0.22,-0.00478,-0.19,0.03843,0.00,1 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-1010/meta.json b/tests/fixtures/intrabar_margin/pm2-ltf-1010/meta.json new file mode 100644 index 00000000..531b1adb --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-1010/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-ltf-1010", + "slug": "pm2-ltf-1010", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:51:32.045Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-10-08", + "to": "2025-10-12" + }, + "pine_sha256": "8572a6f1b97a41f02e8b135b4d2d9dfdd225a26dc41a0e60095f452ac46aa259", + "tv_trades_sha256": "01b82dc91bf8bbed3ff40469dc1d2c4927a1ea83d744a8e997896d36392dc659", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-1010/metrics.json b/tests/fixtures/intrabar_margin/pm2-ltf-1010/metrics.json new file mode 100644 index 00000000..83dd43ea --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-1010/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-ltf-1010", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-10-08", + "to": "2025-10-12", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "8572a6f1b97a41f02e8b135b4d2d9dfdd225a26dc41a0e60095f452ac46aa259", + "tvTradesCsvHash": "01b82dc91bf8bbed3ff40469dc1d2c4927a1ea83d744a8e997896d36392dc659", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75334", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1759881600000, + "to": 1760227200000 + }, + "returnedRange": { + "from": 1759881600000, + "to": 1760227200000 + }, + "rangeProof": "covered", + "rawFrameSha256": "3d6146b46dc9a80f6173a76b32b411e9ec5f10e23042b764df3135872445d738", + "decodedReportSha256": "4c63691a4b7d630fbe9e3986940b95ac7c276808570db928209e8e9b11e98f34", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:51:32.045Z" +} diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-1010/strategy.pine b/tests/fixtures/intrabar_margin/pm2-ltf-1010/strategy.pine new file mode 100644 index 00000000..de211bff --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-1010/strategy.pine @@ -0,0 +1,30 @@ +// R5 lane PAR-MARGIN-2: TradingView's own intrabars inside the 15m bar 2025-10-10 19:15 UTC, +// read through request.security_lower_tf at "2" (the bar magnifier's intrabar timeframe for a +// 15-minute chart in TradingView's table) and at "1", printed as order comments (Signal column). +//@version=6 +strategy("pm2-ltf-1010", overlay=true, initial_capital=1000, + default_qty_type=strategy.fixed, default_qty_value=0.001, pyramiding=10) +t2 = request.security_lower_tf(syminfo.tickerid, "2", time) +o2 = request.security_lower_tf(syminfo.tickerid, "2", open) +h2 = request.security_lower_tf(syminfo.tickerid, "2", high) +l2 = request.security_lower_tf(syminfo.tickerid, "2", low) +c2 = request.security_lower_tf(syminfo.tickerid, "2", close) +l1 = request.security_lower_tf(syminfo.tickerid, "1", low) +h1 = request.security_lower_tf(syminfo.tickerid, "1", high) +mins(a) => + out = array.new_float() + for v in a + array.push(out, (v % 3600000) / 60000) + out +if time == timestamp("UTC", 2025, 10, 10, 19, 15) + strategy.entry("T", strategy.long, comment = "T" + str.tostring(mins(t2))) + strategy.entry("O", strategy.long, comment = "O" + str.tostring(o2)) + strategy.entry("H", strategy.long, comment = "H" + str.tostring(h2)) + strategy.entry("L", strategy.long, comment = "L" + str.tostring(l2)) + strategy.entry("C", strategy.long, comment = "C" + str.tostring(c2)) + strategy.entry("A", strategy.long, comment = "l1a" + str.tostring(array.slice(l1, 0, math.min(8, array.size(l1))))) + strategy.entry("B", strategy.long, comment = "l1b" + str.tostring(array.slice(l1, math.min(8, array.size(l1)), array.size(l1)))) + strategy.entry("D", strategy.long, comment = "h1a" + str.tostring(array.slice(h1, 0, math.min(8, array.size(h1))))) + strategy.entry("E", strategy.long, comment = "h1b" + str.tostring(array.slice(h1, math.min(8, array.size(h1)), array.size(h1)))) +if time == timestamp("UTC", 2025, 10, 10, 19, 30) + strategy.close_all(comment = "n2=" + str.tostring(array.size(l2)) + " n1=" + str.tostring(array.size(l1))) diff --git a/tests/fixtures/intrabar_margin/pm2-ltf-1010/tv_trades.csv b/tests/fixtures/intrabar_margin/pm2-ltf-1010/tv_trades.csv new file mode 100644 index 00000000..5a0b2488 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2-ltf-1010/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-10-11 03:45,n2=7 n1=15,4024.41,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.0733,0.01,1 +1,Entry long,2025-10-11 03:30,"T[14, 16, 18, 20, 22, 24, 26, 28]",3951.11,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.0733,0.01,1 +2,Exit long,2025-10-11 03:45,n2=7 n1=15,4024.41,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.1466,0.01,1 +2,Entry long,2025-10-11 03:30,"O[4034.39, 4025.7, 4028.88, 4027.37, 4003.16, 4000.98, 4020.29, 4008.64]",3951.11,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.1466,0.01,1 +3,Exit long,2025-10-11 03:45,n2=7 n1=15,4024.41,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.2199,0.02,1 +3,Entry long,2025-10-11 03:30,"H[4037.37, 4030.46, 4030, 4027.91, 4009.4, 4027.41, 4028.31, 4009.39]",3951.11,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.2199,0.02,1 +4,Exit long,2025-10-11 03:45,n2=7 n1=15,4024.41,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.2932,0.03,1 +4,Entry long,2025-10-11 03:30,"L[4015.27, 4021, 4023.37, 3991.74, 3991.8, 4000.98, 4007.85, 3950]",3951.11,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.2932,0.03,1 +5,Exit long,2025-10-11 03:45,n2=7 n1=15,4024.41,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.3665,0.04,1 +5,Entry long,2025-10-11 03:30,"C[4025.64, 4028.88, 4027.37, 4003.23, 4000.98, 4020.28, 4008.64, 3951.2]",3951.11,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.3665,0.04,1 +6,Exit long,2025-10-11 03:45,n2=7 n1=15,4024.41,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.4398,0.04,1 +6,Entry long,2025-10-11 03:30,"l1a[4015.27, 4021, 4023.44, 4023.37, 4023.5, 4008.09, 3991.74, 3991.8]",3951.11,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.4398,0.04,1 +7,Exit long,2025-10-11 03:45,n2=7 n1=15,4024.41,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.5131,0.05,1 +7,Entry long,2025-10-11 03:30,"l1b[3995, 4000.98, 4009.16, 4015.39, 4007.85, 3973.88, 3950]",3951.11,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.5131,0.05,1 +8,Exit long,2025-10-11 03:45,n2=7 n1=15,4024.41,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.5864,0.06,1 +8,Entry long,2025-10-11 03:30,"h1a[4029.13, 4029.92, 4030.46, 4030, 4028.3, 4027.91, 4015.71, 4009.4]",3951.11,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.5864,0.06,1 +9,Exit long,2025-10-11 03:45,n2=7 n1=15,4024.41,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.6597,0.07,1 +9,Entry long,2025-10-11 03:30,"h1b[4008.15, 4009.17, 4027.41, 4028.31, 4019.52, 4009.39, 3988.35]",3951.11,0.001,3.95111,0.0733,1.86,0,0.0755,1.91,-0.00334,-0.08,0.6597,0.07,1 \ No newline at end of file diff --git a/tests/fixtures/intrabar_margin/pm2_bars_1m.inc b/tests/fixtures/intrabar_margin/pm2_bars_1m.inc new file mode 100644 index 00000000..17dcd165 --- /dev/null +++ b/tests/fixtures/intrabar_margin/pm2_bars_1m.inc @@ -0,0 +1,739 @@ +// R5 lane PAR-MARGIN-2: ETH 1-minute bars per item-3 event of the new shapes (process_orders_on_close, +// the full-margin short), from corpus/data/ohlcv_ETH-USDT-USDT_1m.csv: the signal bar .. +5, 90 bars. +// {timestamp_ms, open, high, low, close} +constexpr FeedBar kPm2Pooc_0109_0115[90] = { // signal bar 2026-01-09 01:15 UTC + {1767921300000LL, 3113.49, 3114.8, 3113.48, 3114.79}, + {1767921360000LL, 3114.8, 3114.8, 3113.87, 3114.12}, + {1767921420000LL, 3114.12, 3114.81, 3113.87, 3114.81}, + {1767921480000LL, 3114.81, 3116.36, 3114.81, 3115.1}, + {1767921540000LL, 3115.09, 3115.09, 3111.91, 3111.91}, + {1767921600000LL, 3111.91, 3112.43, 3111.5, 3112.43}, + {1767921660000LL, 3112.43, 3112.43, 3111.76, 3111.76}, + {1767921720000LL, 3111.76, 3111.77, 3107.32, 3108.15}, + {1767921780000LL, 3108.16, 3108.51, 3106.6, 3106.72}, + {1767921840000LL, 3106.73, 3106.73, 3104, 3105.49}, + {1767921900000LL, 3105.49, 3105.5, 3100.52, 3101.62}, + {1767921960000LL, 3101.63, 3103.22, 3101.63, 3101.74}, + {1767922020000LL, 3101.73, 3101.74, 3097.92, 3099.4}, + {1767922080000LL, 3099.4, 3125.47, 3098.9, 3123.44}, + {1767922140000LL, 3123.44, 3144, 3121.8, 3138.19}, + {1767922200000LL, 3138.19, 3148, 3125.02, 3125.27}, + {1767922260000LL, 3125.27, 3126.19, 3122.81, 3124.18}, + {1767922320000LL, 3124.18, 3127, 3120, 3126.17}, + {1767922380000LL, 3126.17, 3127, 3124.9, 3126.26}, + {1767922440000LL, 3126.26, 3126.27, 3106.26, 3107.77}, + {1767922500000LL, 3107.78, 3108.63, 3096.44, 3103.25}, + {1767922560000LL, 3103.25, 3104.22, 3088.63, 3092.15}, + {1767922620000LL, 3092.15, 3095.39, 3077.46, 3084.86}, + {1767922680000LL, 3084.85, 3094.83, 3084.85, 3092.93}, + {1767922740000LL, 3092.93, 3096.47, 3092.08, 3094.6}, + {1767922800000LL, 3094.59, 3096.3, 3094, 3094.27}, + {1767922860000LL, 3094.27, 3094.31, 3091.89, 3093.87}, + {1767922920000LL, 3093.86, 3097.63, 3093.86, 3095.8}, + {1767922980000LL, 3095.79, 3096.64, 3094.13, 3096.5}, + {1767923040000LL, 3096.51, 3097.91, 3095.43, 3097.81}, + {1767923100000LL, 3097.81, 3101.21, 3097.14, 3099.15}, + {1767923160000LL, 3099.16, 3100.62, 3099.15, 3099.95}, + {1767923220000LL, 3099.95, 3100.11, 3099.49, 3100.1}, + {1767923280000LL, 3100.1, 3103.11, 3100, 3103.04}, + {1767923340000LL, 3103.04, 3104.7, 3102.81, 3104.15}, + {1767923400000LL, 3104.14, 3104.15, 3101, 3101.83}, + {1767923460000LL, 3101.82, 3102.53, 3100.4, 3100.5}, + {1767923520000LL, 3100.5, 3101.55, 3097.45, 3098.48}, + {1767923580000LL, 3098.49, 3102.1, 3098.49, 3102.1}, + {1767923640000LL, 3102.09, 3104, 3102.09, 3103.47}, + {1767923700000LL, 3103.48, 3104, 3102.42, 3102.92}, + {1767923760000LL, 3102.93, 3103.91, 3102.92, 3103.68}, + {1767923820000LL, 3103.67, 3104.44, 3103.21, 3104.19}, + {1767923880000LL, 3104.19, 3104.78, 3103.86, 3104.6}, + {1767923940000LL, 3104.6, 3104.88, 3104.31, 3104.31}, + {1767924000000LL, 3104.32, 3105.68, 3103.15, 3103.15}, + {1767924060000LL, 3103.15, 3103.36, 3102.44, 3102.9}, + {1767924120000LL, 3102.9, 3104, 3102.09, 3104}, + {1767924180000LL, 3104, 3107.76, 3103.99, 3107.7}, + {1767924240000LL, 3107.7, 3111.8, 3107.69, 3109.96}, + {1767924300000LL, 3109.96, 3112.92, 3109.96, 3112.38}, + {1767924360000LL, 3112.38, 3122.87, 3112.37, 3121.29}, + {1767924420000LL, 3121.29, 3125.05, 3118.86, 3120.93}, + {1767924480000LL, 3120.93, 3121.95, 3119.57, 3121.66}, + {1767924540000LL, 3121.66, 3123.24, 3119.8, 3120.74}, + {1767924600000LL, 3120.73, 3124.6, 3120.4, 3123.44}, + {1767924660000LL, 3123.44, 3123.91, 3121.44, 3122.03}, + {1767924720000LL, 3122.03, 3122.3, 3121.06, 3121.78}, + {1767924780000LL, 3121.77, 3123.44, 3121.77, 3122.75}, + {1767924840000LL, 3122.74, 3123, 3121.8, 3122.09}, + {1767924900000LL, 3122.1, 3122.1, 3120, 3121.01}, + {1767924960000LL, 3121.01, 3123.25, 3120.9, 3121.83}, + {1767925020000LL, 3121.83, 3122.85, 3119, 3120.55}, + {1767925080000LL, 3120.55, 3120.87, 3119.36, 3120.35}, + {1767925140000LL, 3120.36, 3121, 3119.08, 3120.99}, + {1767925200000LL, 3121, 3124.9, 3120.87, 3124.44}, + {1767925260000LL, 3124.43, 3124.43, 3121.13, 3121.13}, + {1767925320000LL, 3121.13, 3122.76, 3120.56, 3122.76}, + {1767925380000LL, 3122.76, 3123.88, 3122.25, 3122.25}, + {1767925440000LL, 3122.25, 3123.35, 3121.01, 3121.16}, + {1767925500000LL, 3121.16, 3121.17, 3119.45, 3120.05}, + {1767925560000LL, 3120.05, 3121.31, 3118.99, 3119.78}, + {1767925620000LL, 3119.78, 3119.79, 3116.41, 3116.59}, + {1767925680000LL, 3116.59, 3117.44, 3115.29, 3117.27}, + {1767925740000LL, 3117.28, 3117.28, 3116.62, 3116.62}, + {1767925800000LL, 3116.62, 3118.48, 3116.62, 3118.48}, + {1767925860000LL, 3118.48, 3119.84, 3118, 3118.3}, + {1767925920000LL, 3118.3, 3118.31, 3115, 3115}, + {1767925980000LL, 3115, 3115.79, 3113.62, 3115.57}, + {1767926040000LL, 3115.56, 3115.76, 3114.48, 3114.48}, + {1767926100000LL, 3114.48, 3115.26, 3114.22, 3115.26}, + {1767926160000LL, 3115.25, 3117.01, 3115, 3117}, + {1767926220000LL, 3117.01, 3117.72, 3115.59, 3116.46}, + {1767926280000LL, 3116.46, 3116.54, 3114.57, 3114.58}, + {1767926340000LL, 3114.57, 3114.57, 3113.68, 3113.68}, + {1767926400000LL, 3113.67, 3113.67, 3110.9, 3111.01}, + {1767926460000LL, 3111.02, 3113.37, 3111.01, 3113.36}, + {1767926520000LL, 3113.36, 3113.37, 3111.84, 3111.84}, + {1767926580000LL, 3111.84, 3113.6, 3111.83, 3113.59}, + {1767926640000LL, 3113.59, 3114.8, 3113.24, 3114.79}, +}; +constexpr FeedBar kPm2Pooc_0402_2000[90] = { // signal bar 2025-04-02 20:00 UTC + {1743624000000LL, 1910.59, 1910.76, 1908.71, 1908.93}, + {1743624060000LL, 1908.93, 1908.93, 1906.8, 1906.8}, + {1743624120000LL, 1906.8, 1907, 1903.43, 1905.51}, + {1743624180000LL, 1905.53, 1908.14, 1905.53, 1906.4}, + {1743624240000LL, 1906.39, 1906.39, 1903.99, 1905.01}, + {1743624300000LL, 1905.01, 1905.8, 1904.5, 1905.4}, + {1743624360000LL, 1905.4, 1908.42, 1905.4, 1907.82}, + {1743624420000LL, 1907.81, 1908.16, 1904.01, 1904.66}, + {1743624480000LL, 1904.65, 1911.26, 1902.38, 1903.09}, + {1743624540000LL, 1903.08, 1904.22, 1898, 1898.11}, + {1743624600000LL, 1898.1, 1898.43, 1890.85, 1892.08}, + {1743624660000LL, 1892.08, 1893.4, 1888.7, 1888.8}, + {1743624720000LL, 1888.79, 1893.92, 1882.98, 1884.03}, + {1743624780000LL, 1884.03, 1909.83, 1881, 1908.19}, + {1743624840000LL, 1908.17, 1946.03, 1908.17, 1933.11}, + {1743624900000LL, 1933.11, 1936.36, 1916.85, 1919.95}, + {1743624960000LL, 1919.98, 1957.97, 1919.12, 1949.49}, + {1743625020000LL, 1949.49, 1952.68, 1930.35, 1938.27}, + {1743625080000LL, 1938.25, 1943.67, 1933.53, 1935.36}, + {1743625140000LL, 1935.35, 1943.49, 1931, 1941.28}, + {1743625200000LL, 1941.27, 1941.49, 1930, 1930.94}, + {1743625260000LL, 1930.94, 1932.11, 1923.36, 1929.34}, + {1743625320000LL, 1929.33, 1930.97, 1919.97, 1923.83}, + {1743625380000LL, 1923.83, 1925.8, 1922.01, 1922.6}, + {1743625440000LL, 1922.6, 1922.69, 1917.68, 1919.81}, + {1743625500000LL, 1919.8, 1923.68, 1919.37, 1922.3}, + {1743625560000LL, 1922.35, 1928.28, 1906, 1906}, + {1743625620000LL, 1906, 1910, 1888.67, 1891.84}, + {1743625680000LL, 1891.85, 1892.7, 1872.48, 1891.92}, + {1743625740000LL, 1891.96, 1900.53, 1884.51, 1897.82}, + {1743625800000LL, 1897.82, 1904.55, 1891.72, 1892.59}, + {1743625860000LL, 1892.6, 1898, 1888.06, 1898}, + {1743625920000LL, 1898, 1901.61, 1892.3, 1897.1}, + {1743625980000LL, 1897.15, 1897.82, 1890, 1890.47}, + {1743626040000LL, 1890.47, 1895, 1887.58, 1892.11}, + {1743626100000LL, 1892.11, 1895.26, 1889.81, 1890.97}, + {1743626160000LL, 1890.97, 1891.31, 1879.47, 1880.37}, + {1743626220000LL, 1880.38, 1883.73, 1874.5, 1879.52}, + {1743626280000LL, 1879.53, 1883.09, 1876.46, 1881.98}, + {1743626340000LL, 1882.61, 1885.45, 1875.86, 1877.35}, + {1743626400000LL, 1877.35, 1879.3, 1875.36, 1876.64}, + {1743626460000LL, 1876.63, 1876.72, 1868.34, 1873.98}, + {1743626520000LL, 1873.98, 1874.98, 1869.9, 1874.98}, + {1743626580000LL, 1874.98, 1874.98, 1868, 1868.21}, + {1743626640000LL, 1868.21, 1872.62, 1865, 1870.06}, + {1743626700000LL, 1870, 1874.75, 1866.79, 1871.01}, + {1743626760000LL, 1871.01, 1873.53, 1867.05, 1873.53}, + {1743626820000LL, 1873.53, 1879.31, 1873.08, 1876.73}, + {1743626880000LL, 1876.73, 1877.25, 1874.39, 1876.13}, + {1743626940000LL, 1876.13, 1881.11, 1876.13, 1880.91}, + {1743627000000LL, 1880.9, 1883, 1879.12, 1883}, + {1743627060000LL, 1883, 1883.75, 1877.35, 1877.99}, + {1743627120000LL, 1877.98, 1879.3, 1875.88, 1877.5}, + {1743627180000LL, 1877.51, 1881.2, 1877.24, 1879.59}, + {1743627240000LL, 1879.59, 1881.53, 1877.67, 1879.37}, + {1743627300000LL, 1879.37, 1882.35, 1878.86, 1881.69}, + {1743627360000LL, 1881.69, 1883, 1879.49, 1880.38}, + {1743627420000LL, 1880.38, 1883.25, 1879.35, 1880.49}, + {1743627480000LL, 1880.49, 1881, 1878.35, 1878.73}, + {1743627540000LL, 1878.73, 1881.89, 1877.38, 1880}, + {1743627600000LL, 1880.16, 1881.95, 1879.67, 1881.56}, + {1743627660000LL, 1881.56, 1883.64, 1881.27, 1883.13}, + {1743627720000LL, 1883.13, 1883.91, 1881.64, 1881.78}, + {1743627780000LL, 1881.77, 1881.77, 1875.95, 1875.95}, + {1743627840000LL, 1875.95, 1877.71, 1875.66, 1876.77}, + {1743627900000LL, 1876.77, 1876.78, 1870, 1871.37}, + {1743627960000LL, 1871.36, 1871.49, 1865, 1865}, + {1743628020000LL, 1865.01, 1865.01, 1861.32, 1864.2}, + {1743628080000LL, 1864.2, 1867.08, 1861.66, 1866.63}, + {1743628140000LL, 1866.62, 1868.69, 1856, 1856.92}, + {1743628200000LL, 1856.92, 1859.76, 1856.08, 1857.39}, + {1743628260000LL, 1857.39, 1858.95, 1853.93, 1857.94}, + {1743628320000LL, 1857.93, 1861.44, 1856.47, 1859.87}, + {1743628380000LL, 1859.86, 1864.33, 1856.79, 1863.85}, + {1743628440000LL, 1863.85, 1863.85, 1857.78, 1859.77}, + {1743628500000LL, 1859.76, 1862.7, 1857.66, 1862.27}, + {1743628560000LL, 1862.28, 1867.05, 1860.54, 1863.62}, + {1743628620000LL, 1863.62, 1868.27, 1863.53, 1864.18}, + {1743628680000LL, 1864.18, 1864.63, 1856.75, 1858}, + {1743628740000LL, 1858, 1861.72, 1857.39, 1861.07}, + {1743628800000LL, 1861.07, 1878.45, 1859.32, 1863.65}, + {1743628860000LL, 1863.66, 1864.71, 1859.13, 1860.8}, + {1743628920000LL, 1860.81, 1864, 1860.48, 1862.48}, + {1743628980000LL, 1862.48, 1868.79, 1862.48, 1865.3}, + {1743629040000LL, 1865.3, 1867.5, 1863.78, 1866.67}, + {1743629100000LL, 1866.68, 1866.68, 1863.53, 1864.83}, + {1743629160000LL, 1864.83, 1864.83, 1862.61, 1862.92}, + {1743629220000LL, 1862.92, 1866.61, 1862.78, 1866.18}, + {1743629280000LL, 1866.18, 1866.33, 1864.64, 1865.41}, + {1743629340000LL, 1865.42, 1865.52, 1862.07, 1862.71}, +}; +constexpr FeedBar kPm2Pooc_0707_1600[90] = { // signal bar 2025-07-07 16:00 UTC + {1751904000000LL, 2532.56, 2533.54, 2532, 2533.07}, + {1751904060000LL, 2533.07, 2534.17, 2532.57, 2534}, + {1751904120000LL, 2534, 2535.61, 2533.68, 2533.68}, + {1751904180000LL, 2533.68, 2534.81, 2532.58, 2533.28}, + {1751904240000LL, 2533.27, 2536.6, 2533.27, 2536.56}, + {1751904300000LL, 2536.57, 2537.35, 2535.65, 2536.68}, + {1751904360000LL, 2536.68, 2539.35, 2534.35, 2534.35}, + {1751904420000LL, 2534.36, 2534.36, 2529.83, 2530.02}, + {1751904480000LL, 2530.02, 2531.68, 2529.25, 2531.3}, + {1751904540000LL, 2531.3, 2537, 2531.3, 2536.99}, + {1751904600000LL, 2536.99, 2537.5, 2536.37, 2536.69}, + {1751904660000LL, 2536.7, 2536.99, 2533.22, 2533.46}, + {1751904720000LL, 2533.47, 2536.48, 2533.23, 2536.41}, + {1751904780000LL, 2536.42, 2537.41, 2536.42, 2536.65}, + {1751904840000LL, 2536.65, 2537.58, 2534.54, 2534.55}, + {1751904900000LL, 2534.55, 2535.97, 2533.91, 2535.38}, + {1751904960000LL, 2535.38, 2538.82, 2535.37, 2538.26}, + {1751905020000LL, 2538.27, 2540.57, 2538.27, 2539.25}, + {1751905080000LL, 2539.24, 2540.72, 2537.35, 2540.26}, + {1751905140000LL, 2540.26, 2541.29, 2532.32, 2532.33}, + {1751905200000LL, 2532.32, 2534.79, 2522.02, 2524.91}, + {1751905260000LL, 2524.92, 2524.92, 2505.37, 2519.59}, + {1751905320000LL, 2519.59, 2522.51, 2517.1, 2521.62}, + {1751905380000LL, 2521.62, 2531.27, 2521.3, 2526.29}, + {1751905440000LL, 2526.29, 2530.12, 2523, 2527.5}, + {1751905500000LL, 2527.5, 2530.01, 2526.19, 2527.52}, + {1751905560000LL, 2527.52, 2533, 2527.14, 2532.32}, + {1751905620000LL, 2532.32, 2536.35, 2532.32, 2535.3}, + {1751905680000LL, 2535.29, 2535.29, 2529.31, 2530.41}, + {1751905740000LL, 2530.41, 2532.1, 2530, 2530.24}, + {1751905800000LL, 2530.23, 2534.5, 2529.77, 2533.75}, + {1751905860000LL, 2533.74, 2535.74, 2532.03, 2533.26}, + {1751905920000LL, 2533.25, 2534.23, 2531.56, 2533.43}, + {1751905980000LL, 2533.43, 2534.15, 2531.82, 2534}, + {1751906040000LL, 2533.99, 2534.69, 2532.65, 2534.12}, + {1751906100000LL, 2534.13, 2535.26, 2531.71, 2531.71}, + {1751906160000LL, 2531.7, 2533.82, 2529.67, 2533.5}, + {1751906220000LL, 2533.51, 2535.24, 2532.1, 2534.06}, + {1751906280000LL, 2534.06, 2534.54, 2526.66, 2526.66}, + {1751906340000LL, 2526.66, 2531.33, 2525.45, 2531.32}, + {1751906400000LL, 2531.32, 2533, 2531.19, 2531.34}, + {1751906460000LL, 2531.33, 2531.82, 2529.73, 2530.02}, + {1751906520000LL, 2530.02, 2532.66, 2529.23, 2531.89}, + {1751906580000LL, 2531.89, 2533, 2530, 2531.74}, + {1751906640000LL, 2531.74, 2534.49, 2531.35, 2534.49}, + {1751906700000LL, 2534.5, 2535.17, 2531.47, 2531.47}, + {1751906760000LL, 2531.47, 2531.59, 2527.23, 2527.24}, + {1751906820000LL, 2527.23, 2528.44, 2525.9, 2527.42}, + {1751906880000LL, 2527.42, 2527.49, 2525.39, 2526.2}, + {1751906940000LL, 2526.2, 2526.27, 2523.7, 2525.24}, + {1751907000000LL, 2525.24, 2525.76, 2522.36, 2524.79}, + {1751907060000LL, 2524.79, 2526.83, 2524.04, 2525.85}, + {1751907120000LL, 2525.84, 2530.9, 2525.59, 2530.49}, + {1751907180000LL, 2530.49, 2531, 2528.87, 2529.14}, + {1751907240000LL, 2529.15, 2529.15, 2525.92, 2525.96}, + {1751907300000LL, 2525.97, 2526.84, 2523.81, 2524.35}, + {1751907360000LL, 2524.25, 2524.94, 2522.95, 2524.02}, + {1751907420000LL, 2524.02, 2524.02, 2521.12, 2522.36}, + {1751907480000LL, 2522.35, 2525, 2521.78, 2524.47}, + {1751907540000LL, 2524.48, 2524.48, 2522.1, 2523.24}, + {1751907600000LL, 2523.23, 2523.65, 2520.65, 2522.42}, + {1751907660000LL, 2522.43, 2524.91, 2518.1, 2519.2}, + {1751907720000LL, 2519.21, 2522.66, 2518.92, 2519.28}, + {1751907780000LL, 2519.28, 2519.94, 2516.68, 2518.38}, + {1751907840000LL, 2518.38, 2518.56, 2516.67, 2517.79}, + {1751907900000LL, 2517.78, 2517.78, 2515.35, 2517.17}, + {1751907960000LL, 2517.17, 2521.35, 2516.55, 2520.7}, + {1751908020000LL, 2520.7, 2524.5, 2520.05, 2524.12}, + {1751908080000LL, 2524.13, 2525.72, 2521.68, 2525.3}, + {1751908140000LL, 2525.3, 2526.25, 2524, 2524}, + {1751908200000LL, 2524, 2528.15, 2523.35, 2526.93}, + {1751908260000LL, 2526.93, 2528, 2526.55, 2527.5}, + {1751908320000LL, 2527.49, 2530.49, 2527, 2529.58}, + {1751908380000LL, 2529.58, 2530.61, 2528.84, 2530.18}, + {1751908440000LL, 2530.18, 2530.79, 2529.84, 2530.37}, + {1751908500000LL, 2530.36, 2530.48, 2529.22, 2529.99}, + {1751908560000LL, 2529.98, 2530.86, 2529.44, 2529.65}, + {1751908620000LL, 2529.65, 2529.84, 2528, 2528.82}, + {1751908680000LL, 2528.81, 2529.3, 2527.89, 2527.89}, + {1751908740000LL, 2527.88, 2528.6, 2527.4, 2528.4}, + {1751908800000LL, 2528.39, 2530, 2528.39, 2529.69}, + {1751908860000LL, 2529.68, 2530, 2528, 2528.01}, + {1751908920000LL, 2528, 2528.92, 2526.41, 2527.16}, + {1751908980000LL, 2527.15, 2527.84, 2526.8, 2527.25}, + {1751909040000LL, 2527.26, 2527.52, 2525, 2525.77}, + {1751909100000LL, 2525.78, 2526.85, 2524.83, 2526.37}, + {1751909160000LL, 2526.37, 2527, 2525.66, 2525.66}, + {1751909220000LL, 2525.67, 2528.7, 2525.67, 2527.82}, + {1751909280000LL, 2527.82, 2530.86, 2527.11, 2530.73}, + {1751909340000LL, 2530.72, 2536, 2530.67, 2534.93}, +}; +constexpr FeedBar kPm2Pooc_1010_1900[90] = { // signal bar 2025-10-10 19:00 UTC + {1760122800000LL, 4051.85, 4062.57, 4051, 4059.06}, + {1760122860000LL, 4059.05, 4059.38, 4051.47, 4058.35}, + {1760122920000LL, 4058.36, 4061, 4056.6, 4060.99}, + {1760122980000LL, 4060.99, 4061.61, 4056.79, 4058.16}, + {1760123040000LL, 4058.17, 4058.46, 4054.91, 4057.46}, + {1760123100000LL, 4057.45, 4057.69, 4039.2, 4047.7}, + {1760123160000LL, 4047.71, 4052.86, 4038.78, 4052.22}, + {1760123220000LL, 4052.29, 4054.18, 4046.16, 4048.19}, + {1760123280000LL, 4048.2, 4048.35, 4033, 4033.17}, + {1760123340000LL, 4033.17, 4040.9, 4030.76, 4031.39}, + {1760123400000LL, 4031.39, 4033.97, 4020.3, 4033.73}, + {1760123460000LL, 4033.73, 4039.41, 4022.8, 4026}, + {1760123520000LL, 4026.01, 4035.22, 4020.5, 4035.01}, + {1760123580000LL, 4035.01, 4038.76, 4033.97, 4034.39}, + {1760123640000LL, 4034.39, 4037.37, 4025.19, 4029.13}, + {1760123700000LL, 4029.13, 4029.13, 4015.27, 4025.64}, + {1760123760000LL, 4025.7, 4029.92, 4021, 4024.85}, + {1760123820000LL, 4024.86, 4030.46, 4023.44, 4028.88}, + {1760123880000LL, 4028.88, 4030, 4023.37, 4026.4}, + {1760123940000LL, 4026.42, 4028.3, 4023.5, 4027.37}, + {1760124000000LL, 4027.37, 4027.91, 4008.09, 4011.8}, + {1760124060000LL, 4011.8, 4015.71, 3991.74, 4003.23}, + {1760124120000LL, 4003.16, 4009.4, 3991.8, 4008.2}, + {1760124180000LL, 4008.15, 4008.15, 3995, 4000.98}, + {1760124240000LL, 4000.98, 4009.17, 4000.98, 4009.16}, + {1760124300000LL, 4009.17, 4027.41, 4009.16, 4020.28}, + {1760124360000LL, 4020.29, 4028.31, 4015.39, 4017.32}, + {1760124420000LL, 4017.32, 4019.52, 4007.85, 4008.64}, + {1760124480000LL, 4008.64, 4009.39, 3973.88, 3984.08}, + {1760124540000LL, 3984.08, 3988.35, 3950, 3951.2}, + {1760124600000LL, 3951.11, 3989.92, 3947.77, 3989.18}, + {1760124660000LL, 3989.17, 3992, 3976.8, 3976.8}, + {1760124720000LL, 3976.8, 3981.93, 3969, 3975.44}, + {1760124780000LL, 3975.43, 3975.43, 3950, 3963.21}, + {1760124840000LL, 3963.19, 3972.98, 3959.18, 3971.94}, + {1760124900000LL, 3971.93, 3983.79, 3964.02, 3981.75}, + {1760124960000LL, 3981.74, 3985, 3975.53, 3983.61}, + {1760125020000LL, 3983.61, 3989.53, 3980.29, 3985.44}, + {1760125080000LL, 3985.44, 3999.12, 3985.43, 3989.31}, + {1760125140000LL, 3989.31, 3991.68, 3982.36, 3987.3}, + {1760125200000LL, 3987.3, 3994.65, 3985.37, 3987.44}, + {1760125260000LL, 3987.42, 4008.52, 3985.19, 4008.29}, + {1760125320000LL, 4008.29, 4013.27, 4003, 4010.34}, + {1760125380000LL, 4010.35, 4017.3, 4008.05, 4013.3}, + {1760125440000LL, 4013.3, 4026.61, 4012.28, 4024.23}, + {1760125500000LL, 4024.41, 4033, 4023.03, 4025.64}, + {1760125560000LL, 4025.64, 4026.95, 4014.43, 4017.45}, + {1760125620000LL, 4017.46, 4029.91, 4017.46, 4028.77}, + {1760125680000LL, 4028.75, 4035, 4026.35, 4034.52}, + {1760125740000LL, 4034.52, 4036.16, 4028.68, 4034.97}, + {1760125800000LL, 4034.98, 4035.61, 4017, 4019.46}, + {1760125860000LL, 4019.46, 4022.51, 4015.81, 4016.38}, + {1760125920000LL, 4016.37, 4023.65, 4015.44, 4015.48}, + {1760125980000LL, 4015.48, 4020.65, 4007.79, 4010.24}, + {1760126040000LL, 4010.23, 4012.39, 4003.75, 4005.53}, + {1760126100000LL, 4005.53, 4009, 4001.11, 4008.63}, + {1760126160000LL, 4008.64, 4008.65, 4002.84, 4004.49}, + {1760126220000LL, 4004.44, 4007.8, 4002.52, 4007.69}, + {1760126280000LL, 4007.69, 4007.69, 3996, 4000.8}, + {1760126340000LL, 4000.81, 4000.81, 3990.45, 3994.99}, + {1760126400000LL, 3994.99, 4001.28, 3993.32, 3999.7}, + {1760126460000LL, 3999.69, 4009.83, 3996, 4004.87}, + {1760126520000LL, 4004.87, 4006.46, 3998.84, 4004.36}, + {1760126580000LL, 4004.36, 4008.76, 4000.5, 4008.76}, + {1760126640000LL, 4008.76, 4017.84, 4007.68, 4016.32}, + {1760126700000LL, 4016.33, 4016.33, 4005.66, 4011.19}, + {1760126760000LL, 4011.19, 4017, 4010.21, 4013.76}, + {1760126820000LL, 4013.76, 4015.01, 4006.8, 4010.47}, + {1760126880000LL, 4010.46, 4010.47, 4000, 4000.03}, + {1760126940000LL, 4000.04, 4000.17, 3996.48, 3997.53}, + {1760127000000LL, 3997.53, 4002.1, 3995.44, 4002.09}, + {1760127060000LL, 4002.09, 4004.19, 3999.02, 4003.95}, + {1760127120000LL, 4003.96, 4010, 4002.45, 4010}, + {1760127180000LL, 4009.99, 4010.56, 4000.44, 4004.9}, + {1760127240000LL, 4004.9, 4004.9, 4000.63, 4000.64}, + {1760127300000LL, 4000.64, 4005.37, 3996.89, 4005.24}, + {1760127360000LL, 4005.24, 4009.83, 4004.37, 4009.11}, + {1760127420000LL, 4009.11, 4010, 3998.99, 4000.6}, + {1760127480000LL, 4000.6, 4002, 3994.23, 3998.19}, + {1760127540000LL, 3998.17, 3999.84, 3997.31, 3999.66}, + {1760127600000LL, 3999.66, 4003.66, 3995.93, 4003.65}, + {1760127660000LL, 4003.65, 4007.42, 4003, 4003.43}, + {1760127720000LL, 4003.44, 4008.3, 4003.44, 4007.83}, + {1760127780000LL, 4007.82, 4015.59, 4007.78, 4015.17}, + {1760127840000LL, 4015.18, 4017.06, 4011.09, 4012.79}, + {1760127900000LL, 4012.79, 4013.54, 4009.42, 4013.54}, + {1760127960000LL, 4013.53, 4018.39, 4011.63, 4018.38}, + {1760128020000LL, 4018.39, 4020.48, 4016, 4018.53}, + {1760128080000LL, 4018.52, 4021.6, 4015.64, 4018}, + {1760128140000LL, 4018, 4022, 4016.86, 4021.25}, +}; +constexpr FeedBar kPm2S1x_0201_1100[90] = { // signal bar 2026-02-01 11:00 UTC + {1769943600000LL, 2379.6, 2383.69, 2379.6, 2383.28}, + {1769943660000LL, 2383.28, 2385.71, 2382.09, 2385.41}, + {1769943720000LL, 2385.4, 2386.35, 2383, 2386.34}, + {1769943780000LL, 2386.34, 2388.72, 2385.07, 2385.18}, + {1769943840000LL, 2385.18, 2386.76, 2383.67, 2386.29}, + {1769943900000LL, 2386.28, 2387.69, 2381.92, 2385.14}, + {1769943960000LL, 2385.13, 2388.28, 2384.57, 2387.94}, + {1769944020000LL, 2387.95, 2389.8, 2387.41, 2388.79}, + {1769944080000LL, 2388.8, 2390.22, 2387.95, 2390.21}, + {1769944140000LL, 2390.22, 2391.11, 2389.22, 2391.11}, + {1769944200000LL, 2391.11, 2391.56, 2388.8, 2390.05}, + {1769944260000LL, 2390.05, 2390.44, 2388.66, 2389.66}, + {1769944320000LL, 2389.66, 2390.58, 2389.15, 2389.15}, + {1769944380000LL, 2389.16, 2390.79, 2388.41, 2390.64}, + {1769944440000LL, 2390.65, 2392.59, 2390.65, 2391.98}, + {1769944500000LL, 2391.97, 2391.98, 2388.13, 2388.56}, + {1769944560000LL, 2388.56, 2388.84, 2383.3, 2384.11}, + {1769944620000LL, 2384.12, 2384.97, 2382.21, 2384.7}, + {1769944680000LL, 2384.7, 2387.95, 2383.51, 2385.45}, + {1769944740000LL, 2385.46, 2388.88, 2385.45, 2387.43}, + {1769944800000LL, 2387.42, 2388.14, 2386.4, 2388.09}, + {1769944860000LL, 2388.09, 2390, 2387, 2388.93}, + {1769944920000LL, 2388.92, 2389.92, 2388.74, 2389.21}, + {1769944980000LL, 2389.21, 2390, 2387.24, 2388.42}, + {1769945040000LL, 2388.41, 2388.41, 2385.26, 2385.71}, + {1769945100000LL, 2385.7, 2387.13, 2384, 2387}, + {1769945160000LL, 2387.01, 2387.53, 2386.19, 2386.72}, + {1769945220000LL, 2386.72, 2388.83, 2386.44, 2387.29}, + {1769945280000LL, 2387.29, 2388.05, 2386.02, 2386.24}, + {1769945340000LL, 2386.24, 2386.59, 2385.2, 2386.13}, + {1769945400000LL, 2386.14, 2388.14, 2386.14, 2387.76}, + {1769945460000LL, 2387.76, 2390.26, 2387.76, 2389.73}, + {1769945520000LL, 2389.73, 2389.96, 2387.29, 2388.13}, + {1769945580000LL, 2388.13, 2389.99, 2388.12, 2389.98}, + {1769945640000LL, 2389.97, 2391.55, 2389.97, 2391.55}, + {1769945700000LL, 2391.55, 2402.65, 2391.54, 2400.49}, + {1769945760000LL, 2400.49, 2405.75, 2399.99, 2403.12}, + {1769945820000LL, 2403.13, 2405, 2400.13, 2403.16}, + {1769945880000LL, 2403.17, 2403.98, 2402.64, 2403.34}, + {1769945940000LL, 2403.34, 2405.14, 2401.05, 2404.61}, + {1769946000000LL, 2404.61, 2410.99, 2403.07, 2409.44}, + {1769946060000LL, 2409.44, 2411.95, 2407.39, 2407.99}, + {1769946120000LL, 2407.99, 2413.94, 2407.99, 2413.26}, + {1769946180000LL, 2413.27, 2423.66, 2410.98, 2420.39}, + {1769946240000LL, 2420.38, 2422.08, 2417.96, 2419}, + {1769946300000LL, 2419, 2421.86, 2414.79, 2414.79}, + {1769946360000LL, 2414.77, 2417.62, 2413.4, 2417.38}, + {1769946420000LL, 2417.39, 2418.45, 2416.37, 2417.54}, + {1769946480000LL, 2417.54, 2418.75, 2414.76, 2418.74}, + {1769946540000LL, 2418.74, 2418.74, 2409.06, 2411.07}, + {1769946600000LL, 2411.07, 2412.99, 2410.52, 2412.12}, + {1769946660000LL, 2412.11, 2413.23, 2412.01, 2412.37}, + {1769946720000LL, 2412.37, 2413.11, 2408.84, 2410.12}, + {1769946780000LL, 2410.11, 2411.48, 2409.61, 2410.36}, + {1769946840000LL, 2410.36, 2410.67, 2409.38, 2409.38}, + {1769946900000LL, 2409.38, 2410.79, 2408.46, 2409.22}, + {1769946960000LL, 2409.23, 2410.11, 2408.13, 2409.44}, + {1769947020000LL, 2409.43, 2410.72, 2408.75, 2409.73}, + {1769947080000LL, 2409.72, 2411.18, 2408.8, 2410.34}, + {1769947140000LL, 2410.34, 2410.95, 2409.58, 2410.65}, + {1769947200000LL, 2410.65, 2411.31, 2406.24, 2408.44}, + {1769947260000LL, 2408.44, 2408.74, 2405.92, 2406.8}, + {1769947320000LL, 2406.79, 2407.77, 2406.12, 2407.77}, + {1769947380000LL, 2407.78, 2408.37, 2406.75, 2406.97}, + {1769947440000LL, 2406.97, 2407.28, 2405.69, 2406.52}, + {1769947500000LL, 2406.52, 2409.6, 2406.52, 2407.22}, + {1769947560000LL, 2407.22, 2407.22, 2405.69, 2407}, + {1769947620000LL, 2407, 2407.68, 2403.96, 2407.53}, + {1769947680000LL, 2407.52, 2408, 2406.42, 2407.12}, + {1769947740000LL, 2407.12, 2408.03, 2405.96, 2406.77}, + {1769947800000LL, 2406.77, 2408.54, 2405.22, 2407.59}, + {1769947860000LL, 2407.57, 2408.69, 2407.19, 2407.77}, + {1769947920000LL, 2407.77, 2408.7, 2407.42, 2407.43}, + {1769947980000LL, 2407.42, 2408.37, 2406.76, 2408.11}, + {1769948040000LL, 2408.11, 2408.11, 2405.5, 2405.5}, + {1769948100000LL, 2405.51, 2405.51, 2402.01, 2403.25}, + {1769948160000LL, 2403.25, 2404.08, 2395.97, 2397.52}, + {1769948220000LL, 2397.53, 2399.15, 2396.62, 2397.18}, + {1769948280000LL, 2397.19, 2399.13, 2397.05, 2397.9}, + {1769948340000LL, 2397.9, 2402.72, 2397.9, 2402.17}, + {1769948400000LL, 2402.16, 2402.74, 2400.49, 2401.25}, + {1769948460000LL, 2401.25, 2401.26, 2398.09, 2398.15}, + {1769948520000LL, 2398.16, 2399.11, 2392.33, 2393.33}, + {1769948580000LL, 2393.33, 2394.21, 2388.58, 2389.37}, + {1769948640000LL, 2389.37, 2390.08, 2382.7, 2387.45}, + {1769948700000LL, 2387.44, 2389.49, 2387.4, 2389.31}, + {1769948760000LL, 2389.31, 2391.47, 2387.54, 2390.86}, + {1769948820000LL, 2390.85, 2392.27, 2390.31, 2392.03}, + {1769948880000LL, 2392.04, 2393, 2391.67, 2391.68}, + {1769948940000LL, 2391.67, 2393.46, 2391.67, 2393.45}, +}; +constexpr FeedBar kPm2S1x_0407_1145[90] = { // signal bar 2025-04-07 11:45 UTC + {1744026300000LL, 1479.29, 1479.87, 1475.74, 1477.3}, + {1744026360000LL, 1477.29, 1481, 1477.14, 1480.98}, + {1744026420000LL, 1481, 1481.49, 1479.91, 1480.24}, + {1744026480000LL, 1480.24, 1480.31, 1477.79, 1478.76}, + {1744026540000LL, 1478.77, 1478.9, 1474.66, 1476.13}, + {1744026600000LL, 1476.14, 1479.58, 1476.14, 1478.18}, + {1744026660000LL, 1478.17, 1479.72, 1477.78, 1478.57}, + {1744026720000LL, 1478.57, 1480.08, 1477.36, 1479.65}, + {1744026780000LL, 1479.66, 1482.06, 1479.44, 1480.91}, + {1744026840000LL, 1480.92, 1481.38, 1480.25, 1480.91}, + {1744026900000LL, 1480.91, 1483.6, 1480.02, 1482.9}, + {1744026960000LL, 1482.9, 1484.06, 1481.22, 1481.5}, + {1744027020000LL, 1481.49, 1483, 1481.46, 1482.25}, + {1744027080000LL, 1482.25, 1482.53, 1480.1, 1480.89}, + {1744027140000LL, 1480.89, 1482.34, 1480.41, 1482.34}, + {1744027200000LL, 1482.34, 1488, 1481.8, 1487.33}, + {1744027260000LL, 1487.32, 1488, 1481, 1482.12}, + {1744027320000LL, 1482.1, 1484.32, 1480.32, 1481.1}, + {1744027380000LL, 1481.1, 1481.83, 1479.11, 1480.6}, + {1744027440000LL, 1480.6, 1485, 1479.62, 1484.78}, + {1744027500000LL, 1484.78, 1484.78, 1479.89, 1480.68}, + {1744027560000LL, 1480.69, 1482.14, 1479.09, 1481.56}, + {1744027620000LL, 1481.57, 1485.2, 1481.29, 1482.59}, + {1744027680000LL, 1482.53, 1483.02, 1479.83, 1479.84}, + {1744027740000LL, 1479.83, 1483.04, 1479.36, 1480.03}, + {1744027800000LL, 1480.01, 1481.6, 1478.66, 1481.6}, + {1744027860000LL, 1481.6, 1482.18, 1480.09, 1480.26}, + {1744027920000LL, 1480.26, 1480.52, 1477.17, 1477.88}, + {1744027980000LL, 1477.89, 1483.56, 1477.89, 1481.7}, + {1744028040000LL, 1481.65, 1485, 1481.6, 1484.81}, + {1744028100000LL, 1484.81, 1485, 1482.6, 1483.21}, + {1744028160000LL, 1483.2, 1487.57, 1482.72, 1486.95}, + {1744028220000LL, 1486.96, 1486.99, 1483.75, 1484.22}, + {1744028280000LL, 1484.22, 1484.59, 1482, 1483.14}, + {1744028340000LL, 1483.14, 1484.9, 1481.21, 1481.44}, + {1744028400000LL, 1481.43, 1485, 1480.77, 1484.54}, + {1744028460000LL, 1484.53, 1487.46, 1484.53, 1486.57}, + {1744028520000LL, 1486.58, 1486.92, 1484.68, 1485.67}, + {1744028580000LL, 1485.67, 1487, 1483.26, 1483.58}, + {1744028640000LL, 1483.58, 1489.96, 1483.58, 1489.36}, + {1744028700000LL, 1489.37, 1498.93, 1489.32, 1498.59}, + {1744028760000LL, 1498.58, 1516, 1498.58, 1504.43}, + {1744028820000LL, 1504.4, 1505.41, 1500.38, 1504.53}, + {1744028880000LL, 1504.53, 1512.79, 1504.52, 1511.1}, + {1744028940000LL, 1511.13, 1519.12, 1510.74, 1516.79}, + {1744029000000LL, 1516.78, 1522.66, 1512.63, 1519.62}, + {1744029060000LL, 1519.61, 1523.99, 1512.22, 1513.98}, + {1744029120000LL, 1513.98, 1518.01, 1512.86, 1514.47}, + {1744029180000LL, 1514.47, 1518.25, 1513.68, 1517.07}, + {1744029240000LL, 1517.06, 1518.25, 1511.4, 1511.62}, + {1744029300000LL, 1511.61, 1515.79, 1509.92, 1515.1}, + {1744029360000LL, 1515.1, 1517.11, 1512.93, 1513.62}, + {1744029420000LL, 1513.61, 1516.27, 1512.3, 1513.4}, + {1744029480000LL, 1513.41, 1513.41, 1505.44, 1508.51}, + {1744029540000LL, 1508.51, 1509.7, 1507.2, 1509.65}, + {1744029600000LL, 1509.65, 1509.65, 1504, 1504.52}, + {1744029660000LL, 1504.52, 1507.09, 1504.21, 1506.17}, + {1744029720000LL, 1506.17, 1506.95, 1503.27, 1504.44}, + {1744029780000LL, 1504.44, 1508.4, 1504.44, 1506.94}, + {1744029840000LL, 1506.94, 1508.06, 1505.38, 1505.59}, + {1744029900000LL, 1505.58, 1505.86, 1498.1, 1498.93}, + {1744029960000LL, 1498.95, 1501.71, 1498.76, 1500}, + {1744030020000LL, 1500, 1500.54, 1497, 1500.53}, + {1744030080000LL, 1500.53, 1505.93, 1500.2, 1505.76}, + {1744030140000LL, 1505.76, 1517.59, 1505.76, 1510.88}, + {1744030200000LL, 1510.87, 1519.35, 1510.51, 1516.8}, + {1744030260000LL, 1516.79, 1520.87, 1512.73, 1512.73}, + {1744030320000LL, 1512.73, 1517.65, 1512.73, 1515.94}, + {1744030380000LL, 1515.94, 1520.37, 1514.3, 1518.93}, + {1744030440000LL, 1518.94, 1519.44, 1516.02, 1518.44}, + {1744030500000LL, 1518.42, 1524.3, 1518.34, 1523.3}, + {1744030560000LL, 1523.3, 1526.48, 1520.13, 1520.97}, + {1744030620000LL, 1520.97, 1524.05, 1519.85, 1520.28}, + {1744030680000LL, 1520.29, 1523.41, 1517.17, 1518.06}, + {1744030740000LL, 1518.06, 1520.39, 1517, 1520.01}, + {1744030800000LL, 1520.01, 1520.01, 1514.51, 1515.83}, + {1744030860000LL, 1515.82, 1520.78, 1514.65, 1519.19}, + {1744030920000LL, 1519.19, 1519.79, 1515.32, 1517.26}, + {1744030980000LL, 1517.25, 1520.21, 1514.95, 1516.89}, + {1744031040000LL, 1516.89, 1520.21, 1516.42, 1517.17}, + {1744031100000LL, 1517.18, 1520.16, 1515.78, 1519.41}, + {1744031160000LL, 1519.42, 1523.69, 1514.5, 1515.01}, + {1744031220000LL, 1515.01, 1515.15, 1508.72, 1512.83}, + {1744031280000LL, 1512.83, 1515.41, 1511.82, 1514.68}, + {1744031340000LL, 1514.69, 1517.25, 1514.46, 1515.77}, + {1744031400000LL, 1515.77, 1516.32, 1511.55, 1511.69}, + {1744031460000LL, 1511.68, 1517.7, 1511.4, 1516.2}, + {1744031520000LL, 1516.21, 1516.64, 1512.82, 1512.97}, + {1744031580000LL, 1512.96, 1514.04, 1511.3, 1511.31}, + {1744031640000LL, 1511.31, 1513.66, 1511.17, 1513.3}, +}; +constexpr FeedBar kPm2S1x_0623_2130[90] = { // signal bar 2025-06-23 21:30 UTC + {1750714200000LL, 2343.01, 2343.01, 2340.21, 2342.54}, + {1750714260000LL, 2342.55, 2347.8, 2342.54, 2345.86}, + {1750714320000LL, 2345.87, 2346.25, 2343.26, 2343.26}, + {1750714380000LL, 2343.26, 2343.27, 2340.54, 2341.89}, + {1750714440000LL, 2341.88, 2342.34, 2340.52, 2341.15}, + {1750714500000LL, 2341.15, 2342.5, 2337.28, 2339.72}, + {1750714560000LL, 2339.72, 2341.46, 2338.87, 2340.57}, + {1750714620000LL, 2340.57, 2341.48, 2340.36, 2340.93}, + {1750714680000LL, 2340.92, 2343, 2339.42, 2339.84}, + {1750714740000LL, 2339.84, 2340.14, 2339.11, 2339.51}, + {1750714800000LL, 2339.52, 2340.94, 2339.07, 2340.64}, + {1750714860000LL, 2340.64, 2342.51, 2340.48, 2341.96}, + {1750714920000LL, 2341.96, 2341.98, 2340, 2341.07}, + {1750714980000LL, 2341.08, 2341.95, 2341.07, 2341.95}, + {1750715040000LL, 2341.95, 2343.38, 2341.5, 2341.5}, + {1750715100000LL, 2341.5, 2344.54, 2341.5, 2344.54}, + {1750715160000LL, 2344.53, 2344.59, 2341, 2341.07}, + {1750715220000LL, 2341.07, 2343.78, 2341.07, 2343.73}, + {1750715280000LL, 2343.73, 2345.41, 2343.69, 2343.69}, + {1750715340000LL, 2343.7, 2343.86, 2340.34, 2340.35}, + {1750715400000LL, 2340.34, 2340.7, 2339.1, 2340.39}, + {1750715460000LL, 2340.38, 2340.64, 2339.5, 2339.84}, + {1750715520000LL, 2339.84, 2340.59, 2337.54, 2337.65}, + {1750715580000LL, 2337.65, 2338.18, 2331, 2335.95}, + {1750715640000LL, 2335.94, 2337.58, 2333.63, 2337.58}, + {1750715700000LL, 2337.57, 2340.47, 2337.57, 2339.01}, + {1750715760000LL, 2339, 2340.55, 2339, 2339.62}, + {1750715820000LL, 2339.62, 2342.23, 2339.1, 2341.85}, + {1750715880000LL, 2341.85, 2342, 2339.02, 2339.08}, + {1750715940000LL, 2339.04, 2340.67, 2339.04, 2339.89}, + {1750716000000LL, 2339.88, 2341.68, 2339.63, 2341.67}, + {1750716060000LL, 2341.68, 2342.71, 2339.51, 2341.05}, + {1750716120000LL, 2341.05, 2360, 2339.82, 2357.5}, + {1750716180000LL, 2357.82, 2369.42, 2352.51, 2362.71}, + {1750716240000LL, 2362.71, 2372.63, 2359.28, 2371.49}, + {1750716300000LL, 2371.5, 2378, 2366, 2372.2}, + {1750716360000LL, 2372.21, 2378.42, 2370.3, 2372.92}, + {1750716420000LL, 2372.84, 2375.97, 2372.04, 2374.47}, + {1750716480000LL, 2374.47, 2386.38, 2374.47, 2380.2}, + {1750716540000LL, 2380.21, 2393.24, 2380.12, 2388.93}, + {1750716600000LL, 2388.93, 2396.5, 2386.71, 2387.37}, + {1750716660000LL, 2387.38, 2409.52, 2387.29, 2405.6}, + {1750716720000LL, 2405.61, 2408.28, 2395.96, 2395.96}, + {1750716780000LL, 2395.96, 2398.77, 2391.07, 2396.34}, + {1750716840000LL, 2396.33, 2404.25, 2396.1, 2402.79}, + {1750716900000LL, 2402.8, 2406, 2396.77, 2401.59}, + {1750716960000LL, 2401.59, 2405.2, 2395, 2398.8}, + {1750717020000LL, 2398.8, 2404.76, 2397.28, 2400.9}, + {1750717080000LL, 2400.9, 2401.28, 2390, 2390.86}, + {1750717140000LL, 2390.86, 2396.24, 2389.75, 2395.51}, + {1750717200000LL, 2395.51, 2398.57, 2394.45, 2395.93}, + {1750717260000LL, 2395.94, 2399.99, 2393.95, 2398.56}, + {1750717320000LL, 2398.55, 2413, 2398.53, 2411.86}, + {1750717380000LL, 2411.86, 2416.6, 2411.86, 2413.98}, + {1750717440000LL, 2413.98, 2440, 2410.72, 2426.86}, + {1750717500000LL, 2426.86, 2432, 2423.14, 2424.03}, + {1750717560000LL, 2424.05, 2428.87, 2419.95, 2427.8}, + {1750717620000LL, 2427.81, 2428.06, 2417.06, 2422.13}, + {1750717680000LL, 2421.94, 2423.99, 2418.26, 2420.43}, + {1750717740000LL, 2420.41, 2426.36, 2419.2, 2425.28}, + {1750717800000LL, 2425.27, 2430.74, 2423.26, 2426.51}, + {1750717860000LL, 2426.52, 2429.47, 2422.44, 2427.73}, + {1750717920000LL, 2427.73, 2429.44, 2419.54, 2419.9}, + {1750717980000LL, 2419.91, 2424.13, 2402, 2412.99}, + {1750718040000LL, 2412.9, 2412.9, 2402.32, 2407.07}, + {1750718100000LL, 2407.07, 2409.83, 2402, 2405.82}, + {1750718160000LL, 2405.82, 2408.89, 2404.18, 2407.41}, + {1750718220000LL, 2407.41, 2407.89, 2396.33, 2398.67}, + {1750718280000LL, 2398.66, 2403.75, 2396.05, 2403.67}, + {1750718340000LL, 2403.67, 2405.39, 2398.8, 2399.81}, + {1750718400000LL, 2399.81, 2401.43, 2397.41, 2398.21}, + {1750718460000LL, 2398.21, 2398.21, 2392.29, 2394.55}, + {1750718520000LL, 2394.57, 2395.61, 2392.26, 2392.45}, + {1750718580000LL, 2392.45, 2393.15, 2382.14, 2392.64}, + {1750718640000LL, 2392.64, 2395.83, 2390.86, 2394.21}, + {1750718700000LL, 2394.2, 2395.51, 2388.03, 2389.51}, + {1750718760000LL, 2389.51, 2390.65, 2385.4, 2388.91}, + {1750718820000LL, 2388.9, 2395.61, 2388.9, 2394.76}, + {1750718880000LL, 2394.76, 2400.23, 2394.11, 2399.81}, + {1750718940000LL, 2399.8, 2400.29, 2397.38, 2397.54}, + {1750719000000LL, 2397.54, 2398.2, 2393.43, 2394.58}, + {1750719060000LL, 2394.58, 2396.56, 2394.39, 2394.79}, + {1750719120000LL, 2394.79, 2398.11, 2394.45, 2398.1}, + {1750719180000LL, 2398.11, 2399.24, 2397.09, 2398.77}, + {1750719240000LL, 2398.76, 2398.89, 2395.94, 2396.9}, + {1750719300000LL, 2396.89, 2396.9, 2389.94, 2390.16}, + {1750719360000LL, 2390.17, 2396.02, 2388.82, 2395.8}, + {1750719420000LL, 2395.86, 2408.71, 2395.59, 2403.9}, + {1750719480000LL, 2403.9, 2414.61, 2403.3, 2412.03}, + {1750719540000LL, 2412.03, 2414.62, 2404.01, 2405.81}, +}; +constexpr FeedBar kPm2S1x_1104_2130[90] = { // signal bar 2025-11-04 21:30 UTC + {1762291800000LL, 3162.91, 3164.56, 3149, 3151.51}, + {1762291860000LL, 3151.5, 3155.83, 3133.41, 3134.25}, + {1762291920000LL, 3134.25, 3140, 3107.11, 3109.34}, + {1762291980000LL, 3108.42, 3127.45, 3105.73, 3124.42}, + {1762292040000LL, 3124.43, 3129.33, 3093.63, 3095.54}, + {1762292100000LL, 3095.55, 3100, 3055, 3057.55}, + {1762292160000LL, 3057.55, 3108.47, 3056.76, 3106.75}, + {1762292220000LL, 3106.74, 3106.75, 3082.86, 3097.02}, + {1762292280000LL, 3097.01, 3097.3, 3072.22, 3077.12}, + {1762292340000LL, 3077.21, 3101.59, 3071.01, 3101.59}, + {1762292400000LL, 3101.41, 3119.53, 3098.24, 3115.34}, + {1762292460000LL, 3115.33, 3120.45, 3098.92, 3100.97}, + {1762292520000LL, 3100.96, 3100.96, 3085, 3090.95}, + {1762292580000LL, 3090.94, 3109.43, 3090.94, 3109.01}, + {1762292640000LL, 3109.01, 3109.03, 3102.45, 3106.06}, + {1762292700000LL, 3106.07, 3146.85, 3104.57, 3146.85}, + {1762292760000LL, 3146.84, 3158.99, 3140.43, 3141.36}, + {1762292820000LL, 3141.36, 3141.36, 3130.24, 3136.28}, + {1762292880000LL, 3136.28, 3145.45, 3133.34, 3143.38}, + {1762292940000LL, 3143.39, 3145.03, 3132.01, 3137.26}, + {1762293000000LL, 3137.28, 3144.62, 3134.52, 3141.72}, + {1762293060000LL, 3141.72, 3145.44, 3131.76, 3132.09}, + {1762293120000LL, 3132.09, 3141.76, 3128.15, 3132.61}, + {1762293180000LL, 3132.6, 3132.6, 3112.2, 3131.01}, + {1762293240000LL, 3131.01, 3147.19, 3130.17, 3145.7}, + {1762293300000LL, 3145.75, 3193.4, 3145.7, 3188.02}, + {1762293360000LL, 3188.02, 3215.34, 3178.32, 3205.99}, + {1762293420000LL, 3206, 3211.26, 3190, 3190.41}, + {1762293480000LL, 3190.42, 3205.61, 3188.76, 3205.12}, + {1762293540000LL, 3205.12, 3215, 3202.4, 3213.77}, + {1762293600000LL, 3213.78, 3214.87, 3197, 3207.36}, + {1762293660000LL, 3207.36, 3223, 3205.48, 3207.56}, + {1762293720000LL, 3207.56, 3218.11, 3205.86, 3215.95}, + {1762293780000LL, 3215.94, 3238.56, 3215.94, 3233.76}, + {1762293840000LL, 3233.75, 3234.37, 3224.47, 3230.26}, + {1762293900000LL, 3230.26, 3239.03, 3221.74, 3221.84}, + {1762293960000LL, 3221.84, 3250, 3221.2, 3247.54}, + {1762294020000LL, 3247.53, 3253, 3235.81, 3239.65}, + {1762294080000LL, 3239.64, 3251.98, 3238.39, 3248.72}, + {1762294140000LL, 3248.71, 3258.82, 3245.8, 3258.82}, + {1762294200000LL, 3258.83, 3271.1, 3254.24, 3265.87}, + {1762294260000LL, 3265.88, 3266.24, 3250.41, 3253.97}, + {1762294320000LL, 3253.97, 3268.38, 3250.18, 3257.61}, + {1762294380000LL, 3257.6, 3267.4, 3250.09, 3264.84}, + {1762294440000LL, 3264.84, 3267.49, 3259.99, 3266.29}, + {1762294500000LL, 3266.29, 3268.26, 3258.41, 3260.8}, + {1762294560000LL, 3260.8, 3263.66, 3254.84, 3262.7}, + {1762294620000LL, 3262.7, 3263.77, 3255.88, 3260.96}, + {1762294680000LL, 3260.95, 3260.95, 3254.55, 3258.78}, + {1762294740000LL, 3258.78, 3263.81, 3255.22, 3262.75}, + {1762294800000LL, 3262.77, 3262.77, 3251, 3260.36}, + {1762294860000LL, 3260.35, 3264.66, 3258.39, 3263.25}, + {1762294920000LL, 3263.25, 3266.28, 3259.6, 3259.6}, + {1762294980000LL, 3259.57, 3259.67, 3253, 3254.86}, + {1762295040000LL, 3254.86, 3260.31, 3254.84, 3259.76}, + {1762295100000LL, 3259.75, 3259.76, 3244.18, 3244.19}, + {1762295160000LL, 3244.18, 3252.63, 3238.54, 3250.41}, + {1762295220000LL, 3250.42, 3257.47, 3249.77, 3256.23}, + {1762295280000LL, 3256.23, 3256.8, 3250.28, 3254.78}, + {1762295340000LL, 3254.79, 3258.49, 3252.32, 3258.49}, + {1762295400000LL, 3258.48, 3262.76, 3256.58, 3259.59}, + {1762295460000LL, 3259.59, 3262, 3251.34, 3260.85}, + {1762295520000LL, 3260.86, 3261, 3256.56, 3259.89}, + {1762295580000LL, 3259.88, 3268.98, 3259.88, 3268.05}, + {1762295640000LL, 3268.05, 3279.24, 3265.3, 3270.74}, + {1762295700000LL, 3270.74, 3289.66, 3270, 3277.71}, + {1762295760000LL, 3277.72, 3285.35, 3272.49, 3272.5}, + {1762295820000LL, 3272.49, 3272.49, 3257.99, 3259.77}, + {1762295880000LL, 3259.76, 3266.25, 3254.47, 3265.67}, + {1762295940000LL, 3265.66, 3269.17, 3260.01, 3261.62}, + {1762296000000LL, 3261.61, 3261.61, 3254.68, 3255.37}, + {1762296060000LL, 3255.36, 3258.01, 3251.6, 3257.07}, + {1762296120000LL, 3257.06, 3261.18, 3255, 3255}, + {1762296180000LL, 3255, 3256.67, 3246.3, 3248.52}, + {1762296240000LL, 3248.51, 3252.9, 3243.53, 3250.91}, + {1762296300000LL, 3250.87, 3255, 3250.86, 3254.99}, + {1762296360000LL, 3254.99, 3259.25, 3254.99, 3255.2}, + {1762296420000LL, 3255.19, 3255.2, 3250.31, 3251.37}, + {1762296480000LL, 3251.37, 3251.38, 3245.5, 3250.2}, + {1762296540000LL, 3250.19, 3250.2, 3242, 3242.83}, + {1762296600000LL, 3242.83, 3242.83, 3231.05, 3233.79}, + {1762296660000LL, 3233.78, 3234.85, 3231, 3234.2}, + {1762296720000LL, 3234.21, 3235.44, 3232.7, 3232.81}, + {1762296780000LL, 3232.8, 3238.19, 3230.1, 3230.1}, + {1762296840000LL, 3230.1, 3230.11, 3226.11, 3228.48}, + {1762296900000LL, 3228.47, 3236, 3227.19, 3235.99}, + {1762296960000LL, 3235.99, 3239.6, 3231.72, 3233.5}, + {1762297020000LL, 3233.5, 3236.92, 3229.55, 3232.39}, + {1762297080000LL, 3232.38, 3232.76, 3227.45, 3232.76}, + {1762297140000LL, 3232.76, 3246.65, 3232.19, 3246.44}, +}; diff --git a/tests/test_adapter_margin_schedule_differential.cpp b/tests/test_adapter_margin_schedule_differential.cpp index e2f8bcd2..f50bd64b 100644 --- a/tests/test_adapter_margin_schedule_differential.cpp +++ b/tests/test_adapter_margin_schedule_differential.cpp @@ -64,6 +64,14 @@ * openings -- 36 tapes, all TradingView's rows (the kernel's post-fill * point now measures from the fill's own waypoint: a limit filled on its * way down faces the bar's low, a short filled on its way up its high); + * intrabar shapes: the magnifier under COOF and POOC and on a full-margin + * short -- TradingView checks at its own 2-minute intrabars (a model of + * which books all 16 tapes' calls), the adapter at the host's 1-minute + * samples (the same model on those books the adapter's); 10 agree, six + * recorded, plus 8 chart controls; + * TradingView intrabars: its own request.security_lower_tf "2" arrays + * equal the feed's minute pairs owned by the chart bar of their last + * minute; * * Source-bound (includes pineforge/source): release profile only. */ @@ -75,6 +83,7 @@ #include #include #include +#include #include #include #include @@ -102,6 +111,7 @@ struct FeedBar { #include "fixtures/margin_entry_bar/m10_bars.inc" #include "fixtures/intrabar_margin/bars_1m.inc" #include "fixtures/margin_entry_bar/pm2_bars.inc" +#include "fixtures/intrabar_margin/pm2_bars_1m.inc" // ------------------------------------------------------------ TV policy // The twin's TradingView money and slice (test_native_margin_hooks_twin.cpp, @@ -1584,6 +1594,246 @@ void m7_shapes_on_tapes() { } } +// ---- TradingView's bar magnifier on a 15-minute chart samples 2-MINUTE +// intrabars (TradingView's own table: chart 15 -> intrabar 2; "15 / 2 yields +// 7.5 bars, which is rounded down to 7"), epoch-aligned, each owned by the +// chart bar that holds its LAST minute: request.security_lower_tf(..., "2", +// ...) prints [14, 16, .., 28] for a :15 chart bar and [30, .., 42] for a :30 +// one (tests/fixtures/intrabar_margin/pm2-ltf-*). The model below is that +// broker's margin check -- at each sample's adverse extreme, TradingView's +// money and slice (the twin's) -- and, with one-minute samples, the adapter's. +struct ModelCall { + std::int64_t bar; + double price; + double qty; +}; + +double tv_slice(double held, double mark, double equity0, double entry, double margin, + bool is_long) { + const double equity = equity0 + (is_long ? 1.0 : -1.0) * held * (mark - entry); + const double required = held * mark * margin; + if (!(required > equity)) return 0.0; + const double raw = (required - equity) / (mark * margin); + const double minimum = std::floor(raw / 0.0001) * 0.0001; + double units = std::floor(4.0 * minimum / 0.0001 + 1e-6) * 0.0001; + if (!(units > 0.0) && raw > 0.0 && raw < 1.0) units = std::min(1.0, held); + return std::min(held, units); +} + +std::vector tv_intrabar_model(const std::vector& one, std::int64_t first_bar, + std::int64_t end, double held, double entry, + double margin, bool is_long, bool two_minute) { + const std::int64_t minute = 60000; + const std::int64_t chart = 15 * minute; + struct Sample { std::int64_t bar; double high; double low; }; + std::vector samples; + for (std::size_t i = 0; i < one.size(); ++i) { + const Bar& b = one[i]; + if (!two_minute) { + const std::int64_t owner = b.timestamp - b.timestamp % chart; + if (owner >= first_bar && owner < end) samples.push_back({owner, b.high, b.low}); + continue; + } + if (b.timestamp % (2 * minute) != 0 || i + 1 >= one.size()) continue; + const Bar& next = one[i + 1]; + const std::int64_t owner = next.timestamp - next.timestamp % chart; + if (owner >= first_bar && owner < end) + samples.push_back({owner, std::max(b.high, next.high), std::min(b.low, next.low)}); + } + double equity = 1000.0; + std::vector out; + for (const Sample& s : samples) { + const double mark = is_long ? s.low : s.high; + const double units = tv_slice(held, mark, equity, entry, margin, is_long); + if (!(units > 0.0)) continue; + out.push_back({s.bar, mark, units}); + equity += (is_long ? 1.0 : -1.0) * units * (mark - entry); + held -= units; + if (!(held > 1e-12)) break; + } + return out; +} + +bool same_calls(const std::vector& model, const std::vector& rows) { + std::vector calls; + for (const auto& r : rows) if (r.margin_call) calls.push_back(r); + if (calls.size() != model.size()) return false; + auto sorted = model; + std::sort(sorted.begin(), sorted.end(), [](const ModelCall& a, const ModelCall& b) { + return a.bar != b.bar ? a.bar < b.bar : a.qty < b.qty; + }); + for (std::size_t i = 0; i < calls.size(); ++i) { + if (calls[i].bar != sorted[i].bar || std::abs(calls[i].price - sorted[i].price) > 5e-3 + || std::abs(calls[i].qty - sorted[i].qty) > 5e-5) { + return false; + } + } + return true; +} + +void print_model(const char* side, const std::vector& rows) { + std::printf(" %-8s", side); + for (const auto& r : rows) std::printf(" | %s %.17g @ %.4f MC", utc_text(r.bar).c_str(), r.qty, r.price); + std::printf("\n"); +} + +// ---- item 3's shapes on tapes (tests/fixtures/intrabar_margin) +// A carried book under the magnifier crossing its line at an intrabar sample: +// pm-i3-eth-mag-* lane PAR-MARGIN's plain leveraged long; +// pm2-i3-coof-* the same scripts under calc_on_order_fills: the SAME +// rows as without it, 4 of 4; +// pm2-i3-pooc-mag-* a leveraged long under process_orders_on_close; +// pm2-i3-s1x-mag-* a full-margin (1x) short; +// and the chart-path controls pm2-i3-pooc-chart-* / pm2-i3-s1x-chart-*, one +// call at the chart bar's extreme, which the adapter books as before (8 of 8). +// On every magnified tape here (16) TradingView's calls are the model's at +// its 2-minute intrabars; the adapter books the model's at the feed's 1-minute +// samples (the lane admits the kernel's IntrabarSample point under POOC, +// COOF and on a full-margin short, as it was on the plain leveraged long). The +// two sample grids agree on 10 of the 16 (and on the four pm2-m7-mag-* above) +// and part on six -- 0130 plain and COOF (TradingView's 01:42-01:43 intrabar +// has one low, 2673.33; the feed's 01:42 minute crosses at 2690 first), pooc +// 0109, 0402 (the straddling 20:14-20:16 intrabar's 1908.17 is the signal +// bar's last minute), 0707 and s1x 0623 -- which stay recorded: the adapter +// samples what its host feeds (1 minute), not TradingView's intrabar +// timeframe. Landing that is a magnifier capability (every magnified fill, not +// only margin), not this lane's. +struct IntrabarShape { + const char* slug; + const FeedBar* one; // 90 one-minute bars from the signal bar +}; + +void intrabar_shapes_on_tapes() { + const std::string dir = std::string(PINEFORGE_HM_M7A_FIXTURE_DIR) + "/../intrabar_margin"; + const IntrabarShape tapes[] = { + {"pm-i3-eth-mag-0622", kI3_0622}, {"pm-i3-eth-mag-0130", kI3_0130}, + {"pm-i3-eth-mag-0824", kI3_0824}, {"pm-i3-eth-mag-0406", kI3_0406}, + {"pm2-i3-coof-0622", kI3_0622}, {"pm2-i3-coof-0130", kI3_0130}, + {"pm2-i3-coof-0824", kI3_0824}, {"pm2-i3-coof-0406", kI3_0406}, + {"pm2-i3-pooc-mag-0109-0115", kPm2Pooc_0109_0115}, {"pm2-i3-pooc-mag-0402-2000", kPm2Pooc_0402_2000}, + {"pm2-i3-pooc-mag-0707-1600", kPm2Pooc_0707_1600}, {"pm2-i3-pooc-mag-1010-1900", kPm2Pooc_1010_1900}, + {"pm2-i3-s1x-mag-0201-1100", kPm2S1x_0201_1100}, {"pm2-i3-s1x-mag-0407-1145", kPm2S1x_0407_1145}, + {"pm2-i3-s1x-mag-0623-2130", kPm2S1x_0623_2130}, {"pm2-i3-s1x-mag-1104-2130", kPm2S1x_1104_2130}, + {"pm2-i3-pooc-chart-0109-0115", kPm2Pooc_0109_0115}, {"pm2-i3-pooc-chart-0402-2000", kPm2Pooc_0402_2000}, + {"pm2-i3-pooc-chart-0707-1600", kPm2Pooc_0707_1600}, {"pm2-i3-pooc-chart-1010-1900", kPm2Pooc_1010_1900}, + {"pm2-i3-s1x-chart-0201-1100", kPm2S1x_0201_1100}, {"pm2-i3-s1x-chart-0407-1145", kPm2S1x_0407_1145}, + {"pm2-i3-s1x-chart-0623-2130", kPm2S1x_0623_2130}, {"pm2-i3-s1x-chart-1104-2130", kPm2S1x_1104_2130}, + }; + int agree = 0; + int recorded = 0; + for (const IntrabarShape& tape : tapes) { + std::printf("-- intrabar shape %s\n", tape.slug); + const std::string at = dir + "/" + tape.slug; + const ProbeScript ps = parse_probe(at + "/strategy.pine"); + REQUIRE(ps.entries.size() == 1); + const auto one = feed_bars(tape.one, 90); + const auto adapter = run_probe(ps, one, true, 0.0001, 0.01); + const auto tv = tape_exit_rows(at); + print_exits("tape", tv); + print_exits("adapter", adapter); + if (!ps.magnifier) { + CHECK(same_exits(adapter, tv)); + continue; + } + // The book the scripts open: K x the equity at the signal close, filled + // at the entry bar's open (the signal close under POOC). + const std::int64_t chart = 15 * 60000LL; + const ProbeEntry& e = ps.entries.front(); + const double signal_close = one[14].close; + const double held = std::floor(e.K * ps.capital / signal_close * 1000.0) / 1000.0; + const double entry = ps.pooc ? signal_close : one[15].open; + const std::int64_t first = e.at + chart; + const std::int64_t end = e.at + (ps.pooc ? 4 : 5) * chart; + const auto tv_rule = tv_intrabar_model(one, first, end, held, entry, ps.margin / 100.0, + e.is_long, true); + const auto feed_rule = tv_intrabar_model(one, first, end, held, entry, ps.margin / 100.0, + e.is_long, false); + print_model("2m", tv_rule); + print_model("1m", feed_rule); + // TradingView's rule, on all 20: the per-sample check at its intrabars. + CHECK(same_calls(tv_rule, tv)); + // The adapter's calls: the same rule at the feed's one-minute samples. + CHECK(same_calls(feed_rule, adapter)); + bool grids_agree = tv_rule.size() == feed_rule.size(); + for (std::size_t i = 0; grids_agree && i < tv_rule.size(); ++i) { + grids_agree = tv_rule[i].bar == feed_rule[i].bar + && std::abs(tv_rule[i].price - feed_rule[i].price) <= 5e-3 + && std::abs(tv_rule[i].qty - feed_rule[i].qty) <= 5e-5; + } + if (grids_agree) { + ++agree; + CHECK(same_exits(adapter, tv)); + } else { + ++recorded; + // Recorded: the call lands on TradingView's bar, at a sample the + // one-minute grid resolves differently. + CHECK(!adapter.empty() && adapter.front().margin_call); + CHECK(!tv.empty() && tv.front().margin_call); + CHECK(!adapter.empty() && !tv.empty() && adapter.front().bar == tv.front().bar); + } + } + std::printf(" magnified tapes: %d on both grids, %d recorded (2-minute intrabars)\n", agree, + recorded); + CHECK(agree == 10); + CHECK(recorded == 6); +} + +// ---- TradingView's own intrabars (tests/fixtures/intrabar_margin/pm2-ltf-*): +// request.security_lower_tf(syminfo.tickerid, "2", time/open/high/low/close) +// inside six chart bars, printed as order comments. Each array equals the +// feed's one-minute bars paired from an even minute, owned by the chart bar +// holding the pair's second minute. +std::vector printed_array(const std::string& cell, const char* tag) { + std::vector out; + if (cell.rfind(tag, 0) != 0) return out; + std::stringstream in(cell.substr(std::strlen(tag) + 1)); // past "[" + std::string item; + while (std::getline(in, item, ',')) out.push_back(std::atof(item.c_str())); + return out; +} + +void tradingview_intrabars() { + const std::string dir = std::string(PINEFORGE_HM_M7A_FIXTURE_DIR) + "/../intrabar_margin"; + struct Probe { const char* slug; std::int64_t bar; const FeedBar* one; }; + const Probe probes[] = { + {"pm2-ltf-0402", utc_ms(2025, 4, 2, 20, 15), kPm2Pooc_0402_2000}, + {"pm2-ltf-0109", utc_ms(2026, 1, 9, 1, 30), kPm2Pooc_0109_0115}, + {"pm2-ltf-0707", utc_ms(2025, 7, 7, 16, 15), kPm2Pooc_0707_1600}, + {"pm2-ltf-0623", utc_ms(2025, 6, 23, 22, 0), kPm2S1x_0623_2130}, + {"pm2-ltf-0130", utc_ms(2026, 1, 30, 1, 30), kI3_0130}, + {"pm2-ltf-1010", utc_ms(2025, 10, 10, 19, 15), kPm2Pooc_1010_1900}, + }; + for (const Probe& p : probes) { + std::map> printed; + for (const auto& cell : tape_rows(dir + "/" + p.slug)) { + if (cell.size() < 4 || cell[1].rfind("Entry", 0) != 0) continue; + for (const char* tag : {"T", "O", "H", "L", "C"}) { + auto values = printed_array(cell[3], tag); + if (!values.empty() && cell[3][1] == '[') printed[tag] = values; + } + } + // The feed's pairs owned by this chart bar. + std::vector t, o, h, l, c; + for (int i = 0; i + 1 < 90; ++i) { + const FeedBar& a = p.one[i]; + const FeedBar& b = p.one[i + 1]; + if (a.ts % 120000 != 0 || b.ts - b.ts % 900000 != p.bar) continue; + t.push_back(static_cast((a.ts % 3600000) / 60000)); + o.push_back(a.open); + h.push_back(std::max(a.high, b.high)); + l.push_back(std::min(a.low, b.low)); + c.push_back(b.close); + } + std::printf("-- TradingView 2m intrabars %s: %zu printed, %zu from the feed\n", p.slug, + printed["T"].size(), t.size()); + CHECK(printed["T"] == t); + CHECK(printed["O"] == o); + CHECK(printed["H"] == h); + CHECK(printed["L"] == l); + CHECK(printed["C"] == c); + } +} + } // namespace int main() { @@ -1603,6 +1853,8 @@ int main() { test("M10 TradingView tapes", m10_tradingview_tapes); test("intrabar margin tapes", intrabar_margin_tapes); test("PAR-MARGIN-2 M7 shapes", m7_shapes_on_tapes); + test("PAR-MARGIN-2 intrabar shapes", intrabar_shapes_on_tapes); + test("PAR-MARGIN-2 TradingView intrabars", tradingview_intrabars); std::printf("%d checks, %d failures\n", checks, failures); return failures == 0 ? 0 : 1; } From 449ed4ffe6153a9fa805e9222c27eaf498971f8d Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:36:14 +0800 Subject: [PATCH 45/83] Adapter: a margin call books the nearest tick of the print it fired at, TradingView's market fill (R5 lane PAR-MARGIN-2, item 2) At an exact half-cent low the pre-open margin slice booked a tick below TradingView (pm-m10-f-k4-1007: 12.105 -> 12.10 against 12.11; -gapup-0501: 10.025 -> 10.02 against 10.03). The helper that produced it is directional_tick, reached through source_stop_resolved -> source_stop_fill: the slice rests as a Stop at the raw adverse print, so its fill took the stop-LEVEL rule (a sell's adverse tick). TradingView's margin call is a MARKET execution at the print (its raw report files the 1007 call as "tp": "MARKET", "p": 12.11), and TradingView books a fill at a print on the print's nearest tick. Three lab tv exports prove which rule is whose, beyond margin calls, on two symbols with different mintick (tests/fixtures/half_tick_rounding): - pm2-round-f (NYSE:F, mintick 0.01) and pm2-round-es (CME_MINI:ES1!, mintick 0.25): a buy stop, sell stop, buy limit and sell limit, each at a half-tick LEVEL crossed inside the next bar: TradingView books the DIRECTIONAL tick 8 of 8 (no nearest rule, exact or floating, and no half-even rule fits all eight) -- the adapter's rule, before and after; - pm2-round-f-print (NYSE:F): a market buy, a market sell and a sell stop already through the open, each filled at a half-cent OPEN print: TradingView books the NEAREST tick 3 of 3 -- the adapter's rule (source_bar_fill_tick), before and after. So directional_tick stays for every stop and limit: the margin fill takes the print rule. source_stop_resolved books a Margin-family row crossed in-path at source_margin_fill_price (the print's nearest tick, then the exit side's slippage), and source_margin_fill_price rounds the print at zero slippage too (it returned the raw print, so a kernel liquidation of a long at an off-grid low booked the raw price). An on-grid print is its own tick (source_bar_fill_tick is idempotent there), so an on-grid feed -- every corpus ETH probe -- books the same bits. Callers of the changed fill rule (population-visible only at an off-grid print): source_stop_resolved's in-path branch (the pre-open slice, schedule_preopen_margin_slice); source_margin_fill_price's callers -- resolve_terms' kernel-liquidation branch (every kernel margin call of a Pine run) and submit_margin_call_units (the adapter's checkpoint slices at the open, close and FX roll). Tests: the differential's M10 half-cent pins flip to TradingView's 12.11 and 10.03 (every M10 price now compared exactly) and a "half-tick fills" section replays the three probes from their own strategy.pine (bars: the lab feeds, tests/fixtures/half_tick_rounding/bars.inc); tests/test_native_margin_hooks_twin restates the rule on its bare-kernel twin (tv_bar_fill_tick at zero slippage) and its MG-B/G pin moves with the adapter: an off-grid 99.999999996 print books 100, bit for bit on both sides. Docs: native-engine.md, ADR-0001, design MG15, pine-adapter-kernel-notes, the fixture README; anchors carried by an exact line map. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 3ea143e5adc2ae6c0f2d197a92099b38821bf490) INT26 pick note: the docs merged on anchor digits only; no pinned witness behaviour moves on this tree (the margin twin's MG-B/G pin is the lane's). --- docs/adr/0001-kernel-adapter-boundary.md | 17 ++-- docs/design/native-feature-parity.md | 64 ++++++------- docs/pages/native-engine.md | 8 +- docs/pine-adapter-kernel-notes.md | 6 +- src/source/pine_adapter.cpp | 24 ++++- tests/fixtures/half_tick_rounding/README.md | 38 ++++++++ tests/fixtures/half_tick_rounding/bars.inc | 61 +++++++++++++ .../half_tick_rounding/pm2-round-es/meta.json | 16 ++++ .../pm2-round-es/metrics.json | 40 ++++++++ .../pm2-round-es/strategy.pine | 23 +++++ .../pm2-round-es/tv_trades.csv | 9 ++ .../pm2-round-f-print/meta.json | 16 ++++ .../pm2-round-f-print/metrics.json | 40 ++++++++ .../pm2-round-f-print/strategy.pine | 19 ++++ .../pm2-round-f-print/tv_trades.csv | 7 ++ .../half_tick_rounding/pm2-round-f/meta.json | 16 ++++ .../pm2-round-f/metrics.json | 40 ++++++++ .../pm2-round-f/strategy.pine | 23 +++++ .../pm2-round-f/tv_trades.csv | 9 ++ ...t_adapter_margin_schedule_differential.cpp | 91 ++++++++++++++----- tests/test_native_margin_hooks_twin.cpp | 21 ++++- 21 files changed, 518 insertions(+), 70 deletions(-) create mode 100644 tests/fixtures/half_tick_rounding/README.md create mode 100644 tests/fixtures/half_tick_rounding/bars.inc create mode 100644 tests/fixtures/half_tick_rounding/pm2-round-es/meta.json create mode 100644 tests/fixtures/half_tick_rounding/pm2-round-es/metrics.json create mode 100644 tests/fixtures/half_tick_rounding/pm2-round-es/strategy.pine create mode 100644 tests/fixtures/half_tick_rounding/pm2-round-es/tv_trades.csv create mode 100644 tests/fixtures/half_tick_rounding/pm2-round-f-print/meta.json create mode 100644 tests/fixtures/half_tick_rounding/pm2-round-f-print/metrics.json create mode 100644 tests/fixtures/half_tick_rounding/pm2-round-f-print/strategy.pine create mode 100644 tests/fixtures/half_tick_rounding/pm2-round-f-print/tv_trades.csv create mode 100644 tests/fixtures/half_tick_rounding/pm2-round-f/meta.json create mode 100644 tests/fixtures/half_tick_rounding/pm2-round-f/metrics.json create mode 100644 tests/fixtures/half_tick_rounding/pm2-round-f/strategy.pine create mode 100644 tests/fixtures/half_tick_rounding/pm2-round-f/tv_trades.csv diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index c1f0d26b..4e60fe21 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -209,8 +209,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:677`) for TV sizing and fill spelling; `validate_execution_precommit` (`pine_strategy_host.cpp:702`), whose - `validate_precommit` (`pine_adapter.cpp:13068`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:13313`) to own the opening margin decision; `owns_lot_excursions() = true` + `validate_precommit` (`pine_adapter.cpp:13079`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:13324`) to own the opening margin decision; `owns_lot_excursions() = true` (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:787`), so the Pine host keeps its own per-lot MFE/MAE, `ab9714be`'s model (RULING A48). Measured against TradingView by R5 lane H-MEASURE (`tests/test_e19_excursion_tape.cpp`, `lab tv` tapes): where @@ -249,8 +249,8 @@ kernel and own these quirks, each at its site: (`select_reservation_growth_sources` src/compat/pine/reservation_expansion.cpp:9). - **Margin.** The kernel owns the margin *mechanism* — the level solve, the check points, the kernel request, its re-pricing, the receipt — and the adapter answers its three policy hooks - with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14103`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14172`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14119`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:14188`) 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 @@ -261,7 +261,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:16876`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16892`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -301,7 +301,10 @@ kernel and own these quirks, each at its site: on the corpus), so six tapes part where the two grids resolve a crossing differently -- a recorded divergence whose fix is the magnifier's sampling, every magnified fill with it, not this check. A full-margin long (its one-contract money call) and a short under - `process_orders_on_close` keep their routes there. + `process_orders_on_close` keep their routes there. A margin call books the nearest tick of the + print it fired at, as TradingView's market fills do, where a stop or limit crossed at its own + off-grid level keeps its directional tick (`source_margin_fill_price`; + `tests/fixtures/half_tick_rounding`, R5 lane PAR-MARGIN-2). - **Calculation timing.** The cadence itself is the kernel's: a `calc_on_order_fills` strategy projects `NativeCalculationTrigger::BarCloseAndFills` with TradingView's guard literal as `max_recalculations_per_point`. What stays are the specifics COOF adds on top — @@ -670,7 +673,7 @@ Every line number in this section names its symbol on this tree, which |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:342`), `source_level_on_price_grid` (`:328`), `nearest_tick` (`:266`) / `source_bar_fill_tick` (`:287`) / `directional_tick` (`:311`), 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`, `tests/test_native_price_grid.cpp` | | `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13953`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:15008`), `chart_day_key` (`pine_adapter.cpp:13850`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13260-13261`): 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_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:13271-13272`): 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:2361-2363`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2388-2396`, `margin.maintenance_long` `pine_adapter.cpp:2403`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index ef1a80f0..03770766 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -50,16 +50,16 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2738-2749; `entry()` pine_adapter.cpp:5314, `order()` pine_adapter.cpp:10515) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5698-5745; `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:8417 → `Flatten{}` pine_adapter.cpp:8446-8447; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2708-3915 → `host.replace` pine_adapter.cpp:2724-2804) | **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:4723-4769, `cancel_all` pine_adapter.cpp:18104-18119; `live_by_source_key_` pine_adapter.hpp:1999; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **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. | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4723-4769, `cancel_all` pine_adapter.cpp:18120-18135; `live_by_source_key_` pine_adapter.hpp:1999; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5538-5573, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7508-7525) | **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:2747-2753) | **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:13038; lowering `submit_one` pine_adapter.cpp:9075-9087, `BindCohort` pine_adapter.cpp:9576-9582, `submit_leg` pine_adapter.cpp:9600-9624; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8680-8683; `materialize_relative_exits` pine_adapter.cpp:17394-17435, pine_adapter.hpp:1824-1836) | **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:5624-5658. `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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19178-19207) | **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:2001-2019); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10915-10939), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1585-1608, `CloseCallsiteState` pine_adapter.hpp:1679-1694; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15703, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15937) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:13049; lowering `submit_one` pine_adapter.cpp:9075-9087, `BindCohort` pine_adapter.cpp:9576-9582, `submit_leg` pine_adapter.cpp:9600-9624; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8680-8683; `materialize_relative_exits` pine_adapter.cpp:17410-17451, pine_adapter.hpp:1824-1836) | **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:5624-5658. `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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19194-19223) | **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:2001-2019); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10915-10939), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1585-1608, `CloseCallsiteState` pine_adapter.hpp:1679-1694; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15719, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15953) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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 @@ -83,10 +83,10 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | SZ5 | Fee-net sizing (commission reserve `/(1+c)`, asked of the kernel as `reserve_percent_fee` pine_adapter.cpp:2588; `percent_commission_live_equity` pine_adapter.cpp:4329-4369) | **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. | | SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2514-2529, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6680-6706) | **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:592, 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:4992-4995; 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:12513-12541, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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. | +| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:12524-12552, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:9002-9312) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4444-4459) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6741), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7948), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12769), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13365, pine_adapter.cpp:13418) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15751) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18774) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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). | +| 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:6741), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7948), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12780), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13376, pine_adapter.cpp:13429) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15767) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18790) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7210-7561; ledger `close_batch_bar_` pine_adapter.hpp:2042-2053; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | @@ -95,21 +95,21 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2362) | **yes** — `allowed_open_directions` native_run_spec.hpp:597 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:595 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13260-13263) is TV. | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:595 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13271-13274) is TV. | | MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6230-6279, cap left unset `max_open_lots` pine_adapter.cpp:2361-2363) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13314-13379 and leaves the kernel gate unset pine_adapter.cpp:13313-13315) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13969-13970, `margin_pct` pine_adapter.cpp:13377) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:14069-14280, `submit_margin_call_units` pine_adapter.cpp:14241-14287, `schedule_margin_call_path` pine_adapter.cpp:18257-18358) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14000-14036) | **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. | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13325-13390 and leaves the kernel gate unset pine_adapter.cpp:13324-13326) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13980-13981, `margin_pct` pine_adapter.cpp:13388) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:14085-14296, `submit_margin_call_units` pine_adapter.cpp:14257-14303, `schedule_margin_call_path` pine_adapter.cpp:18273-18374) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14011-14047) | **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. | | MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16355-16371, `apply_fx_opening_margin_slice` pine_adapter.cpp:18359-18387) | **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:16846-16885) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13349-13352) | **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:13930-13944, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14994-15024) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15037-15271) | **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:13740-13819) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1323 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:13958, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14017, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14758-14798, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14282-14409, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14282) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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, `tests/fixtures/margin_entry_bar/pm2-*`). 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 (`tests/fixtures/intrabar_margin`); six tapes part on the one-minute samples the host feeds, a recorded divergence of the magnifier's sampling. | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15275-15333) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16371-16387, `apply_fx_opening_margin_slice` pine_adapter.cpp:18375-18403) | **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:16862-16901) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13360-13363) | **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:13941-13955, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:15010-15040) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15053-15287) | **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:13751-13830) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1323 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:13969, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14028, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14774-14814, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14298-14425, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14298) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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, `tests/fixtures/margin_entry_bar/pm2-*`). 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 (`tests/fixtures/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; `tests/fixtures/half_tick_rounding`, PAR-MARGIN-2). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15291-15349) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -117,10 +117,10 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7542-7546 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16190) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16206) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `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:7027 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7078 (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:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5635, `flush_coof_tail` pine_adapter.cpp:16333, `end_coof_recalc` pine_adapter.cpp:5656) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5635, `flush_coof_tail` pine_adapter.cpp:16349, `end_coof_recalc` pine_adapter.cpp:5656) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7166-7170; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | | 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:7536-7655 **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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19178-19207 | 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:19194-19223 | 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:523, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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:604. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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:1274 | 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) | @@ -298,7 +298,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3014-3082) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3018-3086) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:45, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -522,11 +522,11 @@ line numbers are this tree's (683a82f); kernel semantics are | 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:2362); 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:13260-13261; 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:16846, `update_risk_state` pine_adapter.cpp:13901-13928): 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:13253), 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_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:13271-13272; 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:16862, `update_risk_state` pine_adapter.cpp:13912-13939): 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:13264), 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:1200-1208): +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:17724-17733); 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:16349); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16461), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15024-15033, `submit_intraday_loss_close` pine_adapter.cpp:14994-15022), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17203); 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:5986, `:7655`, `:8379`, `:8507`, `:11484`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18102-18105) | `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:13892-13894); 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:13798-13819, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_intraday_loss` | day-open equity at the day's first bar (`intraday_start_equity` pine_adapter.cpp:16365); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16477), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15040-15049, `submit_intraday_loss_close` pine_adapter.cpp:15010-15038), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17219); 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:5986, `:7655`, `:8379`, `:8507`, `:11484`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18118-18121) | `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:13903-13905); 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:13809-13830, `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, @@ -1300,7 +1300,7 @@ unchanged after it. | 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5782) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6290-6303); 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:16190-16219) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:16206-16235) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | @@ -1345,7 +1345,7 @@ unchanged after it. | **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:16355-16371) 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:2646) 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:16371-16387) 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:2646) 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. | @@ -1443,7 +1443,7 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1693-1694) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6995-7007) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1697-1698) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6995-7007) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). 2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5629-5633) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1660-1667) is calibrated against it. 3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6848-6855). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2553); valid in bar-open, applied and tick callbacks. @@ -1531,7 +1531,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:1329) (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:373), 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:13724-13822); 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:13735-13833); 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:4675-4688 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 527c45c5..2517322f 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1483,7 +1483,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:14103-14136`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:14119-14152`), 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 @@ -1547,7 +1547,11 @@ minute on the corpus, so six tapes part where the two grids resolve a crossing differently -- recorded, not fixed here: sampling the magnifier at TradingView's intrabar timeframe moves every magnified fill, not only margin calls. A full-margin long (its one-contract money call) and a short under -`process_orders_on_close` keep their routes there. +`process_orders_on_close` keep their routes there. A margin call is a market +execution at the print it fired at, so an off-grid print books its nearest +tick (12.105 books 12.11), as every TradingView market fill does, while a stop +or limit crossed at its own off-grid level keeps its directional tick +(`tests/fixtures/half_tick_rounding`). ### Risk limits diff --git a/docs/pine-adapter-kernel-notes.md b/docs/pine-adapter-kernel-notes.md index eff81e22..caf0f4fd 100644 --- a/docs/pine-adapter-kernel-notes.md +++ b/docs/pine-adapter-kernel-notes.md @@ -312,7 +312,11 @@ Since R5 lane PAR-MARGIN the adapter keeps the placement quantity above 100 % as well: eight `lab tv` tapes of a 390-400 % stop on NYSE:F (`tests/fixtures/margin_entry_bar/pm-m10-*`) open the placement quotient, never the fill's (`tests/test_adapter_margin_schedule_differential.cpp`, "M10 on -tapes"). +tapes"). The margin call those tapes book at a half-cent low is TradingView's +market execution at that print, so since R5 lane PAR-MARGIN-2 the pre-open +slice books the print's nearest tick (12.105 books 12.11), where a stop or +limit crossed at its own off-grid level keeps the directional tick +(`tests/fixtures/half_tick_rounding`). ```text design-declined-reversal-close-leg: called at the KI-54 reversal-decline diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index a311996c..aa28e876 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -12120,7 +12120,18 @@ native_order::ExecutionTerms PineExecutionAdapter::resolve_terms( const bool open_gapped = (!non_open || armed_after_open_fill) && std::isfinite(level) && (facts.is_buy ? open_px >= level : open_px <= level); - if (!open_gapped) return source_stop_fill(); + // The pre-open margin slice rests at the raw adverse print + // (schedule_preopen_margin_slice) but is TradingView's margin call, a + // MARKET execution at that print: the print's nearest tick, then the + // exit side's slippage (source_margin_fill_price), not a stop level's + // adverse tick -- 12.105 books 12.11 (lab tv tapes pm-m10-f-k4-1007 + // and -gapup-0501, R5 lane PAR-MARGIN-2). A stop crossed at its own + // off-grid LEVEL keeps the directional rule (pm2-round-f, -es). + if (!open_gapped) { + return source.family == PineOrderFamily::Margin + ? source_margin_fill_price(level, facts.is_buy) + : source_stop_fill(); + } // ab9714be try_exit_open_gap_fill books bar.open (the script-bar // open), even when the matching sample is a later one-price tick. const double rounded = source_bar_fill_tick( @@ -14124,10 +14135,15 @@ double PineExecutionAdapter::source_margin_units( // The forced execution price of a liquidation that fired at `fire`: the fire // price on the chart tick ladder, then the EXIT side's own market slippage // (ab9714be pine_fills.cpp:1712-1726 and :2649-2658). Reducing a long is a -// sell (slippage subtracts); reducing a short is a buy (it adds). At zero -// slippage this is the identity on the ladder. +// sell (slippage subtracts); reducing a short is a buy (it adds). A margin +// call is a MARKET execution at the print it fired at, so an off-ladder print +// (a half-cent low) books its nearest tick, as every TradingView market fill +// at a print does -- 12.105 books 12.11, 9.815 books 9.81 (lab tv tapes +// tests/fixtures/margin_entry_bar/pm-m10-f-k4-1007, -gapup-0501, -gapup-0424 +// and, beyond margin calls, pm2-round-f-print; R5 lane PAR-MARGIN-2). A print +// on the ladder is its own tick (source_bar_fill_tick is idempotent there). double PineExecutionAdapter::source_margin_fill_price(double fire, bool close_is_buy) const { - if (config_.slippage == 0) return fire; + if (config_.slippage == 0) return source_bar_fill_tick(fire, staged_.syminfo.mintick); const double rounded = source_bar_fill_tick(fire, staged_.syminfo.mintick); const double slipped = rounded + (close_is_buy ? 1.0 : -1.0) * config_.slippage * staged_.syminfo.mintick; diff --git a/tests/fixtures/half_tick_rounding/README.md b/tests/fixtures/half_tick_rounding/README.md new file mode 100644 index 00000000..6c0d286b --- /dev/null +++ b/tests/fixtures/half_tick_rounding/README.md @@ -0,0 +1,38 @@ +# Half-tick fills (R5 lane PAR-MARGIN-2, item 2) + +A margin call at an exact half-cent low booked a tick below TradingView (lane +PAR-MARGIN's M10 tapes `../margin_entry_bar/pm-m10-f-k4-1007` and +`-gapup-0501`: 12.105 booked 12.10 where TradingView books 12.11, 10.025 booked +10.02 against 10.03). Which rounding does TradingView apply, and to which fill? +These three `lab tv` exports (ws-report-v1, rangeProof covered; the lane's own +scripts) answer it on two symbols with different mintick. Every order is +placed at a signal close and closed 2 bars later. + +- `pm2-round-f` (NYSE:F, mintick 0.01) and `pm2-round-es` (CME_MINI:ES1!, + mintick 0.25): a buy stop, a sell stop, a buy limit and a sell limit, each at + a HALF-TICK LEVEL (a literal) strictly inside the next bar's range on its + crossing side, never gapped. The F levels are chosen so the nearest tick of + the stored double and the order's directional tick differ; on ES they are + exact binary halves. TradingView books the DIRECTIONAL tick, 8 of 8: a stop + its adverse tick, a limit its favourable one -- no nearest rule, exact or + floating, and no half-even rule fits all eight. +- `pm2-round-f-print` (NYSE:F): a market buy, a market sell and a sell stop + already through the open, each filled at a half-cent OPEN print whose nearest + tick differs from the order's adverse one. TradingView books the print's + NEAREST tick, 3 of 3. + +TradingView's margin call is a MARKET execution at the print it fired at (its +raw report files the 1007 call as `"tp": "MARKET"`, `"p": 12.11`), so it +takes the print rule; the adapter's pre-open slice took the stop-level rule. +Since the lane it books the print's nearest tick (`source_margin_fill_price`), +and the adapter books all three tapes' rows before and after -- the shared +`directional_tick` helper is TradingView's for every stop and limit and is not +changed. `bars.inc` holds the bars around each order from the lab feeds (NYSE:F +`80f404ae85ef`, CME_MINI:ES1! `766e8149d7e1`); replayed by +`tests/test_adapter_margin_schedule_differential.cpp` ("half-tick fills"). + +| probe | window | entries TradingView books (signal id: price) | tvTradesCsvHash | +|---|---|---|---| +| `pm2-round-f` | NYSE:F 2025-09-29 .. 2025-10-10 | BS: 12.33, SS: 12.2, SL: 12.17, BL: 12.27 | `e8377a83d2be83c4` | +| `pm2-round-es` | CME_MINI:ES1! 2025-10-05 .. 2025-10-10 | BS: 6785.75, SS: 6786.5, BL: 6785.25, SL: 6791.75 | `9774fca0167ddff8` | +| `pm2-round-f-print` | NYSE:F 2025-09-08 .. 2025-10-31 | MS: 11.7, GS: 11.76, MB: 13.91 | `a144cc0c22eafe0d` | diff --git a/tests/fixtures/half_tick_rounding/bars.inc b/tests/fixtures/half_tick_rounding/bars.inc new file mode 100644 index 00000000..77cd3a8f --- /dev/null +++ b/tests/fixtures/half_tick_rounding/bars.inc @@ -0,0 +1,61 @@ +// R5 lane PAR-MARGIN-2 item 2: 15m bars around each order of the half-tick probes, from the lab +// feeds (NYSE:F 80f404ae85ef..., CME_MINI:ES1! 766e8149d7e1...): the signal bar .. +4 per order. +// {timestamp_ms, open, high, low, close} +constexpr FeedBar kRound_f[20] = { // pm2-round-f: 4 orders, feed 80f404ae85ef + {1759411800000LL, 12.32, 12.425, 12.27, 12.31}, + {1759412700000LL, 12.305, 12.41, 12.305, 12.375}, + {1759413600000LL, 12.37, 12.38, 12.25, 12.26}, + {1759414500000LL, 12.26, 12.285, 12.245, 12.285}, + {1759415400000LL, 12.28, 12.295, 12.265, 12.265}, + {1759417200000LL, 12.235, 12.26, 12.22, 12.23}, + {1759418100000LL, 12.23, 12.23, 12.185, 12.195}, + {1759419000000LL, 12.195, 12.2, 12.155, 12.2}, + {1759419900000LL, 12.2, 12.21, 12.16, 12.175}, + {1759420800000LL, 12.175, 12.18, 12.15, 12.155}, + {1759422600000LL, 12.16, 12.165, 12.14, 12.145}, + {1759423500000LL, 12.14, 12.19, 12.125, 12.175}, + {1759424400000LL, 12.18, 12.215, 12.17, 12.21}, + {1759425300000LL, 12.2, 12.205, 12.17, 12.195}, + {1759426200000LL, 12.195, 12.205, 12.185, 12.19}, + {1759499100000LL, 12.27, 12.31, 12.25, 12.31}, + {1759500000000LL, 12.31, 12.32, 12.255, 12.29}, + {1759500900000LL, 12.29, 12.31, 12.26, 12.275}, + {1759501800000LL, 12.275, 12.295, 12.275, 12.285}, + {1759502700000LL, 12.285, 12.31, 12.285, 12.295}, +}; +constexpr FeedBar kRound_f_print[15] = { // pm2-round-f-print: 3 orders, feed 80f404ae85ef + {1757615400000LL, 11.72, 11.725, 11.69, 11.695}, + {1757616300000LL, 11.695, 11.715, 11.695, 11.715}, + {1757617200000LL, 11.715, 11.74, 11.715, 11.72}, + {1757618100000LL, 11.725, 11.735, 11.725, 11.73}, + {1757619000000LL, 11.73, 11.745, 11.72, 11.745}, + {1757684700000LL, 11.745, 11.78, 11.725, 11.755}, + {1757685600000LL, 11.755, 11.755, 11.715, 11.735}, + {1757686500000LL, 11.74, 11.74, 11.7, 11.705}, + {1757687400000LL, 11.705, 11.71, 11.67, 11.675}, + {1757688300000LL, 11.675, 11.685, 11.67, 11.675}, + {1761334200000LL, 13.95, 13.95, 13.86, 13.915}, + {1761335100000LL, 13.915, 13.915, 13.8, 13.835}, + {1761571800000LL, 13.78, 13.78, 13.37, 13.505}, + {1761572700000LL, 13.505, 13.635, 13.45, 13.63}, + {1761573600000LL, 13.625, 13.65, 13.555, 13.595}, +}; +constexpr FeedBar kRound_es[17] = { // pm2-round-es: 4 orders, feed 766e8149d7e1 + {1759828500000LL, 6785.25, 6786.25, 6784.25, 6785.25}, + {1759829400000LL, 6785.25, 6786.25, 6784.75, 6785}, + {1759830300000LL, 6784.75, 6786.75, 6784.75, 6786.25}, + {1759831200000LL, 6786.25, 6787.5, 6784.25, 6787.5}, + {1759832100000LL, 6787.5, 6787.75, 6785.75, 6787}, + {1759833000000LL, 6787.25, 6789.5, 6786.25, 6786.75}, + {1759833900000LL, 6786.75, 6787.75, 6785.25, 6785.5}, + {1759834800000LL, 6785.5, 6787, 6785.25, 6786.5}, + {1759835700000LL, 6786.75, 6786.75, 6784.75, 6786}, + {1759836600000LL, 6786.25, 6787, 6785, 6785.75}, + {1759837500000LL, 6785.75, 6791.5, 6785.5, 6791.5}, + {1759838400000LL, 6791.5, 6792, 6788.5, 6789.75}, + {1759839300000LL, 6789.5, 6791.25, 6789.25, 6791.25}, + {1759840200000LL, 6791, 6794.25, 6790.5, 6794}, + {1759841100000LL, 6794, 6799.5, 6792.75, 6798.75}, + {1759842000000LL, 6799, 6802.75, 6797.25, 6797.75}, + {1759842900000LL, 6797.75, 6798.5, 6796.25, 6797}, +}; diff --git a/tests/fixtures/half_tick_rounding/pm2-round-es/meta.json b/tests/fixtures/half_tick_rounding/pm2-round-es/meta.json new file mode 100644 index 00000000..4c7b7a6c --- /dev/null +++ b/tests/fixtures/half_tick_rounding/pm2-round-es/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-round-es", + "slug": "pm2-round-es", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:09:53.413Z", + "chart": { + "symbol": "CME_MINI:ES1!", + "interval": "15", + "from": "2025-10-05", + "to": "2025-10-10" + }, + "pine_sha256": "e727be14dc2888fab447251ca85bf0c77d58229aa5d1d9245441b16a2c7334df", + "tv_trades_sha256": "9774fca0167ddff8804630438639d79e2010f21a3d4b05fb2fea6bf4ac874366", + "tv_trades_rows": 8 +} diff --git a/tests/fixtures/half_tick_rounding/pm2-round-es/metrics.json b/tests/fixtures/half_tick_rounding/pm2-round-es/metrics.json new file mode 100644 index 00000000..6959ccd6 --- /dev/null +++ b/tests/fixtures/half_tick_rounding/pm2-round-es/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-round-es", + "symbol": "CME_MINI:ES1!", + "interval": "15", + "from": "2025-10-05", + "to": "2025-10-10", + "deepBacktesting": true, + "trades": 4, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "e727be14dc2888fab447251ca85bf0c77d58229aa5d1d9245441b16a2c7334df", + "tvTradesCsvHash": "9774fca0167ddff8804630438639d79e2010f21a3d4b05fb2fea6bf4ac874366", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.76129", + "symbol": "CME_MINI:ES1!", + "interval": "15", + "requestedRange": { + "from": 1759622400000, + "to": 1760054400000 + }, + "returnedRange": { + "from": 1759701600000, + "to": 1760054400000 + }, + "rangeProof": "covered", + "rawFrameSha256": "0b96018167d665b27dda33b588fbb4bdf8628471bc428b090a2407606cf3c3ae", + "decodedReportSha256": "ef34deb260cf2546bbc9fbdec6c0cb94d31910652910075a5d43a28d83a6a0b4", + "closedTrades": 4, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:09:53.412Z" +} diff --git a/tests/fixtures/half_tick_rounding/pm2-round-es/strategy.pine b/tests/fixtures/half_tick_rounding/pm2-round-es/strategy.pine new file mode 100644 index 00000000..8c4cba40 --- /dev/null +++ b/tests/fixtures/half_tick_rounding/pm2-round-es/strategy.pine @@ -0,0 +1,23 @@ +// R5 lane PAR-MARGIN-2 item 2: how TradingView fills a stop or a limit whose level is a HALF TICK +// (CME_MINI:ES1!, mintick 0.25). Each order is placed on a signal close, not marketable there, at a half-tick +// level strictly inside the next bar's range on its crossing side (never gapped), and closed 2 bars later. +//@version=6 +strategy("pm2-round-es", overlay=true, initial_capital=1000000, + default_qty_type=strategy.fixed, default_qty_value=1, pyramiding=1, + commission_value=0, slippage=0, process_orders_on_close=false) +if time == timestamp("UTC", 2025, 10, 7, 9, 15) and strategy.position_size == 0 + strategy.entry("BS", strategy.long, stop = 6785.625) +if strategy.position_size != 0 and strategy.opentrades.entry_id(0) == "BS" and bar_index - strategy.opentrades.entry_bar_index(0) >= 2 + strategy.close("BS", comment = "t") +if time == timestamp("UTC", 2025, 10, 7, 10, 15) and strategy.position_size == 0 + strategy.entry("SS", strategy.short, stop = 6786.625) +if strategy.position_size != 0 and strategy.opentrades.entry_id(0) == "SS" and bar_index - strategy.opentrades.entry_bar_index(0) >= 2 + strategy.close("SS", comment = "t") +if time == timestamp("UTC", 2025, 10, 7, 11, 15) and strategy.position_size == 0 + strategy.entry("BL", strategy.long, limit = 6785.375) +if strategy.position_size != 0 and strategy.opentrades.entry_id(0) == "BL" and bar_index - strategy.opentrades.entry_bar_index(0) >= 2 + strategy.close("BL", comment = "t") +if time == timestamp("UTC", 2025, 10, 7, 12, 15) and strategy.position_size == 0 + strategy.entry("SL", strategy.short, limit = 6791.625) +if strategy.position_size != 0 and strategy.opentrades.entry_id(0) == "SL" and bar_index - strategy.opentrades.entry_bar_index(0) >= 2 + strategy.close("SL", comment = "t") diff --git a/tests/fixtures/half_tick_rounding/pm2-round-es/tv_trades.csv b/tests/fixtures/half_tick_rounding/pm2-round-es/tv_trades.csv new file mode 100644 index 00000000..6b2639ae --- /dev/null +++ b/tests/fixtures/half_tick_rounding/pm2-round-es/tv_trades.csv @@ -0,0 +1,9 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-10-07 18:15,t,6787.5,1,339287.5,87.5,0.03,0,87.5,0.03,-75,-0.02,87.5,0.01,3 +1,Entry long,2025-10-07 17:30,BS,6785.75,1,339287.5,87.5,0.03,0,87.5,0.03,-75,-0.02,87.5,0.01,3 +2,Exit short,2025-10-07 19:15,t,6786.75,1,339325,-12.5,0.00,0,62.5,0.02,-150,-0.04,75,0.01,3 +2,Entry short,2025-10-07 18:30,SS,6786.5,1,339325,-12.5,0.00,0,62.5,0.02,-150,-0.04,75,0.01,3 +3,Exit long,2025-10-07 20:15,t,6789.5,1,339262.5,212.5,0.06,0,337.5,0.10,-12.5,0.00,287.5,0.03,3 +3,Entry long,2025-10-07 19:30,BL,6785.25,1,339262.5,212.5,0.06,0,337.5,0.10,-12.5,0.00,287.5,0.03,3 +4,Exit short,2025-10-07 21:15,t,6797.75,1,339587.5,-300,-0.09,0,0,0.00,-550,-0.16,-12.5,0.00,3 +4,Entry short,2025-10-07 20:30,SL,6791.75,1,339587.5,-300,-0.09,0,0,0.00,-550,-0.16,-12.5,0.00,3 \ No newline at end of file diff --git a/tests/fixtures/half_tick_rounding/pm2-round-f-print/meta.json b/tests/fixtures/half_tick_rounding/pm2-round-f-print/meta.json new file mode 100644 index 00000000..527554da --- /dev/null +++ b/tests/fixtures/half_tick_rounding/pm2-round-f-print/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-round-f-print", + "slug": "pm2-round-f-print", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:12:20.330Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-09-08", + "to": "2025-10-31" + }, + "pine_sha256": "25c0c41f0ffb537161c072fa6a728f65d9ae64b491394ad484ccef58fa984a48", + "tv_trades_sha256": "a144cc0c22eafe0d7335c704d1de8c0148ca4b1fb0ae4a8b125e328c0dd4caba", + "tv_trades_rows": 6 +} diff --git a/tests/fixtures/half_tick_rounding/pm2-round-f-print/metrics.json b/tests/fixtures/half_tick_rounding/pm2-round-f-print/metrics.json new file mode 100644 index 00000000..1aa335b6 --- /dev/null +++ b/tests/fixtures/half_tick_rounding/pm2-round-f-print/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-round-f-print", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-09-08", + "to": "2025-10-31", + "deepBacktesting": true, + "trades": 3, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "25c0c41f0ffb537161c072fa6a728f65d9ae64b491394ad484ccef58fa984a48", + "tvTradesCsvHash": "a144cc0c22eafe0d7335c704d1de8c0148ca4b1fb0ae4a8b125e328c0dd4caba", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.76248", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1757289600000, + "to": 1761868800000 + }, + "returnedRange": { + "from": 1757338200000, + "to": 1761853500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "cc049e53338ab79a2f9b7c457ffd7f7db82640b7ef1c3e7eb8052017cba465a9", + "decodedReportSha256": "3004787c8172b851225066ba7cbf436e41a6d2e47a06b59ae229e142e947e888", + "closedTrades": 3, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:12:20.329Z" +} diff --git a/tests/fixtures/half_tick_rounding/pm2-round-f-print/strategy.pine b/tests/fixtures/half_tick_rounding/pm2-round-f-print/strategy.pine new file mode 100644 index 00000000..c67614fc --- /dev/null +++ b/tests/fixtures/half_tick_rounding/pm2-round-f-print/strategy.pine @@ -0,0 +1,19 @@ +// R5 lane PAR-MARGIN-2 item 2: how TradingView fills an order at a sub-tick PRINT (NYSE:F, mintick 0.01, +// half-cent opens): a market order, and a stop already marketable when placed, each filled at the next +// bar's half-cent open, closed 2 bars later. +//@version=6 +strategy("pm2-round-f-print", overlay=true, initial_capital=1000000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=1, + commission_value=0, slippage=0, process_orders_on_close=false) +if time == timestamp("UTC", 2025, 9, 11, 18, 30) and strategy.position_size == 0 + strategy.entry("MS", strategy.short) +if strategy.position_size != 0 and strategy.opentrades.entry_id(0) == "MS" and bar_index - strategy.opentrades.entry_bar_index(0) >= 2 + strategy.close("MS", comment = "t") +if time == timestamp("UTC", 2025, 9, 12, 13, 45) and strategy.position_size == 0 + strategy.entry("GS", strategy.short, stop = close + 0.10) +if strategy.position_size != 0 and strategy.opentrades.entry_id(0) == "GS" and bar_index - strategy.opentrades.entry_bar_index(0) >= 2 + strategy.close("GS", comment = "t") +if time == timestamp("UTC", 2025, 10, 24, 19, 30) and strategy.position_size == 0 + strategy.entry("MB", strategy.long) +if strategy.position_size != 0 and strategy.opentrades.entry_id(0) == "MB" and bar_index - strategy.opentrades.entry_bar_index(0) >= 2 + strategy.close("MB", comment = "t") diff --git a/tests/fixtures/half_tick_rounding/pm2-round-f-print/tv_trades.csv b/tests/fixtures/half_tick_rounding/pm2-round-f-print/tv_trades.csv new file mode 100644 index 00000000..81fcf17c --- /dev/null +++ b/tests/fixtures/half_tick_rounding/pm2-round-f-print/tv_trades.csv @@ -0,0 +1,7 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit short,2025-09-12 03:30,t,11.73,100,1170,-3,-0.26,0,0,0.00,-4,-0.34,-3,0.00,3 +1,Entry short,2025-09-12 02:45,MS,11.7,100,1170,-3,-0.26,0,0,0.00,-4,-0.34,-3,0.00,3 +2,Exit short,2025-09-12 22:45,t,11.68,100,1176,8,0.68,0,9,0.77,0,0.00,5,0.00,3 +2,Entry short,2025-09-12 22:00,GS,11.76,100,1176,8,0.68,0,9,0.77,0,0.00,5,0.00,3 +3,Exit long,2025-10-27 22:00,t,13.63,100,1391,-28,-2.01,0,0,0.00,-54,-3.88,-23,0.00,3 +3,Entry long,2025-10-25 03:45,MB,13.91,100,1391,-28,-2.01,0,0,0.00,-54,-3.88,-23,0.00,3 \ No newline at end of file diff --git a/tests/fixtures/half_tick_rounding/pm2-round-f/meta.json b/tests/fixtures/half_tick_rounding/pm2-round-f/meta.json new file mode 100644 index 00000000..66b300d6 --- /dev/null +++ b/tests/fixtures/half_tick_rounding/pm2-round-f/meta.json @@ -0,0 +1,16 @@ +{ + "title": "pm2-round-f", + "slug": "pm2-round-f", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T01:11:14.496Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-09-29", + "to": "2025-10-10" + }, + "pine_sha256": "ad92f7cceacc7cac1f63cf6f4870d0f6a74386852a7a512f62d2010e4308c75c", + "tv_trades_sha256": "e8377a83d2be83c41386e8892d9f33e1cd80c9d821a83e3cb346e2c1323b2cd4", + "tv_trades_rows": 8 +} diff --git a/tests/fixtures/half_tick_rounding/pm2-round-f/metrics.json b/tests/fixtures/half_tick_rounding/pm2-round-f/metrics.json new file mode 100644 index 00000000..d3b3df27 --- /dev/null +++ b/tests/fixtures/half_tick_rounding/pm2-round-f/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pm2-round-f", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-09-29", + "to": "2025-10-10", + "deepBacktesting": true, + "trades": 4, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "ad92f7cceacc7cac1f63cf6f4870d0f6a74386852a7a512f62d2010e4308c75c", + "tvTradesCsvHash": "e8377a83d2be83c41386e8892d9f33e1cd80c9d821a83e3cb346e2c1323b2cd4", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.76195", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1759104000000, + "to": 1760054400000 + }, + "returnedRange": { + "from": 1759152600000, + "to": 1760039100000 + }, + "rangeProof": "covered", + "rawFrameSha256": "907edd0658284044261751f85f43eca4e5a06aadd64b11b18ba574bc4f642ee6", + "decodedReportSha256": "e27d4313e91456a63763d84675ae4b7a7a653c2c9a0a75e370531c38638209ee", + "closedTrades": 4, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T01:11:14.495Z" +} diff --git a/tests/fixtures/half_tick_rounding/pm2-round-f/strategy.pine b/tests/fixtures/half_tick_rounding/pm2-round-f/strategy.pine new file mode 100644 index 00000000..03e23933 --- /dev/null +++ b/tests/fixtures/half_tick_rounding/pm2-round-f/strategy.pine @@ -0,0 +1,23 @@ +// R5 lane PAR-MARGIN-2 item 2: how TradingView fills a stop or a limit whose level is a HALF TICK +// (NYSE:F, mintick 0.01). Each order is placed on a signal close, not marketable there, at a half-tick +// level strictly inside the next bar's range on its crossing side (never gapped), and closed 2 bars later. +//@version=6 +strategy("pm2-round-f", overlay=true, initial_capital=1000000, + default_qty_type=strategy.fixed, default_qty_value=100, pyramiding=1, + commission_value=0, slippage=0, process_orders_on_close=false) +if time == timestamp("UTC", 2025, 10, 2, 13, 30) and strategy.position_size == 0 + strategy.entry("BS", strategy.long, stop = 12.325) +if strategy.position_size != 0 and strategy.opentrades.entry_id(0) == "BS" and bar_index - strategy.opentrades.entry_bar_index(0) >= 2 + strategy.close("BS", comment = "t") +if time == timestamp("UTC", 2025, 10, 2, 15, 0) and strategy.position_size == 0 + strategy.entry("SS", strategy.short, stop = 12.205) +if strategy.position_size != 0 and strategy.opentrades.entry_id(0) == "SS" and bar_index - strategy.opentrades.entry_bar_index(0) >= 2 + strategy.close("SS", comment = "t") +if time == timestamp("UTC", 2025, 10, 2, 16, 30) and strategy.position_size == 0 + strategy.entry("SL", strategy.short, limit = 12.165) +if strategy.position_size != 0 and strategy.opentrades.entry_id(0) == "SL" and bar_index - strategy.opentrades.entry_bar_index(0) >= 2 + strategy.close("SL", comment = "t") +if time == timestamp("UTC", 2025, 10, 3, 13, 45) and strategy.position_size == 0 + strategy.entry("BL", strategy.long, limit = 12.275) +if strategy.position_size != 0 and strategy.opentrades.entry_id(0) == "BL" and bar_index - strategy.opentrades.entry_bar_index(0) >= 2 + strategy.close("BL", comment = "t") diff --git a/tests/fixtures/half_tick_rounding/pm2-round-f/tv_trades.csv b/tests/fixtures/half_tick_rounding/pm2-round-f/tv_trades.csv new file mode 100644 index 00000000..80e23563 --- /dev/null +++ b/tests/fixtures/half_tick_rounding/pm2-round-f/tv_trades.csv @@ -0,0 +1,9 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-10-02 22:30,t,12.28,100,1233,-5,-0.41,0,8,0.65,-8,-0.65,-5,0.00,3 +1,Entry long,2025-10-02 21:45,BS,12.33,100,1233,-5,-0.41,0,8,0.65,-8,-0.65,-5,0.00,3 +2,Exit short,2025-10-03 00:00,t,12.18,100,1220,2,0.16,0,4,0.33,-1,-0.08,-3,0.00,3 +2,Entry short,2025-10-02 23:15,SS,12.2,100,1220,2,0.16,0,4,0.33,-1,-0.08,-3,0.00,3 +3,Exit short,2025-10-03 01:30,t,12.2,100,1217,-3,-0.25,0,0,0.00,-5,-0.41,-6,0.00,3 +3,Entry short,2025-10-03 00:45,SL,12.17,100,1217,-3,-0.25,0,0,0.00,-5,-0.41,-6,0.00,3 +4,Exit long,2025-10-03 22:45,t,12.29,100,1227,2,0.16,0,4,0.33,-1,-0.08,-4,0.00,3 +4,Entry long,2025-10-03 22:00,BL,12.27,100,1227,2,0.16,0,4,0.33,-1,-0.08,-4,0.00,3 \ No newline at end of file diff --git a/tests/test_adapter_margin_schedule_differential.cpp b/tests/test_adapter_margin_schedule_differential.cpp index f50bd64b..8c23256e 100644 --- a/tests/test_adapter_margin_schedule_differential.cpp +++ b/tests/test_adapter_margin_schedule_differential.cpp @@ -72,6 +72,9 @@ * TradingView intrabars: its own request.security_lower_tf "2" arrays * equal the feed's minute pairs owned by the chart bar of their last * minute; + * half-tick fills: a stop or limit at a half-tick level books the + * directional tick, a fill at a half-cent print books its nearest tick, + * on NYSE:F and CME_MINI:ES1! -- the margin call is the second kind. * * Source-bound (includes pineforge/source): release profile only. */ @@ -112,6 +115,7 @@ struct FeedBar { #include "fixtures/intrabar_margin/bars_1m.inc" #include "fixtures/margin_entry_bar/pm2_bars.inc" #include "fixtures/intrabar_margin/pm2_bars_1m.inc" +#include "fixtures/half_tick_rounding/bars.inc" // ------------------------------------------------------------ TV policy // The twin's TradingView money and slice (test_native_margin_hooks_twin.cpp, @@ -1055,19 +1059,19 @@ void m7_tradingview_tapes() { // fill, and so did the adapter until R5 lane PAR-MARGIN: 0 of 8 books (3481, // 4385, 3200, 3358; 3842, 3430, 3947, 3728), with margin calls sized to match // or fired where TradingView fired none. It now freezes above 100 % too and -// books every tape's rows: the same bars, quantities and signals, and the -// same prices but two. -// The two (recorded, open): a margin call at an exact half-cent low -- 1007's -// 12.105, 0501's 10.025 -- books a tick below TradingView (12.10 and 10.02 -// against 12.11 and 10.03). Both decimals are stored a hair above the half -// cent, and TradingView rounds the stored value to its nearest tick; 0424's -// 9.815, stored a hair below, books 9.81 on both sides. +// books every tape's rows: the same bars, quantities, signals and prices. +// Two of the prices came with R5 lane PAR-MARGIN-2: a margin call at an +// exact half-cent low -- 1007's 12.105, 0501's 10.025 -- booked a tick below +// TradingView (12.10 and 10.02 against 12.11 and 10.03). TradingView's +// margin call is a MARKET execution at that print and books the print's +// nearest tick (its raw report: 12.11); the pre-open slice took the stop +// level's adverse tick instead. 0424's 9.815, stored a hair below the half +// cent, books 9.81 either way. The prices are compared exactly now. struct M10Tape { const char* slug; const FeedBar* bars; double percent; bool marketable; - int half_cent_row; // the row booked a tick below the tape's; -1: none }; double tape_entry_price(const char* slug) { @@ -1086,14 +1090,14 @@ double tape_entry_price(const char* slug) { void m10_tradingview_tapes() { const M10Tape tapes[] = { - {"pm-m10-f-k4-0402", kM10_k4_0402, 400.0, true, -1}, - {"pm-m10-f-k4-0410", kM10_k4_0410, 400.0, true, -1}, - {"pm-m10-f-k4-1007", kM10_k4_1007, 400.0, true, 0}, - {"pm-m10-f-k4-0309", kM10_k4_0309, 400.0, true, -1}, - {"pm-m10-f-gapup-0501", kM10_gapup_0501, 390.0, false, 0}, - {"pm-m10-f-gapup-0331", kM10_gapup_0331, 390.0, false, -1}, - {"pm-m10-f-gapup-0424", kM10_gapup_0424, 390.0, false, -1}, - {"pm-m10-f-gapup-0527", kM10_gapup_0527, 390.0, false, -1}, + {"pm-m10-f-k4-0402", kM10_k4_0402, 400.0, true}, + {"pm-m10-f-k4-0410", kM10_k4_0410, 400.0, true}, + {"pm-m10-f-k4-1007", kM10_k4_1007, 400.0, true}, + {"pm-m10-f-k4-0309", kM10_k4_0309, 400.0, true}, + {"pm-m10-f-gapup-0501", kM10_gapup_0501, 390.0, false}, + {"pm-m10-f-gapup-0331", kM10_gapup_0331, 390.0, false}, + {"pm-m10-f-gapup-0424", kM10_gapup_0424, 390.0, false}, + {"pm-m10-f-gapup-0527", kM10_gapup_0527, 390.0, false}, }; for (const M10Tape& tape : tapes) { std::printf("-- M10 tape %s (%.0f %%, a stop %s when placed)\n", tape.slug, tape.percent, @@ -1142,12 +1146,7 @@ void m10_tradingview_tapes() { CHECK(adapter[i].bar == tv[i].bar); CHECK(std::abs(adapter[i].qty - tv[i].qty) <= 5e-5); CHECK((tv[i].signal == "Margin call") == (adapter[i].signal == "Margin call")); - if (static_cast(i) == tape.half_cent_row) { - // Recorded: a tick below the tape's price. - CHECK(std::abs(adapter[i].price - (tv[i].price - 0.01)) <= 5e-3); - } else { - CHECK(std::abs(adapter[i].price - tv[i].price) <= 5e-3); - } + CHECK(std::abs(adapter[i].price - tv[i].price) <= 1e-9); } } } @@ -1834,6 +1833,53 @@ void tradingview_intrabars() { } } +// ---- item 2: the half tick (tests/fixtures/half_tick_rounding) +// A margin call at an exact half-cent low booked a tick below TradingView +// (12.105 -> 12.10 where TradingView books 12.11; 10.025 -> 10.02 against +// 10.03) because the pre-open slice's fill took the stop-level rule +// (directional_tick, adverse) while TradingView's margin call is a MARKET +// execution at the print, which books the print's nearest tick. The two +// rules are both TradingView's, each for its own fill: +// pm2-round-f / -es a stop or limit crossed at a half-tick LEVEL books +// the directional tick (stop adverse, limit +// favourable), on NYSE:F (mintick 0.01) and +// CME_MINI:ES1! (mintick 0.25), 8 of 8 -- as the +// adapter always did; +// pm2-round-f-print a market order, and a stop already through the open, +// filled at a half-cent PRINT books its nearest tick, +// 3 of 3 -- as the adapter always did too. +// So the helper stays; the margin slice now takes the print rule +// (source_margin_fill_price), and the M10 tapes above book 12.11 and 10.03. +struct RoundTape { + const char* slug; + const FeedBar* bars; + int n; + double mintick; +}; + +void half_tick_fills_on_tapes() { + const std::string dir = std::string(PINEFORGE_HM_M7A_FIXTURE_DIR) + "/../half_tick_rounding"; + const RoundTape tapes[] = { + {"pm2-round-f", kRound_f, static_cast(sizeof kRound_f / sizeof kRound_f[0]), 0.01}, + {"pm2-round-es", kRound_es, static_cast(sizeof kRound_es / sizeof kRound_es[0]), 0.25}, + {"pm2-round-f-print", kRound_f_print, + static_cast(sizeof kRound_f_print / sizeof kRound_f_print[0]), 0.01}, + }; + for (const RoundTape& tape : tapes) { + std::printf("-- half-tick fills %s\n", tape.slug); + const std::string at = dir + "/" + tape.slug; + const ProbeScript ps = parse_probe(at + "/strategy.pine"); + REQUIRE(ps.entries.size() >= 3); + const auto adapter = run_probe(ps, feed_bars(tape.bars, tape.n), false, 0.0, tape.mintick); + const auto tv = tape_exit_rows(at); + print_exits("tape", tv); + print_exits("adapter", adapter); + // Every trade, its entry (where the half tick is) and its exit. + CHECK(adapter.size() == ps.entries.size()); + CHECK(same_exits(adapter, tv)); + } +} + } // namespace int main() { @@ -1855,6 +1901,7 @@ int main() { test("PAR-MARGIN-2 M7 shapes", m7_shapes_on_tapes); test("PAR-MARGIN-2 intrabar shapes", intrabar_shapes_on_tapes); test("PAR-MARGIN-2 TradingView intrabars", tradingview_intrabars); + test("PAR-MARGIN-2 half-tick fills", half_tick_fills_on_tapes); std::printf("%d checks, %d failures\n", checks, failures); return failures == 0 ? 0 : 1; } diff --git a/tests/test_native_margin_hooks_twin.cpp b/tests/test_native_margin_hooks_twin.cpp index cb1ee9c1..6cf12e7f 100644 --- a/tests/test_native_margin_hooks_twin.cpp +++ b/tests/test_native_margin_hooks_twin.cpp @@ -60,6 +60,17 @@ double tv_nearest_tick(double value, double tick) { const double k = std::floor(value / tick + 0.5); return k * tick == value ? value : k * tick; } +// source_bar_fill_tick (pine_adapter.cpp): a print on the ladder is its own +// tick, any other books its nearest one as the decimal quotient k / (1 / tick). +double tv_bar_fill_tick(double value, double tick) { + if (!std::isfinite(value) || !(tick > 0.0)) return value; + const double k = std::floor(value / tick + 0.5); + if (k * tick == value) return value; + const double inverse = 1.0 / tick; + const double n = std::floor(inverse + 0.5); + if (n > 0.0 && std::abs(inverse - n) <= 1e-6 * n) return k / n; + return k * tick; +} double tv_directional_tick(double value, double tick, bool upward) { if (!std::isfinite(value) || !(tick > 0.0)) return value; const double scaled = value / tick; @@ -216,7 +227,10 @@ struct TvNativeHost final : Host { // as the adapter builds its forced price from its own mark_price. const double fire = facts.trigger_level ? *facts.trigger_level : facts.raw_price; if (config.slippage == 0) { - return {fire, std::nullopt, no::OpeningShape::Transact}; + // A margin call is a MARKET execution at its print: the print's + // nearest tick (R5 lane PAR-MARGIN-2), the identity on the ladder. + return {tv_bar_fill_tick(fire, config.mintick), std::nullopt, + no::OpeningShape::Transact}; } const bool close_is_buy = facts.position.signed_units < 0.0; const double rounded = tv_nearest_tick(fire, config.mintick); @@ -496,7 +510,10 @@ void twin_rounded_money_and_whole_drop() { const auto rows = run_twin("MG-B/G", config, bars, &plain); REQUIRE(rows.size() == 1); CHECK(rows[0].units == 1.0); - CHECK(rows[0].price == 99.999999996); + // Booked at the print's nearest tick: TradingView's margin call is a + // market execution at the print it fired at (R5 lane PAR-MARGIN-2, + // tests/fixtures/half_tick_rounding). The raw 99.999999996 until then. + CHECK(rows[0].price == 100.0); // The kernel's own money never makes this call. CHECK(plain.empty()); } From fd68f43ba9f626de951186c36439bb87059d0e60 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 09:01:56 +0800 Subject: [PATCH 46/83] tests: a timing leg under the minimum grows instead of failing the row (R5 lane CI-FLAKE) Eight of the nine timing-ratio rows calibrated their workload until one sample reached a minimum leg time, then timed their two legs afresh and failed when a timed leg came in under that minimum. A calibration stops anywhere from the minimum to twice it, and a later best of several samples can land under it: engine CI run 36194784147 failed test_native_continuation_digest_tail (macOS Debug, 0.0964 s) and test_native_bare_host_contracts (macOS Release, 0.0991 s) that way, with every engine assertion passing. tests/ratio_timing.hpp holds the sizing the nine rows now share: presized() is the calibration, and time_measurable_legs() times the legs and doubles the workload until every leg reaches the minimum. The ninth row, test_adapter_live_state_scaling, already looped that way on its small leg; its loop is the shared one now, which tests the shorter leg and may reach its cap of 512,000 bars where the old loop stopped at half of it. Every growth cap is at least four times what an Apple M4 Max needs; four rows sat at theirs (the digest tail at 128 repeats, the lookup brackets at 48,000 bars, the matcher at 192,000 bars, the journal at 8,388,608 queries). The ratio bounds, the four-to-one sizes and the best-of counts are unchanged; tests only. The matcher's comment now names its 250 ms minimum. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 4353037d565543de22e603f70505a7b39b9637d6) --- tests/ratio_timing.hpp | 49 ++++++++++++ tests/test_adapter_live_state_scaling.cpp | 23 +++--- tests/test_adapter_lookup_index_scaling.cpp | 37 +++++---- tests/test_native_bare_host_contracts.cpp | 27 ++++--- .../test_native_continuation_digest_tail.cpp | 28 ++++--- tests/test_native_handle_stable_replace.cpp | 36 +++++---- tests/test_native_intrabar_lower_lookup.cpp | 29 ++++--- tests/test_native_journal_window.cpp | 29 ++++--- tests/test_native_match_rescan_scaling.cpp | 32 +++++--- tests/test_native_state_continuation.cpp | 78 +++++++++++-------- 10 files changed, 245 insertions(+), 123 deletions(-) create mode 100644 tests/ratio_timing.hpp diff --git a/tests/ratio_timing.hpp b/tests/ratio_timing.hpp new file mode 100644 index 00000000..fade78fa --- /dev/null +++ b/tests/ratio_timing.hpp @@ -0,0 +1,49 @@ +// The sizing every timing-ratio row shares: a ratio is read only off legs long +// enough to measure (R5 lane CI-FLAKE). +// +// A ratio row times two legs of one workload -- N and 4N bars, a read early +// and late in a run, ten and forty live requests -- and bounds their quotient. +// Clock and scheduler noise decide the quotient of legs a few milliseconds +// long, so RATIO-HARDEN gave every row a minimum leg time and a calibration +// that grows the workload (repeats of a fixed run, its bars, its queries or +// its reads) until a sample reaches the minimum. Eight of the nine rows then +// timed their legs afresh and failed when one came in under the minimum, +// which it can: a calibration stops anywhere from the minimum to twice it, +// and a later best of several samples can come in under the one that crossed +// it. Engine CI run 36194784147 timed 0.0964 s (macOS Debug, +// test_native_continuation_digest_tail) and 0.0991 s (macOS Release, +// test_native_bare_host_contracts) at sizes a sample of at least 0.1 s had +// chosen. A short leg is not a result, and no longer a failure: the row +// doubles the workload and times its legs again, until every leg reaches the +// minimum. (The ninth, test_adapter_live_state_scaling, already timed its +// legs until the small one reached it; its loop is this one now.) A row's +// own minimum checks now fail only where its growth cap stopped the doubling +// -- a workload whose time stopped growing with its size -- and every cap is +// at least four times the size an Apple M4 Max needs, the fastest machine +// this lane timed. +#pragma once + +namespace ratio_timing { + +// Doubles `scale` from its start while one sample of the workload at that +// scale -- the row's `sample(scale)`, in seconds -- is under `min_seconds`, +// and never past `max_scale`: a cheap first size, so that the legs are +// usually timed once. +template +int presized(int scale, int max_scale, double min_seconds, Sample&& sample) { + while (scale <= max_scale / 2 && sample(scale) < min_seconds) scale *= 2; + return scale; +} + +// Times the row's legs at `scale` -- `time_legs(scale)` takes the row's own +// samples of every leg (best of N, in turn where the row alternates them) and +// returns the shortest leg's seconds -- and doubles `scale` until that leg +// reaches `min_seconds`, or `scale` reaches `max_scale`. On return `scale` is +// the size the legs were last timed at. +template +void time_measurable_legs(int& scale, int max_scale, double min_seconds, + TimeLegs&& time_legs) { + while (time_legs(scale) < min_seconds && scale <= max_scale / 2) scale *= 2; +} + +} // namespace ratio_timing diff --git a/tests/test_adapter_live_state_scaling.cpp b/tests/test_adapter_live_state_scaling.cpp index 36c49f31..5d29d068 100644 --- a/tests/test_adapter_live_state_scaling.cpp +++ b/tests/test_adapter_live_state_scaling.cpp @@ -77,7 +77,7 @@ // doubles until its best timed sample reaches kMinLegSeconds; the large leg // is four times that size, so a linear leg measures about four on any machine // while a super-linear one reaches the size sooner and still grows by its own -// power. +// power (the doubling is tests/ratio_timing.hpp's since R5 lane CI-FLAKE). // Two mutants of the product (Mac, Release): K1 holding every cancelled leg // again (V19-D step 7 undone) stopped the doubling at 4,000 bars (0.137 s) and // measured 20.25 at 4,000 / 16,000 bars, 7,999 and 31,999 rows retained; a @@ -122,9 +122,11 @@ #include #include "../src/native_execution_consumer.hpp" +#include "ratio_timing.hpp" #include +#include #include #include #include @@ -351,6 +353,7 @@ constexpr double kShapeBound = 5.0; // keeps a pathological mutant from turning a normal ctest row into an // unbounded run. constexpr double kMinLegSeconds = 0.1; +// The small leg grows to at most this many bars; an Apple M4 Max needs 128,000. constexpr int kMaxCalibratedBars = 512000; // What a run can still hold when it ends: its live requests, the rows the // current position cycle still reads, and the last bar's retirements. @@ -388,21 +391,21 @@ void cost_and_rows_are_live(Workload workload, const char* name, bool recording const std::int64_t step = workload == Workload::Straddle ? kDay : kMinute; Leg small; Leg large; - while (true) { - const std::vector small_tape = tape(bars, step); - const std::vector large_tape = tape(bars * 4, step); + const auto time_legs = [&](int count) { + const std::vector small_tape = tape(count, step); + const std::vector large_tape = tape(count * 4, step); if (short_bars) { interleaved_best(workload, small_tape, large_tape, recording, small, large); } else { small = best_of(workload, small_tape, recording); large = best_of(workload, large_tape, recording); } - if (!gated() || small.seconds >= kMinLegSeconds - || bars >= kMaxCalibratedBars / 2) { - break; - } - bars *= 2; - } + return std::min(small.seconds, large.seconds); + }; + if (gated()) + ratio_timing::time_measurable_legs(bars, kMaxCalibratedBars, kMinLegSeconds, time_legs); + else + time_legs(bars); const double ratio = small.seconds > 0.0 ? large.seconds / small.seconds : 0.0; std::printf("%s (%s): %d bars %.4fs (%d trades, %zu rows retained, %zu roster members), " "%d bars %.4fs (%d trades, %zu rows retained, %zu roster members), " diff --git a/tests/test_adapter_lookup_index_scaling.cpp b/tests/test_adapter_lookup_index_scaling.cpp index 88c683ce..f6a76092 100644 --- a/tests/test_adapter_lookup_index_scaling.cpp +++ b/tests/test_adapter_lookup_index_scaling.cpp @@ -21,13 +21,18 @@ // least 100 ms of CPU; the large leg is four times that size. The original // fixed 6,000-bar leg was only 20--35 ms on this Mac and the hosted Release // failure measured 6.01 at that size. Keeping the bound at 5 after sizing the -// legs preserves the old mutant margin (the scan is still well above 5). +// legs preserves the old mutant margin (the scan is still well above 5). A +// timed leg under the minimum doubles the bars and times both legs again (R5 +// lane CI-FLAKE, tests/ratio_timing.hpp). // What the runs produce is pinned elsewhere (test_adapter_lookup_index_ // witness) and compared bit for bit against the reference scans // (test_adapter_lookup_index_differential); here only the finished state is // checked, so a leg that failed or traded less cannot pass as cheap. #include +#include "ratio_timing.hpp" + +#include #include #include #include @@ -156,26 +161,30 @@ Leg best_of_five(Workload workload, const std::vector& bars) { } constexpr int kInitialBars = 6000; -constexpr int kMaxCalibratedBars = 96000; +// The small leg grows to at most this many bars; an Apple M4 Max needs 48,000. +constexpr int kMaxCalibratedBars = 384000; constexpr double kMinLegSeconds = 0.1; constexpr double kShapeBound = 5.0; int calibrated_bars(Workload workload) { - int bars = kInitialBars; - while (true) { - const Leg sample = best_of_five(workload, tape(bars)); - if (sample.seconds >= kMinLegSeconds || bars >= kMaxCalibratedBars / 2) - return bars; - bars *= 2; - } + return ratio_timing::presized( + kInitialBars, kMaxCalibratedBars, kMinLegSeconds, + [&](int bars) { return best_of_five(workload, tape(bars)).seconds; }); } void cost_is_linear_in_the_bars(Workload workload, const char* name) { - const int bars = gated() ? calibrated_bars(workload) : kInitialBars / 4; - const std::vector small_tape = tape(bars); - const std::vector large_tape = tape(bars * 4); - const Leg small = best_of_five(workload, small_tape); - const Leg large = best_of_five(workload, large_tape); + int bars = gated() ? calibrated_bars(workload) : kInitialBars / 4; + Leg small; + Leg large; + const auto time_legs = [&](int count) { + small = best_of_five(workload, tape(count)); + large = best_of_five(workload, tape(count * 4)); + return std::min(small.seconds, large.seconds); + }; + if (gated()) + ratio_timing::time_measurable_legs(bars, kMaxCalibratedBars, kMinLegSeconds, time_legs); + else + time_legs(bars); const double ratio = small.seconds > 0.0 ? large.seconds / small.seconds : 0.0; std::printf("%s: %d bars %.4fs (%d trades), %d bars %.4fs (%d trades), ratio %.2f " "(bound %.1f)\n", name, bars, small.seconds, small.trades, bars * 4, diff --git a/tests/test_native_bare_host_contracts.cpp b/tests/test_native_bare_host_contracts.cpp index 17020485..83558a80 100644 --- a/tests/test_native_bare_host_contracts.cpp +++ b/tests/test_native_bare_host_contracts.cpp @@ -9,6 +9,8 @@ #include #include +#include "ratio_timing.hpp" + #include #include #include @@ -537,7 +539,10 @@ void owned_excursion_is_recorded_verbatim() { // re-walks the 10 000 path bars at every read measured about 40x when this row // was written and 440-460x on the INT17 tree. RATIO-HARDEN repeats the fixed // 2,000-bar legs until the cheaper read-once leg has at least 100 ms of CPU; -// the x20 bound stays in place so the old fold mutant still fails. +// the x20 bound stays in place so the old fold mutant still fails. A timed leg +// under that minimum doubles the repeats and times both legs again (R5 lane +// CI-FLAKE, tests/ratio_timing.hpp): macOS Release CI had timed the read-once +// leg at 0.0991 s with the repeats a 0.1 s calibration sample chose. struct LowerPathReader final : NativeStrategyHost { bool read_each_bar = false; std::uint64_t last_read = 0; @@ -593,15 +598,19 @@ ReadCost lower_path_read_cost(int script_bars, bool read_each_bar, int repeats) void continuation_read_is_linear_in_the_feed() { constexpr int kScriptBars = 2000; - constexpr int kMaxRepeats = 256; + // The repeats grow to at most this many; an Apple M4 Max needs 64. + constexpr int kMaxRepeats = 1024; constexpr double kMinLegSeconds = 0.1; - int repeats = 1; - while (lower_path_read_cost(kScriptBars, false, repeats).seconds < kMinLegSeconds - && repeats < kMaxRepeats / 2) { - repeats *= 2; - } - const ReadCost every_bar = lower_path_read_cost(kScriptBars, true, repeats); - const ReadCost once = lower_path_read_cost(kScriptBars, false, repeats); + int repeats = ratio_timing::presized(1, kMaxRepeats, kMinLegSeconds, [&](int count) { + return lower_path_read_cost(kScriptBars, false, count).seconds; + }); + ReadCost every_bar; + ReadCost once; + ratio_timing::time_measurable_legs(repeats, kMaxRepeats, kMinLegSeconds, [&](int count) { + every_bar = lower_path_read_cost(kScriptBars, true, count); + once = lower_path_read_cost(kScriptBars, false, count); + return std::min(every_bar.seconds, once.seconds); + }); const double ratio = every_bar.seconds / std::max(once.seconds, 1e-6); std::printf(" %d bars x%d over a lower-timeframe path: continuation read every bar %.4f s," " read once %.4f s, ratio %.2f (bound x20)\n", kScriptBars, repeats, diff --git a/tests/test_native_continuation_digest_tail.cpp b/tests/test_native_continuation_digest_tail.cpp index 015fb1a8..92dbb55d 100644 --- a/tests/test_native_continuation_digest_tail.cpp +++ b/tests/test_native_continuation_digest_tail.cpp @@ -23,9 +23,15 @@ // is process CPU time (std::clock), which is what the A40 rev 7 runtime-budget // ruling found load-robust, and each leg is the best of three. RATIO-HARDEN // repeats the 1,500-bar workload as needed so each timed leg has at least -// 100 ms of CPU; the bar ratio and the x8 bound are unchanged. +// 100 ms of CPU; the bar ratio and the x8 bound are unchanged. A timed leg +// under that minimum doubles the repeats and times both legs again (R5 lane +// CI-FLAKE, tests/ratio_timing.hpp): macOS Debug CI had timed 0.0964 s at the +// repeats a 0.1 s calibration sample chose, and failed the row. #include +#include "ratio_timing.hpp" + +#include #include #include #include @@ -118,22 +124,24 @@ double best_of_three(int count, int repeats) { } constexpr int kBars = 1500; -constexpr int kMaxRepeats = 256; +// The repeats grow to at most this many; an Apple M4 Max needs 128. +constexpr int kMaxRepeats = 2048; constexpr double kMinLegSeconds = 0.1; constexpr double kShapeBound = 8.0; int calibrated_repeats() { - int repeats = 1; - while (best_of_three(kBars, repeats) < kMinLegSeconds - && repeats < kMaxRepeats / 2) { - repeats *= 2; - } - return repeats; + return ratio_timing::presized(1, kMaxRepeats, kMinLegSeconds, + [](int repeats) { return best_of_three(kBars, repeats); }); } void the_per_bar_read_is_linear_in_the_bar_count(int repeats) { - const double small = best_of_three(kBars, repeats); - const double large = best_of_three(kBars * 4, repeats); + double small = 0.0; + double large = 0.0; + ratio_timing::time_measurable_legs(repeats, kMaxRepeats, kMinLegSeconds, [&](int count) { + small = best_of_three(kBars, count); + large = best_of_three(kBars * 4, count); + return std::min(small, large); + }); const double ratio = small > 0.0 ? large / small : 0.0; std::printf("continuation digest cost: %d bars x%d %.4fs, %d bars x%d %.4fs, ratio %.2f " "(bound %.1f)\n", kBars, repeats, small, kBars * 4, repeats, large, ratio, diff --git a/tests/test_native_handle_stable_replace.cpp b/tests/test_native_handle_stable_replace.cpp index 08a231a0..4bec89fa 100644 --- a/tests/test_native_handle_stable_replace.cpp +++ b/tests/test_native_handle_stable_replace.cpp @@ -50,6 +50,7 @@ #include "../src/native_execution_consumer.hpp" #include "native_match_book_fixture.hpp" #include "native_order_full_fold.hpp" +#include "ratio_timing.hpp" #include #include @@ -1123,25 +1124,32 @@ double continuation_seconds(int bars, int repeats) { return seconds; } +// Each leg is repeated until it has at least 100 ms of CPU; a timed leg under +// that doubles the repeats and times both legs again (R5 lane CI-FLAKE, +// tests/ratio_timing.hpp). void scaling() { constexpr int kInitialSmall = 2000; - constexpr int kMaxRepeats = 256; + // The repeats grow to at most this many; an Apple M4 Max needs 64. + constexpr int kMaxRepeats = 1024; constexpr double kMinLegSeconds = 0.1; - int repeats = 1; - while (continuation_seconds(kInitialSmall, repeats) < kMinLegSeconds - && repeats < kMaxRepeats / 2) { - repeats *= 2; - } + int repeats = ratio_timing::presized(1, kMaxRepeats, kMinLegSeconds, [&](int count) { + return continuation_seconds(kInitialSmall, count); + }); double best_small = 1e9, best_large = 1e9; - for (int round = 0; round < 5; ++round) { - if (round % 2 == 0) { - best_small = std::min(best_small, continuation_seconds(kInitialSmall, repeats)); - best_large = std::min(best_large, continuation_seconds(4 * kInitialSmall, repeats)); - } else { - best_large = std::min(best_large, continuation_seconds(4 * kInitialSmall, repeats)); - best_small = std::min(best_small, continuation_seconds(kInitialSmall, repeats)); + ratio_timing::time_measurable_legs(repeats, kMaxRepeats, kMinLegSeconds, [&](int count) { + best_small = 1e9; + best_large = 1e9; + for (int round = 0; round < 5; ++round) { + if (round % 2 == 0) { + best_small = std::min(best_small, continuation_seconds(kInitialSmall, count)); + best_large = std::min(best_large, continuation_seconds(4 * kInitialSmall, count)); + } else { + best_large = std::min(best_large, continuation_seconds(4 * kInitialSmall, count)); + best_small = std::min(best_small, continuation_seconds(kInitialSmall, count)); + } } - } + return std::min(best_small, best_large); + }); const double ratio = best_large / best_small; CHECK(best_small >= kMinLegSeconds); CHECK(best_large >= kMinLegSeconds); diff --git a/tests/test_native_intrabar_lower_lookup.cpp b/tests/test_native_intrabar_lower_lookup.cpp index a09cd87d..a78e12a6 100644 --- a/tests/test_native_intrabar_lower_lookup.cpp +++ b/tests/test_native_intrabar_lower_lookup.cpp @@ -24,7 +24,9 @@ // quadratic one. The sample is process CPU time (std::clock), which the // runtime-budget ruling (A40 rev 7) found load-robust. RATIO-HARDEN repeats // the same 4,000- and 16,000-bar legs until the best small leg has 100 ms of -// CPU, retaining the x6 bound and the mutant margin. +// CPU, retaining the x6 bound and the mutant margin. A timed leg that still +// comes in under it doubles the repeats and times both legs again (R5 lane +// CI-FLAKE, tests/ratio_timing.hpp). // // The value half pins what the selection feeds -- the driver points, the // fills and every hash over them -- for three feeds: a lower feed that tiles @@ -41,6 +43,9 @@ // them. Rebuild them the same way; never edit one by hand to make a run pass. #include +#include "ratio_timing.hpp" + +#include #include #include #include @@ -233,23 +238,25 @@ double best_of_three(int count, int repeats) { } constexpr int kBars = 4000; -constexpr int kMaxRepeats = 256; +// The repeats grow to at most this many; an Apple M4 Max needs 64. +constexpr int kMaxRepeats = 1024; constexpr double kMinLegSeconds = 0.1; constexpr double kShapeBound = 6.0; int calibrated_repeats() { - int repeats = 1; - while (best_of_three(kBars, repeats) < kMinLegSeconds - && repeats < kMaxRepeats / 2) { - repeats *= 2; - } - return repeats; + return ratio_timing::presized(1, kMaxRepeats, kMinLegSeconds, + [](int repeats) { return best_of_three(kBars, repeats); }); } void the_lower_feed_lookup_is_linear_in_the_run() { - const int repeats = calibrated_repeats(); - const double small = best_of_three(kBars, repeats); - const double large = best_of_three(kBars * 4, repeats); + int repeats = calibrated_repeats(); + double small = 0.0; + double large = 0.0; + ratio_timing::time_measurable_legs(repeats, kMaxRepeats, kMinLegSeconds, [&](int count) { + small = best_of_three(kBars, count); + large = best_of_three(kBars * 4, count); + return std::min(small, large); + }); const double ratio = small > 0.0 ? large / small : 0.0; std::printf("lower-feed run: %d bars x%d %.4fs, %d bars x%d %.4fs, ratio %.2f " "(bound %.1f)\n", kBars, repeats, small, kBars * 4, repeats, large, diff --git a/tests/test_native_journal_window.cpp b/tests/test_native_journal_window.cpp index d2b4512e..019a2892 100644 --- a/tests/test_native_journal_window.cpp +++ b/tests/test_native_journal_window.cpp @@ -25,6 +25,8 @@ // retired event, and a successor that retained the ride answers 0. #include +#include "ratio_timing.hpp" + #include #include #include @@ -427,22 +429,29 @@ double chain_queries_seconds(int chain, int queries) { return seconds; } +// The queries double until a leg has at least 100 ms of CPU; a timed leg that +// still comes in under it doubles them and times both legs again (R5 lane +// CI-FLAKE, tests/ratio_timing.hpp). void membership_is_not_a_chain_walk() { constexpr double kMinLegSeconds = 0.1; constexpr int kInitialQueries = 2048; - constexpr int kMaxQueries = 16 * 1024 * 1024; - int queries = kInitialQueries; - while (chain_queries_seconds(400, queries) < kMinLegSeconds - && queries < kMaxQueries / 2) { - queries *= 2; - } + // The queries grow to at most this many; an Apple M4 Max needs 8,388,608. + constexpr int kMaxQueries = 128 * 1024 * 1024; + int queries = ratio_timing::presized( + kInitialQueries, kMaxQueries, kMinLegSeconds, + [](int count) { return chain_queries_seconds(400, count); }); // Best of three at each size, so a scheduling hiccup does not decide it. double small = 1e9; double large = 1e9; - for (int i = 0; i < 3; ++i) { - small = std::min(small, chain_queries_seconds(400, queries)); - large = std::min(large, chain_queries_seconds(1600, queries)); - } + ratio_timing::time_measurable_legs(queries, kMaxQueries, kMinLegSeconds, [&](int count) { + small = 1e9; + large = 1e9; + for (int i = 0; i < 3; ++i) { + small = std::min(small, chain_queries_seconds(400, count)); + large = std::min(large, chain_queries_seconds(1600, count)); + } + return std::min(small, large); + }); const double ratio = large / std::max(small, 1e-9); std::printf(" %d chain-membership queries, 400 -> 1600 replaces: %.6f s -> %.6f s " "(x%.2f for 4x, bound x5)\n", queries, small, large, ratio); diff --git a/tests/test_native_match_rescan_scaling.cpp b/tests/test_native_match_rescan_scaling.cpp index 76242596..5d868dc6 100644 --- a/tests/test_native_match_rescan_scaling.cpp +++ b/tests/test_native_match_rescan_scaling.cpp @@ -23,11 +23,16 @@ // full rescan (test_native_match_row_reuse); here only the finished state is // checked, so a leg that failed or placed fewer orders cannot pass as cheap. // RATIO-HARDEN calibrates the tape length until the best small-leg sample has -// at least 100 ms of CPU. The four-to-one live-order ratio and the x8 bound +// at least 250 ms of CPU. The four-to-one live-order ratio and the x8 bound // stay unchanged; sizing the legs, rather than widening the bound, keeps the -// old full-rescan mutant above the gate. +// old full-rescan mutant above the gate. A timed leg under the minimum +// doubles the tape and times both legs again (R5 lane CI-FLAKE, +// tests/ratio_timing.hpp). #include +#include "ratio_timing.hpp" + +#include #include #include #include @@ -122,7 +127,8 @@ double best_of_three(int live, const std::vector& bars) { } constexpr int kInitialBars = 3000; -constexpr int kMaxCalibratedBars = 384000; +// The tape grows to at most this many bars; an Apple M4 Max needs 192,000. +constexpr int kMaxCalibratedBars = 1536000; // The original 100 ms floor was enough for a single run but one loaded-Mac // repetition still let the best-of-three ratio escape. A 250 ms floor keeps // the process-CPU sample away from that scheduling edge without changing the @@ -132,17 +138,19 @@ constexpr int kLive = 10; constexpr double kShapeBound = 8.0; void matching_is_linear_in_the_live_requests() { - int bars_count = kInitialBars; + int bars_count = ratio_timing::presized( + kInitialBars, kMaxCalibratedBars, kMinLegSeconds, + [](int count) { return best_of_three(kLive, tape(count)); }); double small = 0.0; - while (true) { - const std::vector bars = tape(bars_count); + double large = 0.0; + const auto time_legs = [&](int count) { + const std::vector bars = tape(count); small = best_of_three(kLive, bars); - if (small >= kMinLegSeconds || bars_count >= kMaxCalibratedBars / 2) break; - bars_count *= 2; - } - const std::vector bars = tape(bars_count); - small = best_of_three(kLive, bars); - const double large = best_of_three(kLive * 4, bars); + large = best_of_three(kLive * 4, bars); + return std::min(small, large); + }; + ratio_timing::time_measurable_legs(bars_count, kMaxCalibratedBars, kMinLegSeconds, + time_legs); const double ratio = small > 0.0 ? large / small : 0.0; std::printf("resting orders: %d bars, %d live %.4fs, %d live %.4fs, ratio %.2f " "(bound %.1f)\n", bars_count, kLive, small, kLive * 4, large, ratio, diff --git a/tests/test_native_state_continuation.cpp b/tests/test_native_state_continuation.cpp index 205f36a6..28805cb9 100644 --- a/tests/test_native_state_continuation.cpp +++ b/tests/test_native_state_continuation.cpp @@ -45,6 +45,7 @@ #include #include "../src/native_execution_consumer.hpp" +#include "ratio_timing.hpp" #include #include @@ -792,30 +793,40 @@ double read_seconds(int bars, int read_at, int reads) { // V19-A removed x16, so x8; a live-state read is x1 at 4x the history and a // read that walked the history x4, so x2. RATIO-HARDEN repeats each fixed // workload until the best timed leg has at least 100 ms of process CPU; the -// x8 and x2 bounds stay in place. +// x8 and x2 bounds stay in place. A timed leg that still comes in under it +// doubles the repeats and times both legs again (R5 lane CI-FLAKE, +// tests/ratio_timing.hpp). constexpr int kScalingRounds = 5; constexpr double kMinLegSeconds = 0.1; -constexpr int kMaxTimingRepeats = 256; +// The recorded runs repeat at most this many times (an Apple M4 Max needs +// 32), and the reads grow to at most kMaxReads (it needs 512,000). +constexpr int kMaxTimingRepeats = 512; +constexpr int kMaxReads = 8192000; void recording_and_reads_scale() { - int recording_repeats = 1; - while (recorded_seconds(5000, recording_repeats) < kMinLegSeconds - && recording_repeats < kMaxTimingRepeats / 2) { - recording_repeats *= 2; - } + int recording_repeats = ratio_timing::presized( + 1, kMaxTimingRepeats, kMinLegSeconds, + [](int count) { return recorded_seconds(5000, count); }); double quarter = 1e30; double whole = 1e30; double early = 1e30; double late = 1e30; - for (int round = 0; round < kScalingRounds; ++round) { - if (round % 2 == 0) { - quarter = std::min(quarter, recorded_seconds(5000, recording_repeats)); - whole = std::min(whole, recorded_seconds(20000, recording_repeats)); - } else { - whole = std::min(whole, recorded_seconds(20000, recording_repeats)); - quarter = std::min(quarter, recorded_seconds(5000, recording_repeats)); + const auto time_recording = [&](int count) { + quarter = 1e30; + whole = 1e30; + for (int round = 0; round < kScalingRounds; ++round) { + if (round % 2 == 0) { + quarter = std::min(quarter, recorded_seconds(5000, count)); + whole = std::min(whole, recorded_seconds(20000, count)); + } else { + whole = std::min(whole, recorded_seconds(20000, count)); + quarter = std::min(quarter, recorded_seconds(5000, count)); + } } - } + return std::min(quarter, whole); + }; + ratio_timing::time_measurable_legs(recording_repeats, kMaxTimingRepeats, kMinLegSeconds, + time_recording); const double ratio = quarter > 0.0 ? whole / quarter : 0.0; std::printf(" kernel-recorded per-bar broker hashes, best of %d interleaved rounds: " "5000 bars x%d %.4f s, 20000 bars x%d %.4f s (x%.2f for 4x the bars, bound x8)\n", @@ -824,25 +835,26 @@ void recording_and_reads_scale() { CHECK(quarter >= kMinLegSeconds); CHECK(whole >= kMinLegSeconds); CHECK(ratio < 8.0); - int read_repeats = 2000; - while (true) { - const double early_probe = read_seconds(10000, 2500, read_repeats); - const double late_probe = read_seconds(10000, 9999, read_repeats); - if (std::min(early_probe, late_probe) >= kMinLegSeconds - || read_repeats >= 1024 * 1024) { - break; - } - read_repeats *= 2; - } - for (int round = 0; round < kScalingRounds; ++round) { - if (round % 2 == 0) { - early = std::min(early, read_seconds(10000, 2500, read_repeats)); - late = std::min(late, read_seconds(10000, 9999, read_repeats)); - } else { - late = std::min(late, read_seconds(10000, 9999, read_repeats)); - early = std::min(early, read_seconds(10000, 2500, read_repeats)); + int read_repeats = ratio_timing::presized(2000, kMaxReads, kMinLegSeconds, [](int count) { + const double early_probe = read_seconds(10000, 2500, count); + const double late_probe = read_seconds(10000, 9999, count); + return std::min(early_probe, late_probe); + }); + const auto time_reads = [&](int count) { + early = 1e30; + late = 1e30; + for (int round = 0; round < kScalingRounds; ++round) { + if (round % 2 == 0) { + early = std::min(early, read_seconds(10000, 2500, count)); + late = std::min(late, read_seconds(10000, 9999, count)); + } else { + late = std::min(late, read_seconds(10000, 9999, count)); + early = std::min(early, read_seconds(10000, 2500, count)); + } } - } + return std::min(early, late); + }; + ratio_timing::time_measurable_legs(read_repeats, kMaxReads, kMinLegSeconds, time_reads); const double read_ratio = early > 0.0 ? late / early : 0.0; std::printf(" continuation read, %d reads, best of %d interleaved rounds: at bar 2500 " "%.4f s, at bar 9999 %.4f s (x%.2f, bound x2)\n", From 44e01cd82b0f4be0809b6657d637684782814e64 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 09:01:57 +0800 Subject: [PATCH 47/83] tests: time every live-state scaling workload's two sizes in turn (R5 lane CI-FLAKE) Engine CI run 36194784147 failed test_adapter_live_state_scaling on the native-live job (hosted Ubuntu x86-64, ctest --parallel 4 on four SMT threads): the re-issued exit with recording measured 5.10 against the x5 bound, 2,000 bars 0.1724 s and 8,000 bars 0.8789 s. The shape is linear. On idle spark cores the merged row measured that replay at 4.00-4.05 at the same sizes (Cortex-A725, 20 runs) and 3.99-4.01 at 4,000 / 16,000 bars (Cortex-X925, 20 runs); restoring V19-FIX's pre-sizing changed no case (it never sized this workload); the leg alone costs 31.4 us and about 405,000 user instructions a bar from 1,000 to 32,000 bars. The replay and the churn were the two workloads still timed in blocks, five small runs and then five large, so a load that rose between the blocks landed on the large side alone. They are timed in turn now, like the other eight rows. Moving the replay row to the Mac's efficiency cores partway through failed the blocks for a move 2.75-3.75 s into the row and the rounds for 2.75-3.25 s; for a rise of CI's size the blocks' window is four small runs longer. The bound and the sizes are unchanged; tests only. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 8733fb1fdd4b01ea5f372811b8b7918b9907cb48) --- tests/test_adapter_live_state_scaling.cpp | 42 ++++++++++++----------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/tests/test_adapter_live_state_scaling.cpp b/tests/test_adapter_live_state_scaling.cpp index 5d29d068..31ef0905 100644 --- a/tests/test_adapter_live_state_scaling.cpp +++ b/tests/test_adapter_live_state_scaling.cpp @@ -115,6 +115,23 @@ // removal kept but a walk over every origin the run accepted at every bar // fails the flip at x11.99, on the ratio alone. // +// R5 lane CI-FLAKE times every workload's two sizes in turn. The replay and +// the churn were still timed in blocks -- five runs of the small tape, then +// five of the large -- and engine CI run 36194784147 (hosted Ubuntu x86-64, +// `ctest --parallel 4` on four SMT threads) measured the replay at 5.10: 2,000 +// bars 0.1724 s, 8,000 bars 0.8789 s, 86 against 110 us a bar. The shape is +// linear: on idle spark cores the unchanged row measured the replay at +// 4.00-4.05 at those sizes (Cortex-A725, 20 runs) and 3.99-4.01 at 4,000 / +// 16,000 bars (Cortex-X925, 20 runs), and the leg alone costs 31.4 us and +// about 405,000 user instructions a bar from 1,000 to 32,000 bars (X925). In +// blocks, a load that rises after the first small run and before the first +// large one ends slows the large block alone; in turn, only a rise inside the +// first round does. Moved to the Mac's efficiency cores partway through the +// replay row (3.9 times the CPU a bar), the blocks failed for a move 2.75 to +// 3.75 s into the row and the rounds for one 2.75 to 3.25 s in. For a rise +// 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. +// // Single-leg mode for peak-RSS rows: `test_adapter_live_state_scaling --leg // replay|churn|straddle|recancel|constant|cancelall|flip // [--no-recording]` runs one leg and prints its CPU time, retained rows and @@ -338,15 +355,6 @@ int attempts() { return parsed > 0 ? parsed : 5; } -Leg best_of(Workload workload, const std::vector& bars, bool recording) { - Leg best; - for (int attempt = 0; attempt < attempts(); ++attempt) { - const Leg sample = replay(workload, bars, recording); - if (attempt == 0 || sample.seconds < best.seconds) best = sample; - } - return best; -} - constexpr int kBars = 1000; constexpr double kShapeBound = 5.0; // Every ratio leg is calibrated to at least this much process CPU. The cap @@ -362,9 +370,9 @@ constexpr std::size_t kRowBound = 64; // that still work or hold a lot, and the last bar's closes (R5 lane V19-FIX). constexpr std::size_t kRosterBound = 8; -// The straddle's two sizes, timed in turn round after round (which goes first -// alternates), each keeping its best round: its legs are milliseconds long, so -// a load change between two blocks of rounds would land on one side only. +// A workload's two sizes, timed in turn round after round (which goes first +// alternates), each keeping its best round: a load change between two blocks +// of rounds would land on one side only. void interleaved_best(Workload workload, const std::vector& small_tape, const std::vector& large_tape, bool recording, Leg& small, Leg& large) { @@ -392,14 +400,8 @@ void cost_and_rows_are_live(Workload workload, const char* name, bool recording Leg small; Leg large; const auto time_legs = [&](int count) { - const std::vector small_tape = tape(count, step); - const std::vector large_tape = tape(count * 4, step); - if (short_bars) { - interleaved_best(workload, small_tape, large_tape, recording, small, large); - } else { - small = best_of(workload, small_tape, recording); - large = best_of(workload, large_tape, recording); - } + interleaved_best(workload, tape(count, step), tape(count * 4, step), recording, small, + large); return std::min(small.seconds, large.seconds); }; if (gated()) From 95888a7b1e6025d605c76f69225d1ef7184c926f Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 09:50:55 +0800 Subject: [PATCH 48/83] INT26 merge: three sentences the merged tree made stale - ADR-0001's inventory row "fields of the kernel's rows the Pine host amends" (H-DOCGATES 4cbc6ab6, picked as 8deed8c9) was written on a tree where the Pine host still re-stamped aggregated rows with chart-bar indices. INT25's K-IDX (709526be) made the kernel book script-bar indices for every host and removed that re-stamp, so on this tree the host writes no `PyramidEntry::entry_bar_index` or `Trade::exit_bar_index`: it only dates an aggregated or magnified chart's fills at the chart bar's open (`on_native_applied`, design row AG2). The row now says so and names only the fields the host writes; check_kernel_seam_rows still finds every written member in a row (44, 0 without one). - docs/ci.md said a stale feed cache fails as "30 probes drifted" after H-MEASURE's H12 battery grew the PR subset to 54 (9c0579d2, picked as 771da1c4); the workflow comment it mirrors already says 54. - native-engine.md's FX-cursor rule listed the four margin check kinds that existed before PAR-MARGIN's `NativeMarginCheckKind::IntrabarSample` (7c37ae41, picked as b58592c9); that point converts at its own cursor through the same `maintain_margin_liquidation`, so the list names five. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/adr/0001-kernel-adapter-boundary.md | 2 +- docs/ci.md | 2 +- docs/pages/native-engine.md | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 4e60fe21..ed78370f 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -573,7 +573,7 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | `broker_fill_event_seq_` (`include/pineforge/engine.hpp`, in the hashed region) | the Pine host alone, one step per applied broker instruction (`broker_fill_event_seq_` `pine_strategy_host.cpp:595`, in `on_native_applied`); the kernel only resets and folds it | **retained, hashed: the adapter's counter on the kernel's class**. Its placement facts (`signal_close_mc_fill_seq`) compare against it in fill-time gates that cross a bar; the execution authority is the kernel's own ordinal. It is folded into the broker-state hash (`src/engine_state_hash.cpp`), so moving it to the source layer's hash extension moves every run's hash: an epoch decision, not a docs change | | `broker_state_hashes_` (`include/pineforge/engine.hpp`) | the kernel under `KernelRecorded` (`src/native_execution_consumer.cpp`), and the Pine host at each script point it marks while recording is on (`scheduler_record_broker_hash` `pine_strategy_host.cpp:1961`) | **retained, per driving mode** (design Q4): the report's per-bar hash series, waived as append-only report output that execution never consults | | the position book's mirror -- `position_qty_`, `position_entry_price_`, `position_entry_count_`, `pyramid_entries_` (`include/pineforge/engine.hpp`, all hashed) | the kernel books and settles them. The Pine host writes them in two places. After every applied event it erases each lot at or below `internal::kQtyEpsilon` and recomputes the position's size, average price and count (`pyramid_entries_` `pine_strategy_host.cpp:600-626`, in `on_native_applied`, restating `ab9714be`'s partial-exit settlement). Before every script body it overwrites `position_entry_count_` with its own entry-slot count (`source_entry_slot_count` `pine_strategy_host.cpp:1886`) | **OPEN, two rulings nobody has made**. The dust sweep erases a lot inside a live book, which the fourth audit found with neither a row nor a tape (its item 32): lane H-THIN's A4-DUST. The count overwrite gives one hashed member two writers with two meanings: the kernel's lots in the current direction, Pine's opened entry slots. It is recorded here first and has no lane yet | -| fields of the kernel's rows the Pine host amends -- `PyramidEntry::entry_bar_index`, `PyramidEntry::time`, `PyramidEntry::max_runup`, `PyramidEntry::max_drawdown`, `Trade::exit_bar_index`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it gives the rows chart-bar indices and dates their fills at the chart bar's open (`on_native_applied`; design rows AG1 and AG2). It samples its lots' excursions itself (`sample_open_trade_extremes` in `src/source/pine_adapter.cpp`, `replace_masked_entry_bar_extremes`, the slippage mask in `on_native_bar`). It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:1277`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | +| fields of the kernel's rows the Pine host amends -- `PyramidEntry::time`, `PyramidEntry::max_runup`, `PyramidEntry::max_drawdown`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it dates their fills at the chart bar's open (`on_native_applied`; design row AG2); their bar indices are the kernel's own script-bar indices, which it books for every host (design row AG1, R5 lane K-IDX), so the host writes no index. It samples its lots' excursions itself (`sample_open_trade_extremes` in `src/source/pine_adapter.cpp`, `replace_masked_entry_bar_extremes`, the slippage mask in `on_native_bar`). It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:1277`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | | the range-end report -- `range_end_trades_`, the extremes `max_equity_`, `min_equity_`, `max_drawdown_`, `max_runup_`, and `stream_warmup_mode_` (`include/pineforge/engine.hpp`) | the Pine host. `scheduler_record_range_end` clears the report rows, has the kernel's producer append them at the curve's last point and re-folds the four extremes from the re-marked curve (`range_end_trades_` `pine_strategy_host.cpp:1714`). `PineScheduler` raises `stream_warmup_mode_` for a stream's warmup, and the host clears it at the first realtime input or print | **adapter-policy, the `report_open_position_at_end` row above** (design RP5 and §3.7 part 2): report shape, not a mark-to-market row, and the kernel's run-end producer is gated out of every Pine run. The extremes are hashed; the report rows are waived as report-only | | `host_mutation_guard_inert_` (`include/pineforge/engine.hpp`) | the Pine host, once, in its constructor (`host_mutation_guard_inert_ = true` `pine_strategy_host.cpp:168`) | **retained, the host's declared choice**: an in-run call of a frozen C setter is a no-op on a Pine handle, as `ab9714be`'s legacy consumer refused it, and throws on a native host. Outside the hashed region | | `fold_exit_trail_peak_` (`include/pineforge/engine.hpp`, hashed) | nobody but the per-run reset to NaN (`reset_run_state`, `src/engine_run.cpp`) | **dead, kept by the hash** (design §3.7, "A dead branch found and reported"). Its readers test `isnan` and never take the branch, and the member's own comment says so. The broker-state hash folds it (`src/engine_state_hash.cpp`), so deleting it moves every run's hash and `BacktestEngine`'s layout: an epoch item | diff --git a/docs/ci.md b/docs/ci.md index 09acf22c..7d760ed7 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -418,7 +418,7 @@ The subset job checks the submodule out with `GIT_LFS_SKIP_SMUDGE=1` and restores `corpus/data` from a cache keyed by the gitlink, so the ~176 MB feed is fetched at most once per pin rather than once per push; the restored bytes are then checked against the sha256 in the corpus's own Git-LFS pointer, so a -stale or corrupt cache fails as "the feed is wrong" and not as "30 probes +stale or corrupt cache fails as "the feed is wrong" and not as "54 probes drifted". ## Documentation guards diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 2517322f..4c551312 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1244,7 +1244,8 @@ point's own instant, on the curve's own step semantics — a step is in force from its timestamp onward, and the run's scalar `account_fx` holds before the first one. It is the rate BOTH terms convert at, the requirement and the marked equity it is compared with, and the rate `L` is solved at; at every -check kind (`BarOpen`, `AfterApplied`, `Calculation`, `FxRoll`), in batch and +check kind (`BarOpen`, `AfterApplied`, `Calculation`, `FxRoll`, +`IntrabarSample`), in batch and in stream alike. A run that declares no curve has one rate at every instant, so the rule is inert there by construction. From 9c7bfc003e1221004e53dd1affdb30ec903b35fa Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 09:55:21 +0800 Subject: [PATCH 49/83] INT26 (i): promote-baseline is the campaign's own workflow, installed unchanged .github/workflows/promote-baseline.yml is pineforge-workflow main's campaign/ci/promote-baseline.yml (8f711c4, sha256 0d92ea28...) byte for byte. It replaces INT25's (iii) version (b4b9a367): main's own copy runs on the closed PR (pull_request_target) and runs no PR code, the merge commit on main must carry the gated PR head's tree, and the engine axis hands `lab promote` that tree (--merge-tree), which promotes the pair the merge gate's parity verdict measured (the campaign tool now admits a squash merge by exact tree, so a squash no longer defers). ci_preflight's contract pins the new file, and test_ci_preflight's 20 mutations each fail it: the pull_request_target trigger (a pull_request trigger would run the PR's copy), the statuses and id-token permissions, no checkout of the PR head, the newest-first status fold and both contexts, both green=false and both ok=false skips exiting 0, the merge_commit_sha / head_sha environment and dispatch input, ancestry of the merge commit only, the tree comparison, MERGE_TREE from that step, the --merge-commit / --head-sha / --merge-tree / --ci-head arguments and the exit-2 deferral. INT25's pins for what the new file no longer has are gone: the base-tree check (main moved on after the merge; lab promote compares the merge tree with the measured one) and the ban on checks:read and /check-runs (the codegen axis reads check-runs). AGENTS.md and docs/ci.md describe the installed workflow; the old caveat that a squash defers promotion is gone with the campaign tool's exact-head rule. Required secrets and vars (the file's header), on pineforge-4pass/ pineforge-engine: secrets PINEFORGE_APP_ID and PINEFORGE_APP_PRIVATE_KEY present; vars GCP_WORKLOAD_IDENTITY_PROVIDER and GCP_SERVICE_ACCOUNT present; the optional WORKFLOW_REPO, WORKFLOW_REF and PINEFORGE_EVIDENCE_BUCKET absent (their defaults apply). Co-Authored-By: Claude Opus 5.5 (1M context) The docs/ci.md sentence it retires, on the campaign tool refusing a squash merge, was INT25's (0d76a09). --- .github/workflows/promote-baseline.yml | 165 ++++++++++++++----------- AGENTS.md | 14 ++- docs/ci.md | 14 ++- scripts/ci_preflight.py | 35 ++++-- scripts/test_ci_preflight.py | 20 ++- 5 files changed, 145 insertions(+), 103 deletions(-) diff --git a/.github/workflows/promote-baseline.yml b/.github/workflows/promote-baseline.yml index 177feefb..c04f70b2 100644 --- a/.github/workflows/promote-baseline.yml +++ b/.github/workflows/promote-baseline.yml @@ -4,18 +4,28 @@ # pineforge-engine/.github/workflows/promote-baseline.yml # pineforge-codegen-oss/.github/workflows/promote-baseline.yml # -# On a merged PR it advances the campaign baseline one axis to the merged -# composite, reusing the snapshot from the pr-gate run that already cleared -# it. It promotes ONLY when the merge is exact-tree (the merge commit on the -# base branch carries the gated PR head's tree, and the base has not moved past -# it), both maintainer verification statuses are green on the PR head, and a -# PASS verdict binds the full (engine, codegen) pair. Otherwise it exits green -# without promoting. PRs are squash-merged, so the PR head is never an ancestor -# of the base branch: the merge commit is the event's merge_commit_sha, and the -# guard compares trees rather than asking for ancestry. +# On a merged PR it advances the campaign baseline one axis, reusing the +# snapshot the gate already measured. It promotes ONLY when the merge commit +# now on the base branch carries the gated PR head's TREE (a squash merge mints +# a new commit; the strict up-to-date rule keeps its tree the head's), the gate +# is green on that head, and `lab promote` finds the verdict that binds it: +# engine pineforge/verify and pineforge/parity success on the PR head; +# lab promote adopts the pair the merge gate's parity verdict +# measured — the merged engine with the codegen the sweep measured +# (ruling R2, 2026-09-26), PASS or neutral alike +# codegen no failing check-run on the PR head; lab promote needs a pr-gate +# PASS binding (baseline engine, merged codegen), exact head +# Otherwise it exits green without promoting. # -# The gate logic is exact-tree + pineforge/verify + pineforge/parity + -# baseline-promote.mjs. The plane is now GCP: the +# It runs on `pull_request_target`: MAIN's own copy of this file (ref +# refs/heads/main), never the PR's. The promotion identity's workload-identity +# condition admits only `/.github/workflows/promote-baseline.yml@refs/heads/main` +# in a run of refs/heads/main (terraform/gcp/github-wif.tf), so a pull request +# that edits this file cannot mint it — and a `pull_request` trigger, which +# runs the PR's copy, mints nothing at all. It checks out and runs no PR code: +# it fetches the PR head's objects only to compare trees. +# +# The plane is GCP: the # promotion reads the active baseline and the gate ledger from the Postgres # registry through a cloud-sql-proxy, streams the pinned documents from the # GCS evidence bucket, and appends the new baseline row — authenticated by @@ -43,10 +53,10 @@ # Cloud Run role: this workflow executes no job. Nothing is left for # an operator to grant by hand. # -# This file's PATH is part of the identity: the WIF provider admits only -# `//.github/workflows/promote-baseline.yml@…`, so installing it -# under another name mints no token — and no other workflow in the repo can -# mint one either. +# This file's PATH and REF are part of the identity: the WIF provider admits +# only `//.github/workflows/promote-baseline.yml@refs/heads/main`, +# so installing it under another name, or running another branch's copy, +# mints no token — and no other workflow in the repo can mint one either. # # (Optionally pin WORKFLOW_REF, defaults to main; and PINEFORGE_EVIDENCE_BUCKET, # defaults to pineforge-workflow-evidence.) @@ -54,19 +64,20 @@ name: promote-baseline on: - pull_request: + # Main's own copy of this workflow, with the merged PR's payload + # (merge_commit_sha, head.sha, number, html_url, merged_at). + pull_request_target: types: [closed] - # Manual re-fire for a merge whose pull_request event already passed (e.g. a - # PR merged before this workflow was installed, or a rebase-merge whose PR - # head was orphaned): pass the exact commit now on the base branch and the - # PR head the maintainers verified. + # Manual re-fire, from main, for a merge whose event already passed (e.g. a + # PR merged before this workflow was installed): the commit now on the base + # branch, the PR head the gate cleared, and the PR. workflow_dispatch: inputs: merge_commit: description: "40-hex commit now on the base branch to promote" required: true - pr_head: - description: "40-hex PR head the verification statuses were posted on (the merge commit itself for an exact-head merge)" + head_sha: + description: "40-hex PR head the gate cleared (the merge commit must carry its tree)" required: true pr_number: description: "the PR number" @@ -80,9 +91,10 @@ on: permissions: contents: read - # The two required merge-gate verdicts are commit statuses, not Actions - # check-runs. Inspect the PR head commit's statuses before promotion. + # The engine's gate is two commit statuses (pineforge/verify, + # pineforge/parity): `statuses`. The codegen axis reads check-runs: `checks`. statuses: read + checks: read # Workload identity federation mints the GCP credential from the job's own # OIDC token; nothing here can promote without this repo's identity. id-token: write @@ -100,76 +112,79 @@ jobs: *) echo "repo=engine" >> "$GITHUB_OUTPUT" ;; esac + # The base branch, never the PR (pull_request_target checks out main). - uses: actions/checkout@v4 with: fetch-depth: 0 - # Exact-tree is the safety property: the snapshot describes the gated PR - # head's tree, so we promote only if the merge commit is on the base - # branch, carries exactly that tree, and is still the base branch's - # tree. A squash merge mints a new commit whose tree is the PR head's; a - # merge that rewrote the tree, or a base that moved on, is skipped — - # those must be re-gated. - - name: Verify exact-tree merge - id: exacthead + # The safety property is the TREE: the snapshot describes the gated + # head's tree, so we promote only when the merge commit now on the base + # branch carries exactly that tree. A base that moved before the merge, + # or a rewrite, is skipped — those must be re-gated. + - name: Verify the merged tree is the gated tree + id: exacttree env: MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha || github.event.inputs.merge_commit }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.head_sha }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} BASE_REF: ${{ github.event.pull_request.base.ref || 'main' }} run: | set -euo pipefail - git fetch --quiet origin "$BASE_REF" + # The PR head's objects, to read its tree: fetched, never checked out or run. + git fetch --quiet origin "$BASE_REF" "+refs/pull/$PR_NUMBER/head:refs/remotes/origin/pr-head" if ! git merge-base --is-ancestor "$MERGE_SHA" "origin/$BASE_REF"; then echo "::notice::merge commit $MERGE_SHA is not on origin/$BASE_REF; skipping" echo "ok=false" >> "$GITHUB_OUTPUT"; exit 0 fi - # A squashed PR head is on no branch; fetch it by its sha. - if ! git cat-file -e "$HEAD_SHA^{commit}" 2>/dev/null \ - && ! git fetch --quiet --no-tags origin "$HEAD_SHA"; then - echo "::notice::PR head $HEAD_SHA cannot be fetched; skipping" - echo "ok=false" >> "$GITHUB_OUTPUT"; exit 0 - fi merge_tree=$(git rev-parse "$MERGE_SHA^{tree}") head_tree=$(git rev-parse "$HEAD_SHA^{tree}") if [ "$merge_tree" != "$head_tree" ]; then - echo "::notice::merge tree != PR head tree — the merge rewrote the gated tree; re-gate. skipping" - echo "ok=false" >> "$GITHUB_OUTPUT"; exit 0 - fi - base_tree=$(git rev-parse "origin/$BASE_REF^{tree}") - if [ "$base_tree" != "$merge_tree" ]; then - echo "::notice::base tree != merge tree — the base moved on from the merged tree; re-gate. skipping" + echo "::notice::merge commit tree $merge_tree != gated head tree $head_tree — the base moved before the merge; re-gate. skipping" echo "ok=false" >> "$GITHUB_OUTPUT"; exit 0 fi + echo "merge_tree=$merge_tree" >> "$GITHUB_OUTPUT" echo "ok=true" >> "$GITHUB_OUTPUT" - - name: Confirm the merge gate on the merged PR head + - name: Confirm the gate on the PR head id: ci - if: steps.exacthead.outputs.ok == 'true' + if: steps.exacttree.outputs.ok == 'true' env: GH_TOKEN: ${{ github.token }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.head_sha }} + AXIS: ${{ steps.axis.outputs.repo }} run: | set -euo pipefail - # GitHub lists newest statuses first. Keep the first occurrence of - # each context across pages, so an older success cannot mask a - # newer failure or pending verdict on this exact commit. - statuses=$(gh api --paginate \ - "repos/${{ github.repository }}/commits/$HEAD_SHA/statuses?per_page=100") - if ! jq -es ' - reduce (.[][]) as $s ({}; - if has($s.context) then . else .[$s.context] = $s.state end) - | .["pineforge/verify"] == "success" - and .["pineforge/parity"] == "success" - ' <<< "$statuses" >/dev/null; then - echo "::notice::pineforge/verify and pineforge/parity must both be success on $HEAD_SHA — skipping" - echo "green=false" >> "$GITHUB_OUTPUT"; exit 0 + if [ "$AXIS" = engine ]; then + # The engine merge gate. GitHub lists newest statuses first; keep + # the first occurrence of each context across pages, so an older + # success cannot mask a newer failure or pending verdict. + statuses=$(gh api --paginate \ + "repos/${{ github.repository }}/commits/$HEAD_SHA/statuses?per_page=100") + if ! jq -es ' + reduce (.[][]) as $s ({}; + if has($s.context) then . else .[$s.context] = $s.state end) + | .["pineforge/verify"] == "success" + and .["pineforge/parity"] == "success" + ' <<< "$statuses" >/dev/null; then + echo "::notice::pineforge/verify and pineforge/parity must both be success on $HEAD_SHA — skipping" + echo "green=false" >> "$GITHUB_OUTPUT"; exit 0 + fi + else + bad=$(gh api "repos/${{ github.repository }}/commits/$HEAD_SHA/check-runs" \ + --jq '[.check_runs[] | select(.conclusion=="failure" or .conclusion=="cancelled" or .conclusion=="timed_out" or .conclusion=="action_required")] | length') + if [ "$bad" != "0" ]; then + echo "::notice::$bad failing check-run(s) on $HEAD_SHA — not green; skipping" + echo "green=false" >> "$GITHUB_OUTPUT"; exit 0 + fi fi echo "green=true" >> "$GITHUB_OUTPUT" # The campaign tooling lives in the private pineforge-workflow repo. Rather # than a long-lived PAT, mint a short-lived, read-only token from the org # App already installed here (the same app-id/private-key release.yml - # uses), scoped to just that one repo. + # uses), scoped to just that one repo and to reading its contents: without + # a permission-* input the token would carry every permission the App's + # installation has, and only the checkout below uses it. - name: Mint an org App token to read the campaign tooling id: app if: steps.ci.outputs.green == 'true' @@ -179,6 +194,7 @@ jobs: private-key: ${{ secrets.PINEFORGE_APP_PRIVATE_KEY }} owner: pineforge-4pass repositories: pineforge-workflow + permission-contents: read - name: Checkout campaign tooling (pineforge-workflow) if: steps.ci.outputs.green == 'true' @@ -238,7 +254,8 @@ jobs: # federated gcloud credential explicitly. PINEFORGE_GCP_TOKEN_CMD: gcloud auth print-access-token MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha || github.event.inputs.merge_commit }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.head_sha }} + MERGE_TREE: ${{ steps.exacttree.outputs.merge_tree }} PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} PR_URL: ${{ github.event.pull_request.html_url || github.event.inputs.pr_url }} MERGED_AT: ${{ github.event.pull_request.merged_at || github.event.inputs.merged_at }} @@ -248,22 +265,20 @@ jobs: set +e node .workflow/campaign/src/baseline-promote.mjs \ --repo "${{ steps.axis.outputs.repo }}" \ - --merge-commit "$MERGE_SHA" --head-sha "$HEAD_SHA" --ci-head "$HEAD_SHA" --ci-green \ + --merge-commit "$MERGE_SHA" --head-sha "$HEAD_SHA" --merge-tree "$MERGE_TREE" \ + --ci-head "$HEAD_SHA" --ci-green \ --pr-number "$PR_NUMBER" --pr-url "$PR_URL" --merged-at-ms "$ms" code=$? set -e - # Exit 2 is `refuse`: no PASS verdict binds this merge as a single-axis + # Exit 2 is `refuse`: no verdict binds this merge as a single-axis # advance -- a cross-cutting engine+codegen change (gated as a composite - # the single axis cannot complete), an ungated merge, or a merge commit - # the campaign tooling does not accept for the gated head (the squash - # commit is passed as --merge-commit and the verified PR head as - # --head-sha / --ci-head; the tree equality above is this workflow's - # proof that they carry one tree). Defer green, do not fail CI; a - # cross-cutting pair is promoted by hand with `lab promote --composite` - # once both repos merge (see the baseline-promote skill). Any other - # non-zero is a real error. + # the single axis cannot complete), an ungated merge, a squash-merged + # codegen PR (exact head only there), or an engine merge the diff rule + # cleared (nothing measured). Defer green, do not fail CI; a cross-cutting pair is promoted by + # hand with `lab promote --composite` once both repos merge (see the + # baseline-promote skill). Any other non-zero is a real error. if [ "$code" = "2" ]; then - echo "::notice::baseline not promoted (deferred): the campaign tool refused this merge (its reason is printed above) -- no PASS verdict binds it as a single-axis advance, or it is a squash merge commit the tool does not accept for the verified PR head. If this was a cross-cutting engine+codegen change, run the composite promote by hand once both repos are merged. Nothing was changed." + echo "::notice::baseline not promoted (deferred): no verdict binds this merge as a single-axis advance (see the lab promote refusal above). If this was a cross-cutting engine+codegen change, run the composite promote by hand once both repos are merged. Nothing was changed." exit 0 fi exit "$code" diff --git a/AGENTS.md b/AGENTS.md index b350b068..1b8ebe7c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -140,9 +140,11 @@ dispatch run the full CI profiles. A campaign PASS verdict still binds the exact engine and codegen HEADs for baseline promotion. Merged single-axis PRs advance the campaign baseline automatically through -`.github/workflows/promote-baseline.yml` only when the exact-tree guard (the -merge commit on `main` carries the verified PR head's tree, and `main` has not -moved past it), both required statuses on the PR head, and the campaign verdict -pass. PRs are squash-merged, so the workflow hands the campaign tool the squash -commit and the verified PR head; while that tool requires the merge commit to be -the gated head itself, a squash defers promotion and needs new verification. +`.github/workflows/promote-baseline.yml`, pineforge-workflow's +`campaign/ci/promote-baseline.yml` installed unchanged. `main`'s own copy runs +on the closed PR (`pull_request_target`) and runs no PR code. It promotes only +when the merge commit on `main` carries the gated PR head's tree (PRs are +squash-merged, so the head itself never lands on `main`), both required +statuses are success on that head, and `lab promote` finds the merge gate's +parity verdict for that tree; it then adopts the pair that verdict measured. +Anything else exits green without promoting. diff --git a/docs/ci.md b/docs/ci.md index 7d760ed7..330aa48b 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -623,12 +623,14 @@ Their parity verdict posts `pineforge/parity`, reporting no regression or that the PR changed no engine behaviour. The `PineForge strict CI base` ruleset requires these two commit statuses. GitHub Actions jobs, including `build`, `sanitizers`, docs and the parity subset, are advisory. Baseline promotion -requires both statuses to be successful on the verified PR head and a merge -commit on `main` that carries exactly that head's tree while `main` still does -(PRs are squash-merged, so the head itself never lands on `main`), as well as -its campaign verdict guard. The workflow hands the campaign tool the squash -commit and the verified PR head; while that tool requires the merge commit to -be the gated head itself, a squash defers promotion and needs new verification. +(`promote-baseline.yml`, pineforge-workflow's `campaign/ci/promote-baseline.yml` +installed unchanged) runs `main`'s own copy on the closed PR +(`pull_request_target`) and runs no PR code. It requires both statuses to be +successful on the verified PR head and a merge commit on `main` that carries +exactly that head's tree (PRs are squash-merged, so the head itself never lands +on `main`). It hands the campaign tool the merge commit, the PR head and that +tree, and the tool promotes the pair the merge gate's parity verdict measured +for the tree; every other case exits green without promoting. The full corpus sweep remains a separate acceptance step. The nightly and manual corpus workflow, and the maintainers' full parity verification, keep diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index 60e66096..5ab80cbc 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -93,35 +93,46 @@ def ci_workflow_findings(ci: str, native: str, promote: str, cmake: str) -> list or ' workflow_dispatch:' not in native or "${{ inputs.exclude_slow && '--exclude-label slow' || '' }}" not in native): findings.append('native-live must default to full rows for dispatch and push') - if (' statuses: read' not in promote or ' checks: read' in promote + # Baseline promotion is pineforge-workflow's campaign/ci/promote-baseline.yml, + # installed unchanged. Main's own copy runs on the closed PR + # (pull_request_target, never the PR's copy) and runs no PR code; the + # engine axis needs the newest pineforge/verify and pineforge/parity + # statuses on the PR head. + if (' pull_request_target:\n types: [closed]' not in promote + or re.search(r'^ pull_request:', promote, re.MULTILINE) + or ' statuses: read' not in promote + or ' id-token: write' not in promote + or 'ref: ${{ github.event.pull_request.head' in promote or '/commits/$HEAD_SHA/statuses?per_page=100' not in promote or 'gh api --paginate' not in promote or 'reduce (.[][]) as $s' not in promote or 'if has($s.context) then . else .[$s.context] = $s.state end' not in promote or '.["pineforge/verify"] == "success"' not in promote or '.["pineforge/parity"] == "success"' not in promote - or '/check-runs' in promote): - findings.append('baseline promotion must require the latest two head statuses') + or promote.count('echo "green=false" >> "$GITHUB_OUTPUT"; exit 0') != 2): + findings.append("baseline promotion must run main's copy and require the latest two head statuses") # PRs are squash-merged, so the PR head is never an ancestor of the base # branch. The merge commit (the event's merge_commit_sha, or the dispatch - # input) must be on the base branch, carry the verified PR head's tree and - # still be the base branch's tree; every skip exits before promotion. + # input) must be on the base branch and carry the gated PR head's tree; the + # campaign tool gets that tree and promotes only the pair the merge gate's + # verdict measured for it. Every skip exits green before promotion. merge_env = ('MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha' ' || github.event.inputs.merge_commit }}') - head_env = 'HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head }}' + head_env = 'HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.head_sha }}' if (promote.count(merge_env) != 2 or promote.count(head_env) != 3 or 'github.event.pull_request.head.sha || github.event.inputs.merge_commit' in promote - or ' pr_head:\n' not in promote + or ' head_sha:\n' not in promote or 'git merge-base --is-ancestor "$MERGE_SHA" "origin/$BASE_REF"' not in promote or '--is-ancestor "$HEAD_SHA"' in promote or 'merge_tree=$(git rev-parse "$MERGE_SHA^{tree}")' not in promote or 'head_tree=$(git rev-parse "$HEAD_SHA^{tree}")' not in promote - or 'base_tree=$(git rev-parse "origin/$BASE_REF^{tree}")' not in promote or 'if [ "$merge_tree" != "$head_tree" ]; then' not in promote - or 'if [ "$base_tree" != "$merge_tree" ]; then' not in promote - or promote.count('echo "ok=false" >> "$GITHUB_OUTPUT"; exit 0') != 4 - or '--merge-commit "$MERGE_SHA" --head-sha "$HEAD_SHA" --ci-head "$HEAD_SHA"' - not in promote): + or promote.count('echo "ok=false" >> "$GITHUB_OUTPUT"; exit 0') != 2 + or 'MERGE_TREE: ${{ steps.exacttree.outputs.merge_tree }}' not in promote + or '--merge-commit "$MERGE_SHA" --head-sha "$HEAD_SHA" --merge-tree "$MERGE_TREE"' + not in promote + or '--ci-head "$HEAD_SHA" --ci-green' not in promote + or 'if [ "$code" = "2" ]; then' not in promote): findings.append('baseline promotion must admit a squash merge by tree equality only') blocks = re.findall(r'^set\(PINEFORGE_PR_SLOW_TESTS\n(.*?)^\)', cmake, re.MULTILINE | re.DOTALL) diff --git a/scripts/test_ci_preflight.py b/scripts/test_ci_preflight.py index d5e1ddb8..c6f492e1 100644 --- a/scripts/test_ci_preflight.py +++ b/scripts/test_ci_preflight.py @@ -36,26 +36,38 @@ def test_ci_contract_pins_parallel_pr_light_and_full_events(self): (0, "exclude_slow: ${{ github.event_name == 'pull_request' }}", 'exclude_slow: true'), (1, ' default: false', ' default: true'), (1, "${{ inputs.exclude_slow && '--exclude-label slow' || '' }}", ''), + # Baseline promotion runs main's copy of the workflow and no PR + # code, and needs the newest two statuses on the PR head. + (2, ' pull_request_target:\n', ' pull_request:\n'), (2, ' statuses: read', ' checks: read'), + (2, ' id-token: write', ' id-token: none'), + (2, ' with:\n fetch-depth: 0', + ' with:\n ref: ${{ github.event.pull_request.head.sha }}\n' + ' fetch-depth: 0'), (2, '/commits/$HEAD_SHA/statuses?per_page=100', '/commits/$HEAD_SHA/check-runs'), (2, '.["pineforge/parity"] == "success"', 'true'), (2, 'if has($s.context) then . else .[$s.context] = $s.state end', '.[$s.context] = $s.state'), + (2, 'echo "green=false" >> "$GITHUB_OUTPUT"; exit 0', 'echo "green=false" >> "$GITHUB_OUTPUT"'), # Squash merges: the merge commit is merge_commit_sha, admitted by - # tree equality with the verified PR head, never by its ancestry. + # tree equality with the verified PR head, never by its ancestry, + # and the campaign tool gets that tree. (2, 'github.event.pull_request.merge_commit_sha || github.event.inputs.merge_commit', 'github.event.pull_request.head.sha || github.event.inputs.merge_commit'), - (2, 'HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.pr_head }}', + (2, 'HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.head_sha }}', 'HEAD_SHA: ${{ github.event.pull_request.merge_commit_sha || github.event.inputs.merge_commit }}'), - (2, ' pr_head:\n', ' pr_commit:\n'), + (2, ' head_sha:\n', ' pr_commit:\n'), (2, 'git merge-base --is-ancestor "$MERGE_SHA"', 'git merge-base --is-ancestor "$HEAD_SHA"'), (2, 'if [ "$merge_tree" != "$head_tree" ]; then', 'if false; then'), - (2, 'if [ "$base_tree" != "$merge_tree" ]; then', 'if false; then'), (2, 'head_tree=$(git rev-parse "$HEAD_SHA^{tree}")', 'head_tree=$(git rev-parse "$MERGE_SHA^{tree}")'), (2, 'echo "ok=false" >> "$GITHUB_OUTPUT"; exit 0', 'echo "ok=false" >> "$GITHUB_OUTPUT"'), + (2, 'MERGE_TREE: ${{ steps.exacttree.outputs.merge_tree }}', + 'MERGE_TREE: ${{ github.event.pull_request.head.sha }}'), (2, '--merge-commit "$MERGE_SHA"', '--merge-commit "$HEAD_SHA"'), + (2, ' --merge-tree "$MERGE_TREE"', ''), (2, '--ci-head "$HEAD_SHA"', '--ci-head "$MERGE_SHA"'), + (2, 'if [ "$code" = "2" ]; then', 'if false; then'), (3, ' test_chart_day_memo\n test_ci_verify\n', ' test_chart_day_memo\n'), (3, 'APPEND PROPERTY LABELS slow', 'APPEND PROPERTY LABELS other'), From f2756d6bc3bee5de9e9e8a74ba4f968e610c2d2c Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 09:58:57 +0800 Subject: [PATCH 50/83] INT26 (iii): the public contract's C boundary is H-DOCGATES' table public-contract.md (REL10 5f423b54) pointed its C-surface boundary at the native engine guide's section *Driving the kernel from C* and named only the COVERAGE `[--]` rows and the enumeration exclusions as the list. That section now carries the table "The 1.0 C boundary" (H-DOCGATES 77a3eacb): every C++ capability the 1.0 C surface does not expose, its reason, its C route and the checker row that pins it. The contract names that table and all three kinds of checker row (COVERAGE `[--]`, `ENUM_TWINS` exclusion, `C_V1_EXCLUSIONS`), and what makes check_native_c_api_surface fail for each, including the table's own citation count. The 1.1.0 plan reads as four of the table's rows, and the no-parity sentence bounds itself by the declared fields and calls, as the table does. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/pages/public-contract.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/pages/public-contract.md b/docs/pages/public-contract.md index fb690acb..87bc6ac9 100644 --- a/docs/pages/public-contract.md +++ b/docs/pages/public-contract.md @@ -146,16 +146,20 @@ and `` (@ref native_engine). ## The 1.0 C-surface boundary The C surface is a documented subset of the native C++ API in 1.0, not its -twin. What a C host cannot reach, each exclusion with its reason, is listed in -@ref native_engine, section *Driving the kernel from C*. The `[--]` rows of -`native_c_api.h`'s COVERAGE block and the exclusions -`scripts/check_native_c_api_surface.py` declares hold that list: the checker -fails when a public `NativeStrategyHost` member has no row, or when a C -enumeration is neither twinned to its kernel enumeration nor declared C-only. -Four additions are planned for 1.1.0: a non-mutating execution preview, the +twin. What a C host cannot reach is the table *The 1.0 C boundary* in +@ref native_engine (section *Driving the kernel from C*): every C++ capability +the 1.0 C surface does not expose, with its reason, the C route where there is +one, and the checker row that pins it -- a `[--]` row of `native_c_api.h`'s +COVERAGE block, a named `ENUM_TWINS` exclusion, or a `C_V1_EXCLUSIONS` row of +`scripts/check_native_c_api_surface.py`. The checker fails when a public +`NativeStrategyHost` member has no COVERAGE row, when a C enumeration is +neither twinned to its kernel enumeration nor declared C-only, when a +`C_V1_EXCLUSIONS` row's C++ declaration goes or its C spelling appears, and +when that table cites a `C_V1_EXCLUSIONS` row once too few or too many. +Four of its rows are planned for 1.1.0: a non-mutating execution preview, the origin and label of an applied event, a closed-trade entry-comment accessor and a replace-options word. Nothing in 1.0 promises C and C++ parity beyond -that list. +the fields and calls the C surface declares. ## Pairing with codegen From cb1f4ff57eab44efc1ce3b8fa005f5d9460564a2 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:01:25 +0800 Subject: [PATCH 51/83] INT26 (v): the install pages name the image that is published REL10's finding 4: install.md, docker/README.md, tutorial/README.md and tutorial-macd.md told users to pull ghcr.io/pineforge-4pass/pineforge-engine, which no workflow has built since #38 (1806edff) made the engine a library; release.yml's own header says it publishes no image and its notes point at the hub. They also told users to build `docker/Dockerfile`, which that commit deleted. What is published, per release.yml and the release hub: - an engine release attaches the static-lib tarballs (pineforge-vX.Y.Z-{linux-x86_64,linux-aarch64,macos-universal}) to its GitHub release and dispatches pineforge-release; - the hub builds ghcr.io/pineforge-4pass/pineforge-release from that tarball, the same-version pineforge-codegen and this tree's docker/ harness (vendored), for linux amd64 and arm64: a stable release tagged X.Y.Z, X.Y, latest, engine-codegen and sha-, a release candidate only its exact version, engine-codegen and sha- (public-contract.md, "Pairing with codegen"). The pages now pull that image and mount tutorial/macd/strategy.pine, which the image transpiles with its own codegen (a generated.cpp from this tree's codegen need not compile against an older image's engine); the numbers equal Path A's for the image tagged with this tree's release. docker/README says the tree keeps the harness but no Dockerfile, points at the hub's, and names the image's version labels for reproducibility. Co-Authored-By: Claude Opus 5.5 (1M context) The sentences it retires, on the published runtime image, its pull and its local docker/Dockerfile build, were written with the API site (a8f84b9). --- docker/README.md | 40 ++++++++++++++++++++++--------------- docs/pages/install.md | 25 ++++++++++++++++------- docs/pages/tutorial-macd.md | 18 ++++++++++------- tutorial/README.md | 22 +++++++++++--------- 4 files changed, 66 insertions(+), 39 deletions(-) diff --git a/docker/README.md b/docker/README.md index f654ad2b..35b633b8 100644 --- a/docker/README.md +++ b/docker/README.md @@ -16,30 +16,36 @@ stderr. ## Pull (prebuilt) -Each push to `main` (and every semver tag `vX.Y.Z`) publishes a -multi-arch image to GitHub Container Registry: +This repository's workflows publish no image. The prebuilt image of this +harness is the release hub's, `ghcr.io/pineforge-4pass/pineforge-release`: +pineforge-release builds it from each engine release's static-lib tarball +with the `pineforge-codegen` of the same version, for Linux `amd64` and +`arm64`. ```bash -docker pull ghcr.io/pineforge-4pass/pineforge-engine:latest +docker pull ghcr.io/pineforge-4pass/pineforge-release:latest # or pin a release: -docker pull ghcr.io/pineforge-4pass/pineforge-engine:0.1.0 +docker pull ghcr.io/pineforge-4pass/pineforge-release:X.Y.Z ``` -Available tags: `latest`, `main`, `vX.Y.Z`, `X.Y`, and `sha-`. -Linux `amd64` + `arm64`. Image must be marked public in the repo's -package settings for anonymous pulls; otherwise `docker login ghcr.io` -with a PAT (`read:packages` scope) first. +A stable release is tagged `X.Y.Z`, `X.Y`, `latest`, +`engine-codegen` and `sha-`; a release candidate only its exact +version (for example `1.0.0-rc.1`), `engine-codegen` and +`sha-`, never `latest`. ## Build (from source) -```bash -docker build -t pineforge -f docker/Dockerfile . -``` +This tree keeps the harness the image runs (`docker/entrypoint.sh`, +`docker/run_json.py`) but no Dockerfile: the engine is a library and its +release builds no image (#38). The image's Dockerfile is pineforge-release's +`docker/Dockerfile`. It vendors this harness from the pinned engine release, +fetches that release's static-lib tarball, and adds `g++`, Eigen, `python3` +and the `pineforge-codegen` transpiler; clone that repository to build the +image yourself. Per-run transpile+compile is ~1 second. -Multi-stage build: stage 1 compiles `libpineforge.a`, stage 2 keeps only the -static lib, public headers, `g++`, `python3`, the `pineforge-codegen` -transpiler, and the JSON harness. One-time cost; per-run transpile+compile is -~1 second. +The examples below call the image `pineforge`: tag the pulled one that way +(`docker tag ghcr.io/pineforge-4pass/pineforge-release:latest pineforge`) or +write its full name. ## Run @@ -321,4 +327,6 @@ docker run --rm \ must resolve at compile time because `` transitively includes it. - Reproducibility: the libpineforge inside the image is pinned by the - image SHA. Tag releases (`pineforge:0.1.0`) for stable backtests. + image SHA, and its `io.pineforge.engine.version` / + `io.pineforge.codegen.version` labels name the pair. Pull a release tag + (`ghcr.io/pineforge-4pass/pineforge-release:X.Y.Z`) for stable backtests. diff --git a/docs/pages/install.md b/docs/pages/install.md index 15c81742..bdebfc19 100644 --- a/docs/pages/install.md +++ b/docs/pages/install.md @@ -55,24 +55,35 @@ ${prefix}/ ## Docker -The published image bundles the runtime plus a one-shot transpile + run -harness for ad-hoc strategy execution. +This repository publishes no container image: a release attaches the +prebuilt static-lib tarballs above to its GitHub release and notifies the +release hub, pineforge-release. The hub publishes the image, +`ghcr.io/pineforge-4pass/pineforge-release`: this runtime, the +`pineforge-codegen` transpiler of the same version and the one-shot transpile ++ run harness of `docker/`, built from the release's static-lib tarball. A +stable release is tagged `X.Y.Z`, `X.Y`, `latest` and +`engine-codegen`; a release candidate only under its exact version +(for example `1.0.0-rc.1`) and `engine-codegen`. Pin the tag of the +release you build against. ```bash -docker pull ghcr.io/pineforge-4pass/pineforge-engine:latest +docker pull ghcr.io/pineforge-4pass/pineforge-release:latest ``` -Run the tutorial MACD strategy entirely inside the container: +Run the tutorial MACD strategy entirely inside the container; the image +transpiles the `.pine` with its own codegen: ```bash docker run --rm \ - -v "$(pwd)/tutorial/macd/generated.cpp:/in/strategy.cpp:ro" \ + -v "$(pwd)/tutorial/macd/strategy.pine:/in/strategy.pine:ro" \ -v "$(pwd)/tutorial/data/btcusdt_15m_7d.csv:/in/ohlcv.csv:ro" \ - ghcr.io/pineforge-4pass/pineforge-engine:latest > report.json + ghcr.io/pineforge-4pass/pineforge-release:latest > report.json ``` See [`docker/README.md`](https://github.com/pineforge-4pass/pineforge-engine/blob/main/docker/README.md) -for the full mount + JSON schema reference. +for the full mount + JSON schema reference. This tree keeps that harness +(`docker/entrypoint.sh`, `docker/run_json.py`) but no Dockerfile; the image's +Dockerfile is pineforge-release's. ## Verifying the install diff --git a/docs/pages/tutorial-macd.md b/docs/pages/tutorial-macd.md index 4fb1ba2b..38981320 100644 --- a/docs/pages/tutorial-macd.md +++ b/docs/pages/tutorial-macd.md @@ -85,21 +85,25 @@ Numbers depend on the OHLCV snapshot — refresh with ## Path B — Docker -Mount the strategy + OHLCV into the published runtime image; get a JSON -report on stdout. +Mount the strategy + OHLCV into the release hub's image, +`ghcr.io/pineforge-4pass/pineforge-release` (this runtime plus the +`pineforge-codegen` of the same version; this repository publishes no image +of its own); get a JSON report on stdout. ```bash docker run --rm \ - -v "$(pwd)/tutorial/macd/generated.cpp:/in/strategy.cpp:ro" \ + -v "$(pwd)/tutorial/macd/strategy.pine:/in/strategy.pine:ro" \ -v "$(pwd)/tutorial/data/btcusdt_15m_7d.csv:/in/ohlcv.csv:ro" \ - ghcr.io/pineforge-4pass/pineforge-engine:latest > report.json + ghcr.io/pineforge-4pass/pineforge-release:latest > report.json jq '.summary' report.json ``` -Same engine, identical numbers. Build the image locally instead with -`docker build -t pineforge -f docker/Dockerfile .` if you don't want to -pull from GHCR. +The image transpiles the `.pine` with its own codegen and runs it on its own +engine, so the image tagged with this tree's release gives Path A's numbers. +To build the image yourself, use pineforge-release's `docker/Dockerfile`, +which vendors this tree's `docker/` harness; this repository ships no +Dockerfile. ## Inside run.py — annotated walkthrough diff --git a/tutorial/README.md b/tutorial/README.md index e49e00b8..12a8bc92 100644 --- a/tutorial/README.md +++ b/tutorial/README.md @@ -66,21 +66,25 @@ lifecycle remain the same. ## Path B — Docker (no local toolchain) -Mount the strategy + OHLCV into the published runtime image; get a -JSON report on stdout. +Mount the strategy + OHLCV into the release hub's image, +`ghcr.io/pineforge-4pass/pineforge-release` (this runtime plus the +`pineforge-codegen` of the same version; this repository publishes no image +of its own); get a JSON report on stdout. ```bash docker run --rm \ - -v "$(pwd)/tutorial/macd/generated.cpp:/in/strategy.cpp:ro" \ + -v "$(pwd)/tutorial/macd/strategy.pine:/in/strategy.pine:ro" \ -v "$(pwd)/tutorial/data/btcusdt_15m_7d.csv:/in/ohlcv.csv:ro" \ - ghcr.io/pineforge-4pass/pineforge-engine:latest > report.json + ghcr.io/pineforge-4pass/pineforge-release:latest > report.json jq '.summary' report.json ``` -Same engine, same numbers. Build the image locally instead with -`docker build -t pineforge -f docker/Dockerfile .` if you don't want -to pull from GHCR. Full mount/schema reference in +The image transpiles the `.pine` with its own codegen and runs it on its own +engine, so the image tagged with this tree's release gives Path A's numbers. +To build the image yourself, use pineforge-release's `docker/Dockerfile`, +which vendors this tree's `docker/` harness; this repository ships no +Dockerfile. Full mount/schema reference in [`docker/README.md`](../docker/README.md). ## Advanced — re-run with different params, no rebuild @@ -117,9 +121,9 @@ fast slow qty trades win% net_pnl max_dd ms docker run --rm \ -e PINEFORGE_INPUTS='{"Fast Length": "8", "Slow Length": "21"}' \ -e PINEFORGE_OVERRIDES='{"default_qty_value": "5", "commission_value": "0.04"}' \ - -v "$(pwd)/tutorial/macd/generated.cpp:/in/strategy.cpp:ro" \ + -v "$(pwd)/tutorial/macd/strategy.pine:/in/strategy.pine:ro" \ -v "$(pwd)/tutorial/data/btcusdt_15m_7d.csv:/in/ohlcv.csv:ro" \ - ghcr.io/pineforge-4pass/pineforge-engine:latest \ + ghcr.io/pineforge-4pass/pineforge-release:latest \ | jq '{applied_inputs, applied_overrides, summary}' ``` From 5a209ace29f5a0daf21d52398ff1f0b9067d1f3c Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:02:09 +0800 Subject: [PATCH 52/83] INT26 (vi): the C ABI guard is named by its stage, source-guard-c-abi REL10's finding 8: check_c_abi_runtime.py's docstring, CLAUDE.md's and AGENTS.md's PF_API SOP, and the design record's SOP row named a "C ABI runtime source check" CI step. No workflow or profile has a step by that name: the script runs as the `source-guard-c-abi` stage of ci_preflight.py and of every ci_verify.py profile (SOURCE_GUARD_SCRIPTS), before configure. The SOP now names that stage and says it runs before the build (it said "after build+test"). Co-Authored-By: Claude Opus 5.5 (1M context) --- AGENTS.md | 12 +++++++----- CLAUDE.md | 12 +++++++----- docs/design/native-feature-parity.md | 2 +- scripts/check_c_abi_runtime.py | 5 +++-- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1b8ebe7c..670de8c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -65,11 +65,13 @@ If `scripts/run_corpus.sh` reports any parity drift or failures, investigate the ## SOP: adding a runtime `PF_API` export (CI gate — recurring failure) -CI runs `python3 scripts/check_c_abi_runtime.py` after build+test. It pins the -exact set of `PF_API` symbols implemented in `src/c_abi.cpp` against the -hardcoded `EXPECTED_RUNTIME` frozenset in that script. Adding (or removing) a -runtime export WITHOUT updating that list fails ALL CI matrix jobs at the -"C ABI runtime source check" step, even though build and ctest are green. +CI runs `python3 scripts/check_c_abi_runtime.py` as the `source-guard-c-abi` +stage of `scripts/ci_preflight.py` and of every `scripts/ci_verify.py` profile, +before anything is built. It pins the exact set of `PF_API` symbols implemented +in `src/c_abi.cpp` against the hardcoded `EXPECTED_RUNTIME` frozenset in that +script. Adding (or removing) a runtime export WITHOUT updating that list fails +the `source-guard-c-abi` stage, and with it preflight and every profile, even +though the build and ctest would be green. Checklist when touching runtime exports — update ALL of these together: diff --git a/CLAUDE.md b/CLAUDE.md index 8ae21a17..eac9abe5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,11 +75,13 @@ If `scripts/run_corpus.sh` reports any parity drift or failures, investigate the ## SOP: adding a runtime `PF_API` export (CI gate — recurring failure) -CI runs `python3 scripts/check_c_abi_runtime.py` after build+test. It pins the -exact set of `PF_API` symbols implemented in `src/c_abi.cpp` against the -hardcoded `EXPECTED_RUNTIME` frozenset in that script. Adding (or removing) a -runtime export WITHOUT updating that list fails ALL CI matrix jobs at the -"C ABI runtime source check" step, even though build and ctest are green. +CI runs `python3 scripts/check_c_abi_runtime.py` as the `source-guard-c-abi` +stage of `scripts/ci_preflight.py` and of every `scripts/ci_verify.py` profile, +before anything is built. It pins the exact set of `PF_API` symbols implemented +in `src/c_abi.cpp` against the hardcoded `EXPECTED_RUNTIME` frozenset in that +script. Adding (or removing) a runtime export WITHOUT updating that list fails +the `source-guard-c-abi` stage, and with it preflight and every profile, even +though the build and ctest would be green. Checklist when touching runtime exports — update ALL of these together: diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 03770766..0ad9c37e 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -270,7 +270,7 @@ PF_API int pf_native_events_v1(pf_strategy_t, uint64_t after_ordinal, pf_native_ | Errors | A non-zero callback return maps to `NativeFailureCode::CallbackException`; C callbacks must not unwind. The C++ path already latches exceptions as `CallbackException` native_execution_consumer.cpp:7068 | F O | | Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1406-1413): inside a callback, or between realtime inputs | F | | Hardening | Tagged, size-prefixed PODs; `struct_size` + version; unknown-tag refusal; never cast the C request to a C++ variant; a C fixture compiles against the frozen previous header. S puts the API in a separate header `include/pineforge/native_c_api.h` | S | -| SOP | Every symbol goes into `src/c_abi.cpp`, `pineforge.h`, `EXPECTED_RUNTIME` + both counts (check_c_abi_runtime.py:19-79), the README table and the Python harnesses, or all CI matrix jobs fail at the "C ABI runtime source check" step (`CLAUDE.md`) | F O S | +| SOP | Every symbol goes into `src/c_abi.cpp`, `pineforge.h`, `EXPECTED_RUNTIME` + both counts (check_c_abi_runtime.py:19-79), the README table and the Python harnesses, or the `source-guard-c-abi` stage fails, in preflight and every `ci_verify` profile (`CLAUDE.md`) | F O S | | Not in v1 of the C API | `resolve_execution_terms` / `validate_execution_precommit` are the adapter's seam; a C host that needs sizing uses `Sized` (F). O offers an optional `resolve_terms` callback: deferred. *History: R5 lane F4 routed both (`on_execution_terms`, `on_precommit`); the terms hook's units answer outside a host-sized close is still C++-only (the 1.0 C boundary table)* | F O | | Open | Naming: F and O use `pf_native_*`; S uses `strategy_native_*`, which matches the existing runtime symbols. The v1 C spec `pf_native_run_spec_v1` pineforge.h:548 omitted `intrabar`, `path_order` and `abort_reporting`; the extension's policy tail carries all three now (`pf_native_abort_reporting_e` native_c_api.h:907) | F S | diff --git a/scripts/check_c_abi_runtime.py b/scripts/check_c_abi_runtime.py index 36f343d7..01e0615a 100644 --- a/scripts/check_c_abi_runtime.py +++ b/scripts/check_c_abi_runtime.py @@ -13,8 +13,9 @@ include/pineforge/native_c_api.h and its implementations in src/native_c_host.cpp, so they do not move the counts above; they are pinned by EXPECTED_NATIVE_C_API instead. Adding or removing a symbol - there without updating that list fails every CI matrix job at the "C ABI - runtime source check" step, exactly as it does for c_abi.cpp. + there without updating that list fails this script's stage, + source-guard-c-abi (ci_preflight and every ci_verify profile), exactly as + it does for c_abi.cpp. """ from __future__ import annotations From 1c436980dcdfee419fd0a9113753ef4125a9560c Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:20:51 +0800 Subject: [PATCH 53/83] INT26 (viii): clang-compiled consumer TUs get -fbracket-depth=1024 CGINT's finding: clang's default of 256 nested brackets rejects C++ that codegen transpiles from deep Pine (nested calls, long `+` / `and` chains and nested blocks past about 250 levels, `not` chains past about 125). libpineforge's INTERFACE options now carry -fbracket-depth=1024 to every C++ TU compiled by Clang or AppleClang that links it: the corpus, bench and tutorial strategy targets and any find_package(PineForge) consumer. GCC has no such limit and no such flag, so the generator expression leaves GCC builds (every Linux CI job, docker/entrypoint.sh's g++) untouched; the library's own objects are unchanged (INTERFACE only). Decided by measurement (INT26-scratch/bracket-*.tsv): each of the 312 corpus strategy TUs compiled with its exact recipe and with the flag appended, in pairs on one worker, order alternating per TU and reversed in a second pass: - clang 18.1.3, spark, 9 idle pinned cores: 312/312 objects byte-identical; summed compile CPU time 591.9 s -> 592.1 s (ratio 1.0002) and, reversed, 592.3 s -> 592.2 s (0.9998); - Apple clang 17.0.0, this Mac (loaded): 312/312 objects byte-identical; 403.3 s -> 404.3 s (1.0024). Neither changes object code or build time by 1 %, so the flag is added. Co-Authored-By: Claude Opus 5.5 (1M context) --- CMakeLists.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f530afe..7bd08623 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -235,6 +235,16 @@ foreach(_pf_library ${PINEFORGE_LIBRARY_TARGETS}) # library's own flags above stay exactly as they are. install(EXPORT) # carries it into PineForgeTargets.cmake. target_compile_options(${_pf_library} INTERFACE -ffp-contract=off) + # Clang stops parsing at 256 nested brackets, parentheses and braces + # (-fbracket-depth), and codegen transpiles Pine whose C++ nests deeper: + # nested calls, long `+` / `and` chains and nested blocks fail past about + # 250 levels, `not` chains past about 125. Every clang-compiled consumer + # TU gets 1024. Measured on the 312 corpus strategy TUs: byte-identical + # objects with and without it (clang 18 on spark, Apple clang 17) and the + # same compile time (summed CPU-time ratio 1.0002 and 0.9998 in the two + # orders on spark's idle cores). GCC has no such limit and no such flag. + target_compile_options(${_pf_library} INTERFACE + $<$:-fbracket-depth=1024>) target_include_directories(${_pf_library} PUBLIC From d1e4b5a62c573cf93ae6e47c08dc115ea5d639ef Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 12:53:55 +0800 Subject: [PATCH 54/83] INT26 (iv): the ctest floors are the integrated tree's own counts Every floor recounted with ctest -N under ci_verify's own configure argv per profile on the integrated tree (INT26-scratch/ctestN/pm2-*.txt), against main 0d76a099 (INT25's final lists, the same tree): | profile | main | INT26 | floor | |---|---:|---:|---| | release | 678 | 700 | RELEASE_MIN_TESTS 678 -> 700 | | kernel | 278 reg / 277 run | 286 reg / 285 run | KERNEL_MIN_TESTS 277 -> 285 | | debug | 659 | 681 | EXCLUDED_REGISTERED_MIN 659 -> 681 | | sanitizers | 659 | 681 | 659 -> 681 | | native | 668 | 690 | 668 -> 690 | The twenty-two added rows, named in the floor comments by lane: H-MEASURE +12 (the acid composite, source-free, and eleven source-bound X14 rows), H-DOCGATES +3 (test_doc_reverts, test_kernel_seam_rows, test_native_runtime_ambient_lifo), K-OCA-KEEP +1 (test_native_group_keep_handle), PAR-ORDERS-2 +4 (test_native_current_cohort_refusal, source-free, and its three tape rows), PAR-MARGIN +1 (test_native_margin_intrabar_samples), PAR-MARGIN-2 +1 (test_native_margin_post_fill_path). REL10 renames F6's row (test_deprecated_public_spellings -> test_removed_public_spellings); PAR-ORDERS and CI-FLAKE add none. The picks' summed interim comments are replaced by that one account; test_ci_verify pins the new excluded floors, and the four floor citations name the floors' lines. The PR exclusion list takes one row: test_native_group_keep_handle ran 89.0 s under sanitizers (20.3 s Debug) in K-OCA-KEEP's own lab remote run (rj-20260925t213329-2293be), over CI-LITE's 60 s rule; every other wave-H row stays far below it (largest test_magnified_aggregated_tape, 3.4 s; PAR-ORDERS-2's four took at most 0.4 s under sanitizers in its own run, rj-20260926t025320-9c5f6f; PAR-MARGIN-2's took 0.07 s under sanitizers in rj-20260926t030149-64ec63). 29 slow rows, preflight's digest re-pinned, docs/ci.md says why. Replayed after the late picks PAR-ORDERS-2 and PAR-MARGIN-2: the counts above are this position's, recounted with ctest -N. Co-Authored-By: Claude Opus 5.5 (1M context) --- CONTRIBUTING.md | 4 +- docs/ci.md | 9 +++- docs/pages/contributing-llm.md | 6 +-- scripts/ci_preflight.py | 4 +- scripts/ci_verify.py | 91 ++++++++++++---------------------- scripts/test_ci_verify.py | 2 +- tests/CMakeLists.txt | 1 + 7 files changed, 48 insertions(+), 69 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e0d80f8..1a9a1505 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -152,8 +152,8 @@ What each gate refuses: ### The floors `ci_verify.py` counts the CTest rows that actually **ran** and fails below a -floor — `KERNEL_MIN_TESTS` ci_verify.py:264 and `RELEASE_MIN_TESTS` -ci_verify.py:408. +floor — `KERNEL_MIN_TESTS` ci_verify.py:258 and `RELEASE_MIN_TESTS` +ci_verify.py:387. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/docs/ci.md b/docs/ci.md index 330aa48b..1f50d8ab 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -596,7 +596,7 @@ Superseded pull-request runs are canceled. Main/post-merge and manual CI runs use distinct concurrency groups and remain uncanceled. A PR runs preflight, both Release jobs, kernel-only and the parity subset with their full sets; both Debug jobs, sanitizers and native-live run the registered set excluding -the 28 CTest rows labelled `slow` in `tests/CMakeLists.txt`. The first 27 were +the 29 CTest rows labelled `slow` in `tests/CMakeLists.txt`. The first 27 were chosen from INT23/INT24 job logs: over 60 seconds in either sanitizer run or over 30 seconds in either Debug run. Preflight and all proof jobs start in parallel. The advisory `build` aggregate succeeds only if every job succeeds. @@ -610,7 +610,12 @@ run on the same host and is the 28th. The largest unlabelled rows there were `test_adapter_quiet_bar_differential` (39.3 s sanitizers), `test_native_state_continuation` (34.7 s sanitizers, 18.9 s Debug) and `test_adapter_lookup_index_scaling` (24.3 s sanitizers); the whole sanitizers -set took 7.9 min of wall time, the PR set 614 s of summed test time. +set took 7.9 min of wall time, the PR set 614 s of summed test time. INT26 read +wave H's new rows from the lanes' own sanitizers and Debug runs on the same +hosts: K-OCA-KEEP's `test_native_group_keep_handle` took 89.0 s under +sanitizers (20.3 s Debug) and is the 29th; no other new row comes near a +threshold (the largest, `test_magnified_aggregated_tape`, 3.4 s under +sanitizers). A push to `main` and a manual dispatch of `ci.yml` run every profile without the exclusion. Their sanitizers CTest stage gets an hour (`ci_verify.py` diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 45f948cf..4eb528a1 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -117,8 +117,8 @@ finding to report, not a step to take. `scripts/check_doc_reverts.py`. 10. **A test row never silently disappears.** Each profile counts the rows that - *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:264 and - `RELEASE_MIN_TESTS` ci_verify.py:408. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:258 and + `RELEASE_MIN_TESTS` ci_verify.py:387. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -264,7 +264,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:264, `RELEASE_MIN_TESTS` ci_verify.py:408). It +(`KERNEL_MIN_TESTS` ci_verify.py:258, `RELEASE_MIN_TESTS` ci_verify.py:387). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index 5ab80cbc..6e21a95e 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -33,7 +33,7 @@ ACTIONLINT_VERSION = '1.7.12' # The names live only in tests/CMakeLists.txt. A digest pins that measured # population without maintaining a second row list in this Python guard. -PR_SLOW_ROWS_SHA256 = 'c1abd9bdd0c540398eb5e0870a0f652b12743d653eafb5c4ee9ee94ca6dc4cf0' +PR_SLOW_ROWS_SHA256 = 'e8bddea797abc46ca0deb671d5d35db3491b67a82e1413d08278530d12f7b67b' def _jobs(workflow: str) -> dict[str, str]: @@ -138,7 +138,7 @@ def ci_workflow_findings(ci: str, native: str, promote: str, cmake: str) -> list re.MULTILINE | re.DOTALL) names = re.findall(r'^ (test_[A-Za-z0-9_]+)$', blocks[0], re.MULTILINE) if len(blocks) == 1 else [] canonical = '\n'.join(names) + '\n' - if (len(blocks) != 1 or len(names) != 28 or len(set(names)) != 28 + if (len(blocks) != 1 or len(names) != 29 or len(set(names)) != 29 or hashlib.sha256(canonical.encode()).hexdigest() != PR_SLOW_ROWS_SHA256 or cmake.count('APPEND PROPERTY LABELS slow') != 1 or 'set_property(TEST ${_pf_slow_test} APPEND PROPERTY LABELS slow)' not in cmake diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 132966cc..eb284e40 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -239,28 +239,22 @@ # extracted from the page and run) # All six register in release too; RATIO-HARDEN, V19-FIX and K-IDX add no row. # 278 registered, 277 run: the WebSocket row still skips on a system libcurl. -# 278 run = those 277 plus the one source-free row of R5 lane H-MEASURE -# (INT26; summed at the pick, recounted with ctest -N at INT26 (iv)): -# +1 H-MEASURE test_native_acid_composite (the G1 acid composite, C++ and C) -# Its other eleven rows reach the source layer (see RELEASE_MIN_TESTS). 279 -# registered, 278 run: the WebSocket row still skips on a system libcurl. -# 279 = those 278 plus R5 lane H-DOCGATES' test_doc_reverts (the doc-revert -# gate's self-test, a Python row every profile registers; INT26 pick, summed). -# 282 = those 281 plus the one source-free TU of R5 lane K-OCA-KEEP -# (test_native_group_keep_handle; INT26 pick, summed). It registers in release -# too. -# 283 = those 282 plus R5 lane PAR-ORDERS-2's source-free -# test_native_current_cohort_refusal (it registers in release too; INT26 -# pick, summed). -# 284 = those 283 plus R5 lane PAR-MARGIN's source-free row (INT26 pick, -# summed): -# +1 PAR-MARGIN test_native_margin_intrabar_samples (IntrabarSample, item 3) -# It registers in release too. -# 285 = those 284 plus R5 lane PAR-MARGIN-2's source-free row (INT26 pick, -# summed): -# +1 PAR-MARGIN-2 test_native_margin_post_fill_path (the post-fill margin -# check measures from the fill's own waypoint) -# It registers in release too. +# 285 run = those 277 plus the eight source-free rows of wave H (INT26), +# counted with ctest -N on the integrated tree: +# +1 H-MEASURE test_native_acid_composite (the G1 acid composite, C++ +# and C) +# +3 H-DOCGATES test_doc_reverts and test_kernel_seam_rows (the two gates' +# self-tests, Python rows every profile registers) and +# test_native_runtime_ambient_lifo (the runtime blocks' LIFO +# death row) +# +1 K-OCA-KEEP test_native_group_keep_handle +# +1 PAR-ORDERS-2 test_native_current_cohort_refusal +# +1 PAR-MARGIN test_native_margin_intrabar_samples +# +1 PAR-MARGIN-2 test_native_margin_post_fill_path +# REL10 renames F6's row (test_deprecated_public_spellings -> +# test_removed_public_spellings); PAR-ORDERS and CI-FLAKE add no row. All +# eight register in release too. 286 registered, 285 run: the WebSocket row +# still skips on a system libcurl. KERNEL_MIN_TESTS = 285 # Release-row floor, the same gate for the default profile. Before lane P7 # only the kernel profile had one, so a row that left release alone (a @@ -369,10 +363,10 @@ # wave-G lane adds a source-bound row (the K-ULP4 and K-ULP5 C checks and # V19-FIX's scaling rows are rows inside existing TUs). No release row skips, # so 678 registered is 678 run. -# 690 = those 678 plus the twelve rows of R5 lane H-MEASURE (AUDIT4 X14 and -# A4-ACID-COVERAGE; INT26, summed at the pick, recounted at INT26 (iv)): the -# one KERNEL_MIN_TESTS lists above, which registers here too, plus eleven -# source-bound ones the kernel profile does not build: +# 700 = those 678 plus wave H's twenty-two (INT26), counted with ctest -N on +# the integrated tree: the eight KERNEL_MIN_TESTS lists above, which register +# here too, H-MEASURE's eleven source-bound rows the kernel profile does not +# build (AUDIT4 X14) and PAR-ORDERS-2's three tape rows: # +1 G2-09/-10/-12/-13, E20 f1 test_adapter_margin_schedule_differential # +3 G2-15, G2-18, E5/E14 test_pyramiding_count_differential, # test_zero_trail_sibling_stop, @@ -384,37 +378,13 @@ # +3 F1(e), F1 magnified, G2-23 test_aggregated_entry_bar_index_tape, # test_magnified_aggregated_tape, # test_adapter_security_route_conditions -# No release row skips, so 690 registered is 690 run. -# 691 = those 690 plus R5 lane H-DOCGATES' test_doc_reverts (see -# KERNEL_MIN_TESTS; INT26 pick, summed). -# 694 = those 693 plus the one R5 lane K-OCA-KEEP row KERNEL_MIN_TESTS lists -# above; the C replace takes no options, so the lane adds no C row (INT26 -# pick, summed). -# 695 = those 694 plus R5 lane PAR-ORDERS-2's -# test_native_current_cohort_refusal (see KERNEL_MIN_TESTS; INT26 pick, -# summed). -# 696 = those 695 plus R5 lane PAR-ORDERS-2's test_flat_coof_exit_tapes -# (source-bound; INT26 pick, summed). -# 697 = those 696 plus R5 lane PAR-ORDERS-2's test_second_extreme_order_tapes -# (source-bound; INT26 pick, summed). -# 698 = those 697 plus R5 lane PAR-ORDERS-2's test_pooc_reversing_stop_tapes -# (source-bound; INT26 pick, summed). -# 699 = those 698 plus R5 lane PAR-MARGIN's kernel row above (INT26 pick, -# summed). The lane's own count of 680 also counted H-MEASURE's -# test_adapter_margin_schedule_differential, which it had cherry-picked for its -# pins; H-MEASURE's twelve above already count that row. -# 700 = those 699 plus R5 lane PAR-MARGIN-2's kernel row above (INT26 pick, -# summed). +# +3 PAR-ORDERS-2 items 1-3 test_flat_coof_exit_tapes, +# test_second_extreme_order_tapes, +# test_pooc_reversing_stop_tapes +# PAR-MARGIN's own count also held the margin differential, which that lane +# had cherry-picked for its pins; it counts once, as H-MEASURE's. No release +# row skips, so 700 registered is 700 run. RELEASE_MIN_TESTS = 700 -# R5 lane H-DOCGATES (AUDIT4-opus perf N-7) raises both floors by one more: -# test_native_runtime_ambient_lifo, the runtime blocks' LIFO death row, is -# source-free, so it registers and runs in the kernel profile (279 -> 280) and -# in release (691 -> 692; INT26 pick, summed). Recorded below both floors so -# neither floor's line moves: CONTRIBUTING.md and contributing-llm.md cite -# them by line. -# R5 lane H-DOCGATES (AUDIT4-opus X10) raises both by one more: -# test_kernel_seam_rows, the kernel seam-row gate's self-test, is a Python row -# every profile registers (280 -> 281, 692 -> 693; INT26 pick, summed). # ADR-0001 ruled-count floors, beside the ctest floors (R5 lane H-DOCGATES, # AUDIT4-opus X13 / docs-a N7). check_kernel_residuals.py counts the rulings # its vocabulary reads -- 174 identifiers and 45 texts on the lane's tree: the @@ -435,9 +405,12 @@ # PR-only registration floors: the complete CTest populations of the three # excluded profiles at INT25, counted with ctest -N on the integrated tree -- # 653/653/662 at 91d65ad6 (INT24) plus wave G's six rows (C-SURFACE-1 +1, -# KERNEL-EDGE +1, K-ULP4 +2, K-ULP5 +1, DOC-TRUTH-4 +1) in each. An excluded -# run must still discover at least this many rows before -LE. -EXCLUDED_REGISTERED_MIN = {'debug': 659, 'sanitizers': 659, 'native': 668} +# KERNEL-EDGE +1, K-ULP4 +2, K-ULP5 +1, DOC-TRUTH-4 +1) in each; 681/681/690 +# at INT26, each with wave H's twenty-two rows of RELEASE_MIN_TESTS (H-MEASURE +# +12, H-DOCGATES +3, K-OCA-KEEP +1, PAR-ORDERS-2 +4, PAR-MARGIN +1, +# PAR-MARGIN-2 +1), counted the same way. An excluded run must still discover +# at least this many rows before -LE. +EXCLUDED_REGISTERED_MIN = {'debug': 681, 'sanitizers': 681, 'native': 690} # The ctest stage's bound. A full sanitizers run (push to main, a manual # dispatch, the maintainers' verification) ran out of its 30 minutes twice on # main's four-core runner before every row had finished, so it gets an hour; a diff --git a/scripts/test_ci_verify.py b/scripts/test_ci_verify.py index 2f3be297..b34906e3 100644 --- a/scripts/test_ci_verify.py +++ b/scripts/test_ci_verify.py @@ -1331,7 +1331,7 @@ def test_ctest_label_exclusion_is_forwarded(self): def test_pr_exclusion_proves_registered_minus_labelled_equals_ran(self): self.assertEqual(ci_verify.EXCLUDED_REGISTERED_MIN, - {'debug': 659, 'sanitizers': 659, 'native': 668}) + {'debug': 681, 'sanitizers': 681, 'native': 690}) for profile, registered in ci_verify.EXCLUDED_REGISTERED_MIN.items(): with self.subTest(profile=profile): code, summary, scripted, _ = self.run_profile( diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 77c905e7..d80c49a6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2575,6 +2575,7 @@ set(PINEFORGE_PR_SLOW_TESTS test_native_direct_mutation test_native_event_retention test_native_group_absorption + test_native_group_keep_handle test_native_handle_stable_replace test_native_host_reads test_native_lookup_memos From a3b4b97102bd2f0864cd1c5694ff248e88816b86 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 12:55:13 +0800 Subject: [PATCH 55/83] Kernel: session-day facts on a UTC calendar resolve no calendar interval per bar (R5 lane PERF-KEDGE) Wave G's KERNEL-EDGE made present_session_day read the bar's label and each neighbour at the first eligible instant of its script interval, and resolved those intervals through the calendar memo at every bar: three interval_containing calls a bar (two ring hits, one resolution). On FeedTolerant labels -- the Pine adapter's shape -- nothing else asks the calendar per bar, so the 100 public benchmark slots ran a median +11.9 % more instructions than 1a0e7ea1 (INT25 finding 1); callgrind puts all of slot 033's +1,020 Ir a bar in those calls. On a UTC calendar an instant that is itself in session reads exactly as its interval's first eligible instant does. The calendar is integer arithmetic there (no libc zone, no mktime) and its session days are the fixed daily cycles [date + origin, next date + origin), which tile time: every instant has one session day, and both the memo's candidate-date lookup and the consumer's session-day memo find it. The interval's open is that day's origin plus a whole number of buckets at or before the instant, and its first eligible instant is the first in-session instant of [open, instant] -- on that day, in session. So present_session_day reads such an instant's session point directly and resolves an interval only for an instant out of session, for the walk to the previous eligible slot and for the scheduled close (the last two only with nothing held). Every other zone issues the calendar calls in the order the base issued them, because a libc zone's mktime can answer from the previous call's offset. - native_calendar_memo.hpp / native_calendar.cpp: utc_calendar(calendar, memo), the memo's own once-per-calendar UTC decision, and SessionDayMemo::interval_resolutions(), a derived cost reading folded nowhere. - native_execution_consumer: calendar_is_utc() (cached, reset with the calendar memos); eligible_session_point() out of line, so the neighbour reading stays inlined; the bar's own interval resolved lazily. ADR-0001 rule 2: no capability, spec field or knob is added and no answer moves. The reading is the run's own calendar -- its zone is data -- the same for every venue, and nothing branches on a platform. Nothing is hashed: the facts fold into no digest and the new members are derived memos. Witness, tests/test_native_session_day_utc.cpp (kernel-only): the calendar's UTC decision; the same tapes under "UTC" and "UTC0" (the same clock through the zone path) present identical facts at every bar open, bar, fill recalculation, applied execution and print across 6 sessions, 6 timeframe pairings, both label policies, 5 tape shapes and 4 drive modes (1152 runs, 1.77M facts); and a UTC FeedTolerant batch resolves 1 interval where UTC0 resolves 529 for 528 bars. Fail-before at 0d76a099: "no member named 'interval_resolutions' in 'pineforge::native_calendar::SessionDayMemo'". Six mutants -- either fallback dropped, the walk's or the scheduled close's slot left unresolved, every zone taken for UTC, the shortcut removed -- each fail it. Floors: kernel 277 -> 278, release 678 -> 679, PR registration 659/659/668 -> 660/660/669 (ci_verify.py, its self-test pin and docs/ci.md). 24 doc anchors carried through the exact line map from 0d76a099; the 6 that cite the two rewritten floor lines set by hand (ci_verify.py:247, :357). Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 16788d057fe06407fcd39843b9368ec1ab2782d9) INT26 pick note (late pick, accepted 2026-09-26 10:30, after INT26 item iv): the kernel and test hunks applied without conflict. The floors take this lane's row on top of item (iv)'s integrated counts -- KERNEL 285 -> 286, RELEASE 700 -> 701, EXCLUDED_REGISTERED_MIN 681/681/690 -> 682/682/691 (after PAR-ORDERS-2's and PAR-MARGIN-2's picks) -- and each floor comment lists it with wave H's other rows; test_ci_verify and docs/ci.md follow (docs/ci.md's floor sentence still named PAR-MARGIN's 680/278, which item iv missed). The four floor citations name the merged lines; the pages merged on citation digits only. --- CONTRIBUTING.md | 4 +- docs/adr/0001-kernel-adapter-boundary.md | 8 +- docs/ci.md | 8 +- docs/design/native-feature-parity.md | 22 +- docs/pages/contributing-llm.md | 6 +- docs/pages/native-engine.md | 6 +- docs/pages/pine-to-native.md | 2 +- scripts/ci_verify.py | 28 +- scripts/test_ci_verify.py | 2 +- src/native_calendar.cpp | 11 + src/native_calendar_memo.hpp | 12 + src/native_execution_consumer.cpp | 50 ++- src/native_execution_consumer.hpp | 13 + tests/CMakeLists.txt | 5 + tests/test_native_session_day_utc.cpp | 485 +++++++++++++++++++++++ 15 files changed, 612 insertions(+), 50 deletions(-) create mode 100644 tests/test_native_session_day_utc.cpp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a9a1505..0bace448 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -152,8 +152,8 @@ What each gate refuses: ### The floors `ci_verify.py` counts the CTest rows that actually **ran** and fails below a -floor — `KERNEL_MIN_TESTS` ci_verify.py:258 and `RELEASE_MIN_TESTS` -ci_verify.py:387. +floor — `KERNEL_MIN_TESTS` ci_verify.py:260 and `RELEASE_MIN_TESTS` +ci_verify.py:389. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index ed78370f..cbe80ea3 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -726,7 +726,7 @@ number names its symbol on this tree. | `strategy_configure_native_v1` (`c_abi.cpp:896`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:3982`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | | `strategy_configure_native_fx_curve_v1` (`pineforge.h:584`) | **typed additive route** | `strategy_configure_native_fx_curve_ext_v1` writes the C twin of `NativeFxCurveValidation` and its offending index. The old `-1` route remains unchanged, while a C caller can now distinguish a decreasing timestamp, non-positive rate, allocation failure or wrong phase. | | `on_native_timeframe_bar` (`native_host.hpp:885`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:809`) | **query routed** | The callback signature stays frozen. `strategy_native_timeframe_bar_interval_v1` is valid only during that callback and copies the kernel calendar interval without deriving it from the delivered bar or delivery timestamp. | -| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7665-7677`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1085-1090`) sees the same values, so no hash contract moves. | +| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7663-7675`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1085-1090`) sees the same values, so no hash contract moves. | | `pf_native_lot_excursion_v1` (`native_c_api.h:1876`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | ## TradingView-calibrated kernel mechanisms (rule 2's own account) @@ -784,7 +784,7 @@ The table records, at the tree the doc wave read, what each block described and | `ta::ATR` `include/pineforge/ta.hpp:321` (the `TradingView` comment `sha256:4f6f6939aec2773684b114a13dd002eb58990d6d96c3056ad87361753faf7a0a` :333-340) | average true range over Wilder's `RMA` | Wilder's ATR is the textbook definition; the comment records which convention and the tape that pinned the recompute behaviour | a seeding/averaging field on an indicator whose definition is the convention | `tests/test_ta_rma_warmup.cpp`, `tests/test_recompute.cpp` | | `ta::StdDev` `include/pineforge/ta.hpp:430` (the `TradingView` comment `sha256:7cc1bbcd32dfe673285d51a8ca7080890ac551858099b523480d998f46408666` :420-429) | the population (biased) standard deviation | biased vs sample is a textbook choice, and the kernel states which | a per-instance switch is the *right* answer where two conventions genuinely compete, which is why `ta::EmaSeeding` exists and this one does not need it | `tests/test_ta_indicators_extras.cpp` | | `float_band_eq` `include/pineforge/ta_compare_band.hpp:38` | an absolute 1e-10 float comparison for indicator equality | an epsilon band is needed by any float comparison; the width is the calibration | a band-width field on a comparison the indicators share | `tests/test_dmi_parity.cpp`, `tests/test_ta_osc_edge.cpp` | -| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8639-8642` (lookahead) and `clear_if_gapped` `:8633-8636` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | +| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8621-8624` (lookahead) and `clear_if_gapped` `:8615-8618` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | | `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2235`, `::gaps` `include/pineforge/native_c_api.h:2239` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:795`, `pf_native_gaps_e` `:806`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | | the authoritative-feed partition `src/engine_aux_security.cpp` | a feed is the venue's own bars of one timeframe; its stamps are the period partition | already ruled: design §2.ii row s, and the residual table's own row | the policy knob is installing the feed or not | `tests/test_native_htf_subscriptions.cpp`, `tests/test_native_security_feed.cpp` | | `session_template_knows_early_close` `src/engine_security.cpp` | the instrument-class classification | already ruled: design §2.ii row t; `SymInfo::type`'s vocabulary is fixed by the frozen C ABI | — | `tests/test_native_wm_buckets.cpp` | @@ -799,8 +799,8 @@ The table records, at the tree the doc wave read, what each block described and | the formatting library -- `include/pineforge/str_utils.hpp`, `src/str_utils.cpp` (`str_tostring`, `str_format_values`, `str_format`) | the number text of Pine's `str.tostring` / `str.format` built-ins: a number's shortest round-trip decimal spelling, scaled and rounded half-up on its digits, the default, `percent`, `volume` (K / M / B / T) and pattern modes, MessageFormat placeholders and quoting, `NaN` and `Infinity` -- the published rendering TradingView's exported tapes pin (R5 lane C6), ported from the codegen formatter (lane C6b) by R5 lane B-ENGINE; the `mintick` mode, which generated code delegates to it, is kept as it was | a formatting library is a function of its inputs, the same for every venue, as the indicator library is: it spells a number the way the source language's own `str.*` library defines, and no kernel order, fill, settlement, margin or hash code calls it -- of the thirty-five kernel translation units only `src/str_utils.cpp` names these functions | a rendering switch on functions whose definition the calling script chose by calling them | `tests/test_str_number_format.cpp` (lane C6b's 42-case edge battery, every numeric string of lane C6's TradingView exports, byte-identical output on macOS arm64 and Linux x86-64) and `tests/test_str_utils.cpp` | | the language calendar functions -- `include/pineforge/session_time.hpp`, `src/session_time.cpp` (`timeframe_time`, `local_hour` and its siblings, `session_in_market`, `session_trading_day_open_ms`, ...) | the definitions of Pine's `time()` / `time_close()` / bare-time / `session.is*` built-ins for generated code: session and timezone parsing, the invalid- and 24-hour-session spellings, `time("60")` day keying, `session.is*` on daily charts, each comment naming the tape that pinned it | the inputs are data -- a session string, a timezone, a timeframe -- so another venue is driven by its own session template; nothing branches on a platform, and a native host's calendar is the separate generic `native_calendar` | a rules switch on functions whose definition the generated script chose by calling them | `tests/test_session_time.cpp`, `tests/test_pine_time_day_stamp_grid.cpp` and the corpus parity gate | | `present_session_day` `src/native_execution_consumer.cpp:7286`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (`tests/fixtures/session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | `tests/test_native_session_day_facts.cpp` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), the C scenario of `tests/test_native_c_api.c`, and `tests/test_session_day_facts_adapter.cpp` (the adapter selects them flag for flag) | -| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7910-7926` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | -| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7887-7908` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | +| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7927-7943` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | +| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7904-7925` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | | `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6618` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | | `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:6342` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | | the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:334`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1541`, `:1556`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:1854`). A bare host never calls the builder, so its calendar stays nominal | `tests/test_o_close_pct_day_anchor.cpp` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | diff --git a/docs/ci.md b/docs/ci.md index 1f50d8ab..ff87d703 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -100,10 +100,10 @@ requires the rows that actually ran to equal the second count, and records `registered`, `labelled = registered - selected`, and `ran` in `ctest-exclusion.log` and `ci-summary.json`. A skip, missing executable, lost registration below the PR registration floor, unreadable enumeration, or -missing label fails. The PR registration floors at INT25 are 659 for Debug -and sanitizers and 668 for native (653 and 662 at the INT24 base, plus wave G's -six rows). The full-run release and kernel floors are 680 and 278 rows that -ran; full runs do not exclude a label. +missing label fails. The PR registration floors at INT26 are 682 for Debug +and sanitizers and 691 for native (653 and 662 at the INT24 base, plus wave G's +six rows and wave H's twenty-three). The full-run release and kernel floors are +701 and 286 rows that ran; full runs do not exclude a label. Preflight also runs the detached-comment census of the kernel compile closure (`detached-comments`: `scripts/measure_detached_comments.py --check-ceiling`) diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 0ad9c37e..1824b5ab 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -116,13 +116,13 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7542-7546 | K | — | F:E2 O:F6 S:C1 | — | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7540-7544 | K | — | F:E2 O:F6 S:C1 | — | | CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16206) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `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:7027 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7078 (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:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5635, `flush_coof_tail` pine_adapter.cpp:16349, `end_coof_recalc` pine_adapter.cpp:5656) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7166-7170; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | -| 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:7536-7655 **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 | — | +| 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:7534-7653 **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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:234-320) 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:5782), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1660-1667 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | @@ -136,7 +136,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:637) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:928 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | | HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:21-220, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:241-413; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:515) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:255-336) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | -| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:875; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7910-7927, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7917, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | +| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:875; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7906-7923, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7913, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | ### 1.7 Fill price, slippage, commission, FX @@ -148,7 +148,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11997-12006, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4073; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6363-6364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) 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:8739). **Closed by audit lane N6:** the declared curve is the run's immutable FX epoch and a confirmed-bar stream runs under it exactly as a batch does (stream twin in `tests/test_native_margin_fx_roll.cpp`); no realtime rate ingestion and no tick-driven input under a curve are stated as permanent limits with their reasons (native-engine.md, "Stream FX"). **Re-examined by R5 follow-up lane E3 and KEPT:** the tick refusal was never only about the margin check — it guards the SCRIPT-VISIBLE conversions (`open_trade_profit()`, `marked_equity()`, the equity curve), which read `active_account_currency_fx()`, i.e. the presented clock the observation hook has not yet moved (re-measured: a step at T+90s left the T+100s and T+110s prints converting at the pre-step rate). E3's cursor rule reaches the margin model only, so the refusal stands and is pinned in `tests/test_native_margin_fx_clock.cpp` | K | unassigned → N6 (E3: refusal kept) | F:D9 S:F4 | **Disagree:** F out of scope ("O7 clock") / S its P10, a stream-safe FX epoch. No R5 lane → §4 U4 (answered by N6: S's "same curve in batch and stream", without a new clock or callback). | +| FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1202 in batch; the stream then refused a non-empty curve (today only tick-driven input is refused under one, `refuse_fx_curve_tick_input` native_execution_consumer.cpp:8721). **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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9106-9111, `exit_limit_trigger` pine_adapter.cpp:9640-9661), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11757) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -161,8 +161,8 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:219; `fill_metrics_section` engine_report.cpp:119-139 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | | RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1684) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1409) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | -| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9313, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | -| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1370, native_execution_consumer.cpp:10715; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | +| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9286, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | +| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1370, native_execution_consumer.cpp:10672; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | | RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:538; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1931 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:411 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:405 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:411 is its deprecated spelling, which the generic default forwards to (`tests/test_native_host_hash_extension.cpp`). | K | L2 (the virtual rides the shared bump) | F:I3 O:J1 S:X3 | — | @@ -204,7 +204,7 @@ 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:82-91) and of the rich `BacktestEngine::run` overload (engine.hpp:1780-1785, 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:292-295 | 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) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:29 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:356-359 (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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8800`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2309-2310. 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:260-261. **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:741) | 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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8782`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2309-2310. 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:260-261. **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:741) | 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:454-460) | 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:6434 | 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:1774 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) | @@ -281,7 +281,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the | | F (its §2.iv A) | O (its G10) | S (its P7) | Chosen | |---|---|---|---|---| | API shape | spec `subscriptions` + push callback `on_native_timeframe_bar` | `register_native_series` + pull `native_series_bar(id)` / `native_series_slot_is_new(id)` | spec `security_requests` + `configure_native_security_feed` + `native_security(id)` view | F's subscription in the spec + push callback, plus O's pull accessor | -| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:656-671, `contribute_input` native_execution_consumer.cpp:8046, `seal_script` native_execution_consumer.cpp:8011) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | +| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:656-671, `contribute_input` native_execution_consumer.cpp:8030, `seal_script` native_execution_consumer.cpp:8007) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | | Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7116), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | | Authoritative bars | `authoritative_bars` on the subscription | `Source::ExchangeFeed` through `set_native_security_feed` | `configure_native_security_feed(id, bars, tf)` | F's field, installed through the existing store | | Lookahead | none, by construction | `lookahead = false` default, available | `lookahead_on = false`, `gaps_on = true` | open → §4 U1 | @@ -341,7 +341,7 @@ the campaign's own anchor audit found that the first thing to rot: | the request-epoch sentence | `native_order_v4` | `native_order_v7` native_order.hpp:25 | | the two host-epoch sentences | host epoch v16 | `engine_script_run_v19` native_host.hpp:20 | | "What still requires Pine compatibility to build" | `BacktestEngine` still holds `CapAttachment` / `OrderPriority` / `IntradayCap` | `engine.hpp` has zero references to them; the cap type is `CapAttachment` intraday_cap.hpp:18 and the constructor is `PineStrategyHost`'s pine_strategy_host.hpp:244 | -| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9183-9223 shows no such refusal (still unpinned by a test; the page says so) | +| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9156-9196 shows no such refusal (still unpinned by a test; the page says so) | | added | — | the self-aggregation recipe beside the subscriptions it is an alternative to; the open-bar lookahead warning with `current_partial_bar` native_host.hpp:1073; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -350,7 +350,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:2205-2338) 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:1647-1651). 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 Cloud Run campaign sweep on the final tree (fixed population, 4190 probes, zero tolerance on the hard band — native-refactor-progress.md:41-47; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:139-140). 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:2205-2338) 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:1654-1658). 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 Cloud Run campaign sweep on the final tree (fixed population, 4190 probes, zero tolerance on the hard band — native-refactor-progress.md:41-47; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:139-140). 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 drive the adapter and 14 the native host (O), 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. @@ -1445,7 +1445,7 @@ Callback chronology contract (the normative part of the lane; F P4.1 + O G8): 1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1697-1698) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6995-7007) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). 2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5629-5633) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1660-1667) is calibrated against it. -3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1073-1080, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6848-6855). `on_native_tick` stays the observation hook (S). +3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1083-1090, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6848-6855). `on_native_tick` stays the observation hook (S). 4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2553); valid in bar-open, applied and tick callbacks. 5. The kernel never attempts language-state rollback; that stays Pine's (`snapshot / restore / commit_coof_script_state`, pine_scheduler_native.cpp:52-104) (O). diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 4eb528a1..d22a2043 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -117,8 +117,8 @@ finding to report, not a step to take. `scripts/check_doc_reverts.py`. 10. **A test row never silently disappears.** Each profile counts the rows that - *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:258 and - `RELEASE_MIN_TESTS` ci_verify.py:387. Adding rows means raising the floor + *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:260 and + `RELEASE_MIN_TESTS` ci_verify.py:389. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -264,7 +264,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:258, `RELEASE_MIN_TESTS` ci_verify.py:387). It +(`KERNEL_MIN_TESTS` ci_verify.py:260, `RELEASE_MIN_TESTS` ci_verify.py:389). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 4c551312..6f44a43f 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -463,7 +463,7 @@ spec fields. The rich `run(bars, n, input_tf, script_tf, inputs, syminfo, overrides, …)` overload (`engine.hpp:1623-1634`) is **not** refused as a source mutation: it reaches `NativeExecutionConsumer::run_rich` -(`native_execution_consumer.cpp:9183-9223`), which admits the begin, checks the +(`native_execution_consumer.cpp:9156-9196`), which admits the begin, checks the timeframe arguments against the spec, preflights and pumps the batch exactly like the plain overload. `inputs` / `syminfo` / `overrides` are carried only as `NativeBeginArgs` fields to `prepare_native_begin` — the overrides as the @@ -2098,7 +2098,7 @@ default, set while no run is active — because each row is a full the live state, not the run's length: the closed rows enter through a running digest). With the switch on, one row follows each point, after the extremes that point just folded -(`record_script_report_point`, `native_execution_consumer.cpp:7835`), so +(`record_script_report_point`, `native_execution_consumer.cpp:7833`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -2909,7 +2909,7 @@ Only completed buckets are published, so this recipe has no lookahead by construction. It is the same class the kernel's own subscription evaluator aggregates with, and the one the kernel's `script_bucket_completions` query feeds when the Pine scheduler asks how its input span buckets -(`TimeframeAggregator` `native_execution_consumer.cpp:7917`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:7913`). What it does **not** give you is what a declared subscription does: an `authoritative_bars` feed, the `gaps` and `lookahead` delivery rules, the lazy-seal chronology, a C spelling, and the diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 2204e073..f98ff542 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -127,7 +127,7 @@ rather than defaulted. | `commission_type` | `NativeFeeKind` native_run_spec.hpp:19 | `fee_kind` pineforge.h:540 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | | `commission_value` | `fee_value` native_run_spec.hpp:590 | `fee_value` pineforge.h:540 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | | `slippage` | `slippage_ticks` native_run_spec.hpp:584 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3971-3972. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7542-7543, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7540-7541, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | | `pyramiding` | `max_open_lots` native_run_spec.hpp:596 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:2914). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). | | `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:344 with `size_basis` native_c_api.h:2111 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | | `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2115 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index eb284e40..d16c3332 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -239,7 +239,7 @@ # extracted from the page and run) # All six register in release too; RATIO-HARDEN, V19-FIX and K-IDX add no row. # 278 registered, 277 run: the WebSocket row still skips on a system libcurl. -# 285 run = those 277 plus the eight source-free rows of wave H (INT26), +# 286 run = those 277 plus the nine source-free rows of wave H (INT26), # counted with ctest -N on the integrated tree: # +1 H-MEASURE test_native_acid_composite (the G1 acid composite, C++ # and C) @@ -251,11 +251,13 @@ # +1 PAR-ORDERS-2 test_native_current_cohort_refusal # +1 PAR-MARGIN test_native_margin_intrabar_samples # +1 PAR-MARGIN-2 test_native_margin_post_fill_path +# +1 PERF-KEDGE test_native_session_day_utc (session-day facts on a UTC +# calendar without a calendar interval per bar) # REL10 renames F6's row (test_deprecated_public_spellings -> # test_removed_public_spellings); PAR-ORDERS and CI-FLAKE add no row. All -# eight register in release too. 286 registered, 285 run: the WebSocket row +# nine register in release too. 287 registered, 286 run: the WebSocket row # still skips on a system libcurl. -KERNEL_MIN_TESTS = 285 +KERNEL_MIN_TESTS = 286 # Release-row floor, the same gate for the default profile. Before lane P7 # only the kernel profile had one, so a row that left release alone (a # source-bound TU dropped from TEST_SOURCES, a deleted twin or ABI row) left a @@ -363,8 +365,8 @@ # wave-G lane adds a source-bound row (the K-ULP4 and K-ULP5 C checks and # V19-FIX's scaling rows are rows inside existing TUs). No release row skips, # so 678 registered is 678 run. -# 700 = those 678 plus wave H's twenty-two (INT26), counted with ctest -N on -# the integrated tree: the eight KERNEL_MIN_TESTS lists above, which register +# 701 = those 678 plus wave H's twenty-three (INT26), counted with ctest -N on +# the integrated tree: the nine KERNEL_MIN_TESTS lists above, which register # here too, H-MEASURE's eleven source-bound rows the kernel profile does not # build (AUDIT4 X14) and PAR-ORDERS-2's three tape rows: # +1 G2-09/-10/-12/-13, E20 f1 test_adapter_margin_schedule_differential @@ -383,8 +385,8 @@ # test_pooc_reversing_stop_tapes # PAR-MARGIN's own count also held the margin differential, which that lane # had cherry-picked for its pins; it counts once, as H-MEASURE's. No release -# row skips, so 700 registered is 700 run. -RELEASE_MIN_TESTS = 700 +# row skips, so 701 registered is 701 run. +RELEASE_MIN_TESTS = 701 # ADR-0001 ruled-count floors, beside the ctest floors (R5 lane H-DOCGATES, # AUDIT4-opus X13 / docs-a N7). check_kernel_residuals.py counts the rulings # its vocabulary reads -- 174 identifiers and 45 texts on the lane's tree: the @@ -405,12 +407,12 @@ # PR-only registration floors: the complete CTest populations of the three # excluded profiles at INT25, counted with ctest -N on the integrated tree -- # 653/653/662 at 91d65ad6 (INT24) plus wave G's six rows (C-SURFACE-1 +1, -# KERNEL-EDGE +1, K-ULP4 +2, K-ULP5 +1, DOC-TRUTH-4 +1) in each; 681/681/690 -# at INT26, each with wave H's twenty-two rows of RELEASE_MIN_TESTS (H-MEASURE -# +12, H-DOCGATES +3, K-OCA-KEEP +1, PAR-ORDERS-2 +4, PAR-MARGIN +1, -# PAR-MARGIN-2 +1), counted the same way. An excluded run must still discover -# at least this many rows before -LE. -EXCLUDED_REGISTERED_MIN = {'debug': 681, 'sanitizers': 681, 'native': 690} +# KERNEL-EDGE +1, K-ULP4 +2, K-ULP5 +1, DOC-TRUTH-4 +1) in each; 682/682/691 +# at INT26, each with wave H's twenty-three rows of RELEASE_MIN_TESTS +# (H-MEASURE +12, H-DOCGATES +3, K-OCA-KEEP +1, PAR-ORDERS-2 +4, PAR-MARGIN +# +1, PAR-MARGIN-2 +1, PERF-KEDGE +1), counted the same way. An excluded run +# must still discover at least this many rows before -LE. +EXCLUDED_REGISTERED_MIN = {'debug': 682, 'sanitizers': 682, 'native': 691} # The ctest stage's bound. A full sanitizers run (push to main, a manual # dispatch, the maintainers' verification) ran out of its 30 minutes twice on # main's four-core runner before every row had finished, so it gets an hour; a diff --git a/scripts/test_ci_verify.py b/scripts/test_ci_verify.py index b34906e3..b8267da1 100644 --- a/scripts/test_ci_verify.py +++ b/scripts/test_ci_verify.py @@ -1331,7 +1331,7 @@ def test_ctest_label_exclusion_is_forwarded(self): def test_pr_exclusion_proves_registered_minus_labelled_equals_ran(self): self.assertEqual(ci_verify.EXCLUDED_REGISTERED_MIN, - {'debug': 681, 'sanitizers': 681, 'native': 690}) + {'debug': 682, 'sanitizers': 682, 'native': 691}) for profile, registered in ci_verify.EXCLUDED_REGISTERED_MIN.items(): with self.subTest(profile=profile): code, summary, scripted, _ = self.run_profile( diff --git a/src/native_calendar.cpp b/src/native_calendar.cpp index da90b88c..7d479050 100644 --- a/src/native_calendar.cpp +++ b/src/native_calendar.cpp @@ -1597,6 +1597,8 @@ struct SessionDayMemo::State { std::size_t next_local = 0; std::array intervals{}; std::size_t next_interval = 0; + // Intervals resolved rather than answered from `intervals`. + std::uint64_t interval_resolutions = 0; }; namespace { @@ -1893,6 +1895,10 @@ void SessionDayMemo::reset() noexcept { calendar_ = nullptr; } +std::uint64_t SessionDayMemo::interval_resolutions() const noexcept { + return state_ ? state_->interval_resolutions : 0; +} + SessionDayMemo::State& SessionDayMemo::bind(const SessionCalendar& calendar) { auto fresh = std::make_unique(); fresh->valid = calendar.valid(); @@ -1943,6 +1949,7 @@ std::optional interval_containing(const SessionCalendar& calenda return slot.interval; } } + ++m.interval_resolutions; auto interval = memo_interval_containing(calendar, script_tf, input_tf, ms, m); MemoState::IntervalSlot& slot = m.intervals[m.next_interval]; m.next_interval = (m.next_interval + 1) % MemoState::kIntervalSlots; @@ -1989,5 +1996,9 @@ bool in_session(const SessionCalendar& calendar, int64_t ms, SessionDayMemo& mem return ms_in_spans(day->spans, ms); } +bool utc_calendar(const SessionCalendar& calendar, SessionDayMemo& memo) { + return memo.state(calendar).utc; +} + } // inline namespace native_calendar_v2 } // namespace pineforge::native_calendar diff --git a/src/native_calendar_memo.hpp b/src/native_calendar_memo.hpp index af4d5d69..35accfe6 100644 --- a/src/native_calendar_memo.hpp +++ b/src/native_calendar_memo.hpp @@ -77,6 +77,11 @@ class SessionDayMemo { return bind(calendar); } + // How many intervals the memo has resolved since it was last reset, as + // opposed to answered from the last few it holds: a cost reading, derived + // like everything else here and folded nowhere (R5 lane PERF-KEDGE). + std::uint64_t interval_resolutions() const noexcept; + private: State& bind(const SessionCalendar& calendar); @@ -107,5 +112,12 @@ std::optional session_day_at(const SessionCalendar& calendar, // Value for value what in_session(calendar, ms) answers. bool in_session(const SessionCalendar& calendar, int64_t ms, SessionDayMemo& memo); +// Whether `calendar`'s zone is UTC, as the memo decided once for it and the +// memo-free calendar decides at every resolution. A UTC calendar is integer +// arithmetic throughout (no libc zone, no mktime): its session days are the +// fixed daily cycles [date + origin, next date + origin), which tile time, so +// every instant has exactly one session day and every lookup finds it. +bool utc_calendar(const SessionCalendar& calendar, SessionDayMemo& memo); + } // inline namespace native_calendar_v2 } // namespace pineforge::native_calendar diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index bd2aab09..3cb20e63 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -7314,18 +7314,40 @@ void NativeExecutionConsumer::present_session_day(NativeDecisionContext& context // interval can reopen at 13:30 while its grid label is 13:00). Resolve // its first eligible instant and next input opening from the calendar, // even when a FeedTolerant host partitions by raw provider labels. - const auto slot = native_calendar::interval_containing( - calendar_, script_tf_, input_tf_, label, calendar_memo_); - const int64_t here_ms = slot ? slot->eligible_open_ms : label; - const SessionPoint here = session_point(here_ms); + // + // R5 lane PERF-KEDGE: on a UTC calendar an instant that is itself in + // session reads exactly as its interval's first eligible instant does, so + // neither the bar's interval nor a neighbour's is resolved for it. The + // day's cycles tile (utc_calendar), so the instant, its interval's open + // and that first eligible instant share the one session day holding the + // instant: the eligible instant is the first in-session instant of + // [open, instant], in session on that day, and every session point of + // them is that day's. Only an instant out of session, the walk and the + // scheduled close below resolve an interval. Any other zone asks the + // calendar in the order it always did, because a libc zone's mktime + // answer can depend on what was asked before it. + const bool utc = calendar_is_utc(); + std::optional slot; + bool slot_resolved = false; + const auto resolve_slot = [&] { + if (slot_resolved) return; + slot = native_calendar::interval_containing( + calendar_, script_tf_, input_tf_, label, calendar_memo_); + slot_resolved = true; + }; + SessionPoint here; + if (utc) here = session_point(label); + if (!here.in_session) { + resolve_slot(); + here = session_point(slot ? slot->eligible_open_ms : label); + } if (!here.in_session) return; context.in_session = true; // In session on this bar's own session day. const auto same_day = [&](int64_t other) { - const auto other_slot = native_calendar::interval_containing( - calendar_, script_tf_, input_tf_, other, calendar_memo_); - const SessionPoint there = session_point( - other_slot ? other_slot->eligible_open_ms : other); + SessionPoint there; + if (utc) there = session_point(other); + if (!there.in_session) there = eligible_session_point(other); return there.in_session && there.ordinal && here.ordinal && *there.ordinal == *here.ordinal; }; @@ -7343,12 +7365,14 @@ void NativeExecutionConsumer::present_session_day(NativeDecisionContext& context ? std::optional(pumped_last_label_) : pumped_script_label(script_.first_index - 1); } + if (!run_start && !before) resolve_slot(); if (!run_start && !before && slot) { // A previous eligible input slot ON THIS session day suffices to // establish that this bar does not open it. If none exists, the // predecessor is on another day (or absent). Walk this day's spans // backwards, skipping any declared closed window, then resolve the // slot at the last eligible instant before this script interval. + const int64_t here_ms = slot->eligible_open_ms; if (!session_day_memo_ || !session_day_memo_->holds(here_ms)) { session_day_memo_ = native_calendar::session_day_at( calendar_, here_ms, calendar_memo_); @@ -7388,12 +7412,22 @@ void NativeExecutionConsumer::present_session_day(NativeDecisionContext& context } else { // Nothing held after the bar: the calendar's next eligible input // slot, which jumps across a declared break without ending the day. + resolve_slot(); const bool scheduled = slot && !same_day(slot->next_input_open_ms); context.closes_session_day = run_end || scheduled; context.closes_session_day_open_ended = scheduled; } } +// Out of line, so the neighbour reading present_session_day inlines stays the +// session point alone on its UTC shortcut. +[[gnu::noinline]] NativeExecutionConsumer::SessionPoint +NativeExecutionConsumer::eligible_session_point(int64_t ms) const { + const auto slot = native_calendar::interval_containing( + calendar_, script_tf_, input_tf_, ms, calendar_memo_); + return session_point(slot ? slot->eligible_open_ms : ms); +} + // (in session, session-day ordinal) of one instant, through the memo of the // session day last read: consecutive bars share a day, so a day is resolved // once. A day the calendar cannot key is no session at all, exactly as diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 5da095ba..3a4809c7 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -1015,6 +1015,16 @@ class NativeExecutionConsumer final : public IExecutionConsumer { return session_point_resolved(ms); } SessionPoint session_point_resolved(int64_t ms) const; + // The session point of the first eligible instant of the script interval + // holding `ms` (`ms` itself when the calendar has no such interval): what + // present_session_day reads for an instant, the interval resolved first. + SessionPoint eligible_session_point(int64_t ms) const; + // Whether calendar_ is UTC (native_calendar::utc_calendar), asked once + // per calendar: present_session_day reads it at every bar. + bool calendar_is_utc() const { + if (!calendar_utc_) calendar_utc_ = native_calendar::utc_calendar(calendar_, calendar_memo_); + return *calendar_utc_; + } struct SessionPointMemo { bool held = false; int64_t ms = 0; @@ -1540,6 +1550,8 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // folded; a point whose resolution threw is never held. mutable std::array session_points_{}; mutable std::size_t session_point_next_ = 0; + // calendar_is_utc()'s answer for calendar_. Derived, never folded. + mutable std::optional calendar_utc_; // Forget every memo over the calendar and the lookups it keys: called // wherever calendar_ is rebuilt. void reset_calendar_memos() const noexcept { @@ -1547,6 +1559,7 @@ class NativeExecutionConsumer final : public IExecutionConsumer { calendar_memo_.reset(); session_points_ = {}; session_point_next_ = 0; + calendar_utc_.reset(); interval_cache_.forget_priors(); } Bar forming_{}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d80c49a6..e8345357 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -867,6 +867,11 @@ list(APPEND TEST_SOURCES test_native_margin_intrabar_samples) # bar's low) is in the scan. Source-free, so it also runs in the kernel-only # profile. list(APPEND TEST_SOURCES test_native_margin_post_fill_path) +# R5 PERF-KEDGE: on a UTC calendar the session-day facts read an in-session +# instant without resolving its interval -- the zone path's facts at every +# decision point, and the per-bar resolution cost pinned. Source-free, so it +# also runs in the kernel-only profile. +list(APPEND TEST_SOURCES test_native_session_day_utc) # R5 F5: the Pine adapter selects those facts instead of computing its own: # the same tapes flag for flag through the adapter, its live-probe tail diff --git a/tests/test_native_session_day_utc.cpp b/tests/test_native_session_day_utc.cpp new file mode 100644 index 00000000..24221404 --- /dev/null +++ b/tests/test_native_session_day_utc.cpp @@ -0,0 +1,485 @@ +// R5 lane PERF-KEDGE: session-day facts on a UTC calendar, without resolving +// a calendar interval per bar. +// +// Wave G's KERNEL-EDGE made present_session_day read each instant -- the +// bar's label and its neighbours -- at the first eligible instant of its +// script interval, and resolved that interval from the calendar at every +// bar. On FeedTolerant labels nothing else in the run asks the calendar for +// an interval, so the Pine adapter's 100 public benchmark slots ran a median +// 11.9 % more instructions (INT25, finding 1). Lane PERF-KEDGE reads an +// instant that is itself in session directly when the calendar is UTC: its +// session days are the fixed daily cycles, which tile time, so the instant, +// its interval's open and that first eligible instant share the one session +// day holding the instant, and the eligible instant is in session on it. An +// instant out of session, and every instant of any other zone, is still read +// through its interval, in the order the lane's base read it. +// +// This witness checks that the shortcut changes no fact and keeps the +// calendar off the per-bar path: +// 1. utc_calendar is the calendar's own UTC decision: "", UTC, Etc/UTC, +// GMT, Etc/GMT and the zero offsets that normalise to UTC are UTC; the +// POSIX zone UTC0 -- the same clock through the libc zone path -- a +// fixed offset and named zones are not. +// 2. The same tapes under "UTC" (the shortcut) and "UTC0" (every interval +// resolved, as at the lane's base) present identical facts at every +// decision point a host reads them at -- bar open, bar, fill +// recalculation, applied execution and realtime print -- across 24x7, +// plain, lunch-break (a 60-minute label inside the break reopens at +// 12:30), overnight, weekday-masked and late-origin sessions, script +// timeframes over finer inputs, Canonical and FeedTolerant labels with +// gaps, off-grid provider stamps and stamps inside a declared break, +// batch runs, confirmed-bar streams ended both ways and printed streams. +// Both runs must also end with the same error text (usually none). +// 3. Cost: a UTC FeedTolerant batch -- the Pine adapter's shape -- resolves +// at most one interval for its facts (its final bar's scheduled close), +// where the UTC0 run of the same bars resolves at least one a bar, which +// is what every UTC run cost at the lane's base. +// +// Fail-before: at the lane's base native_calendar::utc_calendar and +// SessionDayMemo::interval_resolutions do not exist, so this TU does not +// compile there (the lane report records the first diagnostic). +// +// Source-free: this TU runs in the kernel-only profile. +#include "../src/native_calendar_memo.hpp" +#include "../src/native_execution_consumer.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace pineforge { +inline namespace engine_script_run_v19 { + +struct NativeExecutionConsumerProbe { + static std::uint64_t interval_resolutions(const BacktestEngine& host) { + return NativeExecutionConsumer::bound(host).calendar_memo_.interval_resolutions(); + } + static bool calendar_is_utc(const BacktestEngine& host) { + return NativeExecutionConsumer::bound(host).calendar_is_utc(); + } +}; + +} // inline namespace engine_script_run_v19 +} // namespace pineforge + +using namespace pineforge; +namespace nc = pineforge::native_calendar; +namespace no = pineforge::native_order; +using Probe = NativeExecutionConsumerProbe; + +namespace { + +int passed = 0; +int failed = 0; + +#define CHECK(cond) \ + do { \ + if (cond) { \ + ++passed; \ + } else { \ + ++failed; \ + std::fprintf(stderr, "CHECK FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); \ + } \ + } while (0) + +constexpr std::int64_t kSecond = 1000; +constexpr std::int64_t kMinute = 60 * kSecond; +constexpr std::int64_t kDay = 1440 * kMinute; +// Thursday 2025-07-03 00:00 UTC: the tapes run Thursday through Tuesday, so +// every one crosses a weekend and a weekday mask has days to drop. +constexpr std::int64_t kThu = 1751500800000LL; + +// ── 1. the UTC decision ──────────────────────────────────────────────────── + +void test_utc_decision() { + std::printf("test_utc_decision\n"); + for (const char* zone : {"", "UTC", "Etc/UTC", "GMT", "Etc/GMT", "UTC+0", "UTC-00:00"}) { + const auto calendar = nc::parse_session("24x7", zone); + CHECK(calendar.has_value()); + if (!calendar) continue; + nc::SessionDayMemo memo; + CHECK(nc::utc_calendar(*calendar, memo)); + } + for (const char* zone : {"UTC0", "GMT0", "UTC+3", "UTC-05:30", "America/New_York", + "Asia/Tokyo", "Europe/London"}) { + const auto calendar = nc::parse_session("24x7", zone); + CHECK(calendar.has_value()); + if (!calendar) continue; + nc::SessionDayMemo memo; + CHECK(!nc::utc_calendar(*calendar, memo)); + } +} + +// ── 2. the same facts, UTC against UTC0 ──────────────────────────────────── + +struct Facts { + std::int64_t ts = 0; + bool in = false; + bool opens = false; + bool closes = false; + bool open_ended = false; + bool operator==(const Facts& o) const { + return ts == o.ts && in == o.in && opens == o.opens && closes == o.closes + && open_ended == o.open_ended; + } +}; + +Facts facts_of(const NativeDecisionContext& c) { + return {c.script_bar_open_ms, c.in_session, c.opens_session_day, c.closes_session_day, + c.closes_session_day_open_ended}; +} + +class Host final : public NativeStrategyHost { +public: + std::vector bar_opens, bars, refills, applied, ticks; + int calculations = 0; + void on_native_bar_open(const Bar&, const NativeDecisionContext& c) override { + bar_opens.push_back(facts_of(c)); + } + void on_native_bar(const Bar&, const NativeDecisionContext& c) override { + bars.push_back(facts_of(c)); + // A market order every fifth calculation, alternating sides, so fills + // and their recalculations read facts too. + if (++calculations % 5 == 0) { + const double units = (calculations / 5) % 2 ? 1.0 : -1.0; + (void)submit(no::Request{no::Transact{units}, "utc-" + std::to_string(calculations), ""}); + } + } + void on_native_recalculate(const Bar& bar, const NativeDecisionContext& c, + NativeCalculationReason reason, + const no::ExecutionAppliedEvent* cause) override { + if (reason == NativeCalculationReason::OrderFill) { + refills.push_back(facts_of(c)); + return; + } + NativeStrategyHost::on_native_recalculate(bar, c, reason, cause); + } + void on_native_applied(const no::ExecutionAppliedEvent&, const NativeDecisionContext& c) override { + applied.push_back(facts_of(c)); + } + void on_native_tick(const Bar&, const NativeTickContext& c) override { + ticks.push_back(facts_of(c.decision)); + } +}; + +struct Session { + const char* text; + const char* name; +}; + +// Every shape the facts read differently: a whole day, a plain window, a +// declared break, a session crossing midnight, a weekday mask, and an origin +// half an hour before midnight. +const Session kSessions[] = { + {"24x7", "24x7"}, + {"0930-1600", "plain"}, + {"0900-1130,1230-1500", "break"}, + {"1700-1600", "overnight"}, + {"0930-1600:23456", "weekdays"}, + {"2330-2300", "late-origin"}, +}; + +struct Pairing { + const char* input; + const char* script; + std::int64_t step; // the input step +}; + +const Pairing kPairings[] = { + {"2", "2", 2 * kMinute}, {"5", "5", 5 * kMinute}, {"1", "5", kMinute}, + {"30", "60", 30 * kMinute}, {"60", "60", 60 * kMinute}, {"15", "240", 15 * kMinute}, +}; + +enum class Tape { Whole, Days, Gaps, OffGrid, InBreak }; +const char* tape_name(Tape tape) { + switch (tape) { + case Tape::Whole: return "whole"; + case Tape::Days: return "whole-days"; + case Tape::Gaps: return "gaps"; + case Tape::OffGrid: return "off-grid"; + case Tape::InBreak: return "in-break"; + } + return "?"; +} + +enum class Mode { Batch, StreamOpen, StreamFinal, Prints }; +const char* mode_name(Mode mode) { + switch (mode) { + case Mode::Batch: return "batch"; + case Mode::StreamOpen: return "stream-end(false)"; + case Mode::StreamFinal: return "stream-end(true)"; + case Mode::Prints: return "prints"; + } + return "?"; +} + +Bar bar_at(std::int64_t ts, int i) { + const double price = 100.0 + 0.25 * static_cast(i % 11); + return {price, price + 0.75, price - 0.75, price + 0.25, 1.0, ts}; +} + +// Input bars on the input grid wherever the session is open, over five and a +// half days -- Days stops at Tuesday 00:00 UTC instead, so a batch's final bar +// and (see warmup_for) a stream's last warmup bar end a session day on the +// sessions that roll at midnight or close before it. Gaps drops every seventh +// bar and the whole of Friday; OffGrid moves every third stamp a little inside +// its own slot (FeedTolerant only); InBreak adds a stamp inside each day's +// closed time (FeedTolerant only). +std::vector tape_for(const nc::SessionCalendar& calendar, const Pairing& pairing, Tape tape) { + std::vector bars; + const std::int64_t first = kThu; + const std::int64_t last = kThu + 5 * kDay + (tape == Tape::Days ? 0 : 12 * 60 * kMinute); + const std::int64_t nudge = pairing.step >= 5 * kMinute ? 2 * kMinute : 20 * kSecond; + int i = 0; + for (std::int64_t ts = first; ts < last; ts += pairing.step) { + const bool open = nc::in_session(calendar, ts); + if (!open) { + // One stamp a day inside declared closed time, off the input grid. + if (tape == Tape::InBreak && (ts / pairing.step) % 97 == 0) { + bars.push_back(bar_at(ts + nudge, i++)); + } + continue; + } + if (tape == Tape::Gaps && (i % 7 == 3 || (ts >= kThu + kDay && ts < kThu + 2 * kDay))) { + ++i; + continue; + } + const std::int64_t stamp = tape == Tape::OffGrid && i % 3 == 1 ? ts + nudge : ts; + bars.push_back(bar_at(stamp, i++)); + } + return bars; +} + +// A stream's warmup: half the tape, or on Days every bar before Sunday 00:00. +int warmup_for(const std::vector& bars, Tape tape) { + if (tape != Tape::Days) return static_cast(bars.size()) / 2; + int warmup = 0; + while (warmup < static_cast(bars.size()) + && bars[static_cast(warmup)].timestamp < kThu + 3 * kDay) ++warmup; + return warmup; +} + +NativeRunSpec spec_for(const Session& session, const char* zone, const Pairing& pairing, + NativeSlotLabelPolicy labels) { + NativeRunSpec spec; + spec.identity = {"perf-kedge-utc", 1}; + spec.input_tf = pairing.input; + spec.script_tf = pairing.script; + spec.tickerid = "TEST:KEDGE"; + spec.timezone = zone; + spec.session = session.text; + spec.slot_label_policy = labels; + spec.calculation = NativeCalculationTrigger::BarCloseAndFills; + spec.initial_capital = 100000.0; + spec.point_value = 1.0; + spec.account_fx = 1.0; + spec.price_tick = 0.01; + spec.fee_kind = NativeFeeKind::CashPerExecution; + spec.fee_value = 0.0; + return spec; +} + +struct Outcome { + std::vector bar_opens, bars, refills, applied, ticks; + std::string error; + std::uint64_t resolutions = 0; + bool utc = false; +}; + +Outcome drive(const NativeRunSpec& spec, const std::vector& bars, Mode mode, + int warmup) { + Host host; + Outcome out; + if (host.configure_native(spec).status != NativeSetupStatus::Applied) { + out.error = "configure: " + host.last_error(); + return out; + } + const int n = static_cast(bars.size()); + switch (mode) { + case Mode::Batch: + host.run(bars.data(), n); + break; + case Mode::StreamOpen: + case Mode::StreamFinal: + if (host.stream_begin(bars.data(), warmup, spec.input_tf, spec.script_tf)) { + for (int i = warmup; i < n; ++i) { + if (!host.stream_push_bar(bars[static_cast(i)])) break; + } + (void)host.stream_end(mode == Mode::StreamFinal); + } + break; + case Mode::Prints: + if (host.stream_begin(bars.data(), warmup, spec.input_tf, spec.script_tf)) { + // Thirty hours of prints: at least one session-day boundary on + // every session, at a bounded cost. + std::uint64_t sequence = 1; + bool ok = true; + const std::int64_t until = warmup < n + ? bars[static_cast(warmup)].timestamp + 30 * 60 * kMinute : 0; + for (int i = warmup; ok && i < n && bars[static_cast(i)].timestamp < until; ++i) { + const Bar& b = bars[static_cast(i)]; + std::int64_t offset = 0; + for (const double price : {b.open, b.high, b.low, b.close}) { + ok = host.stream_push_tick(TradeTick{b.timestamp + offset, sequence++, price, 0.25}); + if (!ok) break; + offset += 5 * kSecond; + } + } + (void)host.stream_end(true); + } + break; + } + out.bar_opens = host.bar_opens; + out.bars = host.bars; + out.refills = host.refills; + out.applied = host.applied; + out.ticks = host.ticks; + out.error = host.last_error(); + out.resolutions = Probe::interval_resolutions(host); + out.utc = Probe::calendar_is_utc(host); + return out; +} + +struct Tally { + int runs = 0; + int completed = 0; + std::uint64_t facts = 0; + std::uint64_t in = 0, out = 0, opens = 0, closes = 0, ticks = 0, refills = 0; + int mismatches = 0; +}; + +void count(Tally& tally, const Outcome& o) { + for (const auto* seen : {&o.bar_opens, &o.bars, &o.refills, &o.applied, &o.ticks}) { + for (const Facts& f : *seen) { + ++tally.facts; + f.in ? ++tally.in : ++tally.out; + tally.opens += f.opens; + tally.closes += f.closes; + } + } + tally.ticks += o.ticks.size(); + tally.refills += o.refills.size(); +} + +void test_same_facts_as_the_zone_path() { + std::printf("test_same_facts_as_the_zone_path\n"); + Tally tally; + for (const Session& session : kSessions) { + const auto calendar = nc::parse_session(session.text, "UTC"); + CHECK(calendar.has_value()); + if (!calendar) continue; + for (const Pairing& pairing : kPairings) { + for (const auto labels : {NativeSlotLabelPolicy::Canonical, + NativeSlotLabelPolicy::FeedTolerant}) { + for (const Tape tape : {Tape::Whole, Tape::Days, Tape::Gaps, Tape::OffGrid, + Tape::InBreak}) { + if (labels == NativeSlotLabelPolicy::Canonical + && (tape == Tape::OffGrid || tape == Tape::InBreak)) continue; + const auto bars = tape_for(*calendar, pairing, tape); + const int warmup = warmup_for(bars, tape); + for (const Mode mode : {Mode::Batch, Mode::StreamOpen, Mode::StreamFinal, + Mode::Prints}) { + const Outcome utc = drive(spec_for(session, "UTC", pairing, labels), bars, + mode, warmup); + const Outcome zone = drive(spec_for(session, "UTC0", pairing, labels), bars, + mode, warmup); + ++tally.runs; + const bool same = utc.bar_opens == zone.bar_opens && utc.bars == zone.bars + && utc.refills == zone.refills && utc.applied == zone.applied + && utc.ticks == zone.ticks && utc.error == zone.error; + CHECK(utc.utc); + CHECK(!zone.utc); + CHECK(same); + if (!same && ++tally.mismatches <= 5) { + std::fprintf(stderr, " MISMATCH %s %s->%s %s %s %s: bars %zu/%zu error '%s' / '%s'\n", + session.name, pairing.input, pairing.script, + labels == NativeSlotLabelPolicy::Canonical ? "canonical" : "tolerant", + tape_name(tape), mode_name(mode), utc.bars.size(), + zone.bars.size(), utc.error.c_str(), zone.error.c_str()); + } + if (utc.error.empty() && !utc.bars.empty()) ++tally.completed; + count(tally, utc); + } + } + } + } + } + std::printf(" %d runs (%d completed with bars), %llu facts: %llu in session, %llu out, " + "%llu opens, %llu closes, %llu prints, %llu fill recalculations; %d mismatches\n", + tally.runs, tally.completed, static_cast(tally.facts), + static_cast(tally.in), static_cast(tally.out), + static_cast(tally.opens), static_cast(tally.closes), + static_cast(tally.ticks), static_cast(tally.refills), + tally.mismatches); + // Not vacuous: most runs complete, and every kind of fact and decision + // point is reached. + CHECK(tally.completed * 4 >= tally.runs * 3); + CHECK(tally.in > 0 && tally.out > 0); + CHECK(tally.opens > 0 && tally.closes > 0); + CHECK(tally.ticks > 0 && tally.refills > 0); +} + +// A 60-minute bar whose label sits in a declared break reads the break's +// reopening: in session, neither opening nor closing its day. On UTC that +// label is out of session, so it takes the interval path inside the shortcut. +void test_break_label_reads_its_reopening() { + std::printf("test_break_label_reads_its_reopening\n"); + const Session session{"0900-1130,1230-1500", "break"}; + const Pairing pairing{"30", "60", 30 * kMinute}; + const auto calendar = nc::parse_session(session.text, "UTC"); + CHECK(calendar.has_value()); + if (!calendar) return; + const auto bars = tape_for(*calendar, pairing, Tape::Whole); + const Outcome o = drive(spec_for(session, "UTC", pairing, NativeSlotLabelPolicy::Canonical), + bars, Mode::Batch, 0); + CHECK(o.error.empty()); + int noon = 0; + for (const Facts& f : o.bars) { + if (f.ts % kDay != 12 * 60 * kMinute) continue; + ++noon; + CHECK(f.in); + CHECK(!f.opens); + CHECK(!f.closes); + } + // Thursday, Friday, Monday and the Tuesday half-day tape each have one. + CHECK(noon >= 4); +} + +// ── 3. cost ──────────────────────────────────────────────────────────────── + +void test_utc_tolerant_batch_resolves_no_interval_per_bar() { + std::printf("test_utc_tolerant_batch_resolves_no_interval_per_bar\n"); + const Session session{"24x7", "24x7"}; + const Pairing pairing{"15", "15", 15 * kMinute}; + const auto calendar = nc::parse_session(session.text, "UTC"); + CHECK(calendar.has_value()); + if (!calendar) return; + const auto bars = tape_for(*calendar, pairing, Tape::Whole); + const Outcome utc = drive(spec_for(session, "UTC", pairing, NativeSlotLabelPolicy::FeedTolerant), + bars, Mode::Batch, 0); + const Outcome zone = drive(spec_for(session, "UTC0", pairing, NativeSlotLabelPolicy::FeedTolerant), + bars, Mode::Batch, 0); + std::printf(" %zu bars: UTC resolved %llu intervals, UTC0 %llu\n", bars.size(), + static_cast(utc.resolutions), + static_cast(zone.resolutions)); + CHECK(utc.error.empty() && zone.error.empty()); + CHECK(utc.bars.size() == bars.size()); + CHECK(utc.bars == zone.bars); + CHECK(utc.resolutions <= 1); + CHECK(zone.resolutions >= bars.size()); +} + +} // namespace + +int main() { + test_utc_decision(); + test_same_facts_as_the_zone_path(); + test_break_label_reads_its_reopening(); + test_utc_tolerant_batch_resolves_no_interval_per_bar(); + std::printf("\n%d passed, %d failed\n", passed, failed); + return failed == 0 ? 0 : 1; +} From debbd7f11b0923083f906533a997c8c1349d83f5 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 09:07:44 +0800 Subject: [PATCH 56/83] Tests: the UTC session-day witness fits the PR set's thresholds and reaches every delivery path (R5 lane PERF-KEDGE, review) The first lab remote runs of 16788d05 timed the witness at 120.5 s under sanitizers (NTU2) and 43.9 s in Debug, over the 60 s / 30 s that put a row in the PR set's `slow` list. The differential keeps its reach at about a third of the work: the tapes run Friday through Tuesday (a weekend and a masked day still inside), the 1-minute and 2-minute inputs are 3-minute (aggregated to 15) and 10-minute ones, and the confirmed-bar streams end with stream_end(true) only, which also seals and presents a calendar-closed final bucket -- the stream_end(false) runs presented nothing the true runs do not. 864 runs, 0.52M facts, 142k of them at prints; all six mutants of 16788d05's message still fail it. An independent review (read-only) found no defect and four low items, taken here: - the delivery paths the matrix did not reach now run, UTC against UTC0: the magnifier's synthesized path over a chart and an aggregated bar (deliver_intrabar_script), a 7-minute bucket that does not divide a session day (its bars at the calendar's own labels), a 30-second timeframe, and an undetected timeframe's one raw-labelled bar -- 54 runs, all completed; - native_calendar_memo.hpp says what it means: one session day for every instant the calendar can represent, and a resolution count since the memo last bound a calendar or was reset (comment-only); - ADR-0001's present_session_day row names the witness. No source line of the library moves: the header change is comment-only. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit f1f3febd0e64607ab46aa783a38ae5c668c78ff9) INT26 pick note: the ADR conflicted on citation digits only; every other hunk of f1f3febd carried unchanged. --- docs/adr/0001-kernel-adapter-boundary.md | 2 +- src/native_calendar_memo.hpp | 8 +- tests/test_native_session_day_utc.cpp | 131 +++++++++++++++++++---- 3 files changed, 115 insertions(+), 26 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index cbe80ea3..232660b8 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -798,7 +798,7 @@ The table records, at the tree the doc wave read, what each block described and | the indicator library -- `include/pineforge/ta.hpp`, `src/ta_moving_averages.cpp`, `src/ta_oscillators.cpp`, `src/ta_extremes_volume.cpp`, `src/ta_volatility_trend.cpp`, `src/ta_misc.cpp`, `include/pineforge/window_sum.hpp`, `include/pineforge/math.hpp` | each indicator implements its Pine `ta.*` / `math.*` definition -- warm-up and na rules, the window call-site ring, the compensated window sum, tie and epsilon rules -- and its comments record the TradingView tape or export that calibrated each number (the `ta::ATR` and standard-deviation rows above are two of them) | an indicator is a function of its inputs, the same for every venue; generated strategies call exactly these classes, and where two textbook conventions genuinely compete the choice is a per-instance option (`ta::EmaSeeding`), never a branch on the platform | a convention switch on every indicator, for a choice the script already made by calling that Pine function | the kernel-only TA suites `tests/test_ta_*.cpp` and the corpus parity gate | | the formatting library -- `include/pineforge/str_utils.hpp`, `src/str_utils.cpp` (`str_tostring`, `str_format_values`, `str_format`) | the number text of Pine's `str.tostring` / `str.format` built-ins: a number's shortest round-trip decimal spelling, scaled and rounded half-up on its digits, the default, `percent`, `volume` (K / M / B / T) and pattern modes, MessageFormat placeholders and quoting, `NaN` and `Infinity` -- the published rendering TradingView's exported tapes pin (R5 lane C6), ported from the codegen formatter (lane C6b) by R5 lane B-ENGINE; the `mintick` mode, which generated code delegates to it, is kept as it was | a formatting library is a function of its inputs, the same for every venue, as the indicator library is: it spells a number the way the source language's own `str.*` library defines, and no kernel order, fill, settlement, margin or hash code calls it -- of the thirty-five kernel translation units only `src/str_utils.cpp` names these functions | a rendering switch on functions whose definition the calling script chose by calling them | `tests/test_str_number_format.cpp` (lane C6b's 42-case edge battery, every numeric string of lane C6's TradingView exports, byte-identical output on macOS arm64 and Linux x86-64) and `tests/test_str_utils.cpp` | | the language calendar functions -- `include/pineforge/session_time.hpp`, `src/session_time.cpp` (`timeframe_time`, `local_hour` and its siblings, `session_in_market`, `session_trading_day_open_ms`, ...) | the definitions of Pine's `time()` / `time_close()` / bare-time / `session.is*` built-ins for generated code: session and timezone parsing, the invalid- and 24-hour-session spellings, `time("60")` day keying, `session.is*` on daily charts, each comment naming the tape that pinned it | the inputs are data -- a session string, a timezone, a timeframe -- so another venue is driven by its own session template; nothing branches on a platform, and a native host's calendar is the separate generic `native_calendar` | a rules switch on functions whose definition the generated script chose by calling them | `tests/test_session_time.cpp`, `tests/test_pine_time_day_stamp_grid.cpp` and the corpus parity gate | -| `present_session_day` `src/native_execution_consumer.cpp:7286`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (`tests/fixtures/session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | `tests/test_native_session_day_facts.cpp` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), the C scenario of `tests/test_native_c_api.c`, and `tests/test_session_day_facts_adapter.cpp` (the adapter selects them flag for flag) | +| `present_session_day` `src/native_execution_consumer.cpp:7286`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (`tests/fixtures/session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | `tests/test_native_session_day_facts.cpp` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), the C scenario of `tests/test_native_c_api.c`, `tests/test_session_day_facts_adapter.cpp` (the adapter selects them flag for flag), and `tests/test_native_session_day_utc.cpp` (kernel-only: on a UTC calendar the facts read an in-session instant without resolving its interval, and match the zone path at every decision point) | | `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7927-7943` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | | `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7904-7925` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | | `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6618` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | diff --git a/src/native_calendar_memo.hpp b/src/native_calendar_memo.hpp index 35accfe6..e661c5e7 100644 --- a/src/native_calendar_memo.hpp +++ b/src/native_calendar_memo.hpp @@ -77,9 +77,9 @@ class SessionDayMemo { return bind(calendar); } - // How many intervals the memo has resolved since it was last reset, as - // opposed to answered from the last few it holds: a cost reading, derived - // like everything else here and folded nowhere (R5 lane PERF-KEDGE). + // How many intervals the memo has resolved since it last bound a calendar + // or was reset, as opposed to answered from the last few it holds: a cost + // reading, derived like everything else here and folded nowhere. std::uint64_t interval_resolutions() const noexcept; private: @@ -116,7 +116,7 @@ bool in_session(const SessionCalendar& calendar, int64_t ms, SessionDayMemo& mem // memo-free calendar decides at every resolution. A UTC calendar is integer // arithmetic throughout (no libc zone, no mktime): its session days are the // fixed daily cycles [date + origin, next date + origin), which tile time, so -// every instant has exactly one session day and every lookup finds it. +// every instant it can represent has one session day, and every lookup finds it. bool utc_calendar(const SessionCalendar& calendar, SessionDayMemo& memo); } // inline namespace native_calendar_v2 diff --git a/tests/test_native_session_day_utc.cpp b/tests/test_native_session_day_utc.cpp index 24221404..298296a5 100644 --- a/tests/test_native_session_day_utc.cpp +++ b/tests/test_native_session_day_utc.cpp @@ -28,7 +28,9 @@ // 12:30), overnight, weekday-masked and late-origin sessions, script // timeframes over finer inputs, Canonical and FeedTolerant labels with // gaps, off-grid provider stamps and stamps inside a declared break, -// batch runs, confirmed-bar streams ended both ways and printed streams. +// batch runs, and confirmed-bar and printed streams -- and on the other +// delivery paths: the magnifier's synthesized path, a bucket that does +// not divide the session day, a seconds timeframe and an undetected one. // Both runs must also end with the same error text (usually none). // 3. Cost: a UTC FeedTolerant batch -- the Pine adapter's shape -- resolves // at most one interval for its facts (its final bar's scheduled close), @@ -89,9 +91,9 @@ int failed = 0; constexpr std::int64_t kSecond = 1000; constexpr std::int64_t kMinute = 60 * kSecond; constexpr std::int64_t kDay = 1440 * kMinute; -// Thursday 2025-07-03 00:00 UTC: the tapes run Thursday through Tuesday, so -// every one crosses a weekend and a weekday mask has days to drop. -constexpr std::int64_t kThu = 1751500800000LL; +// Friday 2025-07-04 00:00 UTC: the tapes run Friday through Tuesday, so every +// one crosses a weekend and a weekday mask has days to drop. +constexpr std::int64_t kFri = 1751587200000LL; // ── 1. the UTC decision ──────────────────────────────────────────────────── @@ -190,7 +192,7 @@ struct Pairing { }; const Pairing kPairings[] = { - {"2", "2", 2 * kMinute}, {"5", "5", 5 * kMinute}, {"1", "5", kMinute}, + {"5", "5", 5 * kMinute}, {"10", "10", 10 * kMinute}, {"3", "15", 3 * kMinute}, {"30", "60", 30 * kMinute}, {"60", "60", 60 * kMinute}, {"15", "240", 15 * kMinute}, }; @@ -206,12 +208,13 @@ const char* tape_name(Tape tape) { return "?"; } -enum class Mode { Batch, StreamOpen, StreamFinal, Prints }; +// Both streams end with stream_end(true), which also seals and presents a +// final bucket the calendar has closed. +enum class Mode { Batch, Stream, Prints }; const char* mode_name(Mode mode) { switch (mode) { case Mode::Batch: return "batch"; - case Mode::StreamOpen: return "stream-end(false)"; - case Mode::StreamFinal: return "stream-end(true)"; + case Mode::Stream: return "stream"; case Mode::Prints: return "prints"; } return "?"; @@ -222,17 +225,18 @@ Bar bar_at(std::int64_t ts, int i) { return {price, price + 0.75, price - 0.75, price + 0.25, 1.0, ts}; } -// Input bars on the input grid wherever the session is open, over five and a +// Input bars on the input grid wherever the session is open, over four and a // half days -- Days stops at Tuesday 00:00 UTC instead, so a batch's final bar // and (see warmup_for) a stream's last warmup bar end a session day on the // sessions that roll at midnight or close before it. Gaps drops every seventh -// bar and the whole of Friday; OffGrid moves every third stamp a little inside +// bar and the whole of Monday; OffGrid moves every third stamp a little inside // its own slot (FeedTolerant only); InBreak adds a stamp inside each day's // closed time (FeedTolerant only). -std::vector tape_for(const nc::SessionCalendar& calendar, const Pairing& pairing, Tape tape) { +std::vector tape_for(const nc::SessionCalendar& calendar, const Pairing& pairing, Tape tape, + std::int64_t span = 4 * kDay + 12 * 60 * kMinute) { std::vector bars; - const std::int64_t first = kThu; - const std::int64_t last = kThu + 5 * kDay + (tape == Tape::Days ? 0 : 12 * 60 * kMinute); + const std::int64_t first = kFri; + const std::int64_t last = kFri + (tape == Tape::Days ? 4 * kDay : span); const std::int64_t nudge = pairing.step >= 5 * kMinute ? 2 * kMinute : 20 * kSecond; int i = 0; for (std::int64_t ts = first; ts < last; ts += pairing.step) { @@ -244,7 +248,7 @@ std::vector tape_for(const nc::SessionCalendar& calendar, const Pairing& pa } continue; } - if (tape == Tape::Gaps && (i % 7 == 3 || (ts >= kThu + kDay && ts < kThu + 2 * kDay))) { + if (tape == Tape::Gaps && (i % 7 == 3 || (ts >= kFri + 3 * kDay && ts < kFri + 4 * kDay))) { ++i; continue; } @@ -254,12 +258,30 @@ std::vector tape_for(const nc::SessionCalendar& calendar, const Pairing& pa return bars; } +// Input bars at the calendar's own slot labels for `tf` wherever the slot has +// an instant in session: the grid restarts at every session-day origin, which +// a timeframe that does not divide the day needs for Canonical labels. +std::vector calendar_grid_tape(const nc::SessionCalendar& calendar, const char* tf, + std::int64_t span) { + const auto timeframe = nc::parse_timeframe(tf); + std::vector bars; + int i = 0; + for (std::int64_t t = kFri; timeframe && t < kFri + span;) { + const auto interval = nc::interval_containing(calendar, *timeframe, t); + if (!interval || interval->next_period_open_ms <= t) break; + if (interval->last_traded_close_ms > interval->eligible_open_ms) + bars.push_back(bar_at(interval->open_ms, i++)); + t = interval->next_period_open_ms; + } + return bars; +} + // A stream's warmup: half the tape, or on Days every bar before Sunday 00:00. int warmup_for(const std::vector& bars, Tape tape) { if (tape != Tape::Days) return static_cast(bars.size()) / 2; int warmup = 0; while (warmup < static_cast(bars.size()) - && bars[static_cast(warmup)].timestamp < kThu + 3 * kDay) ++warmup; + && bars[static_cast(warmup)].timestamp < kFri + 2 * kDay) ++warmup; return warmup; } @@ -269,6 +291,7 @@ NativeRunSpec spec_for(const Session& session, const char* zone, const Pairing& spec.identity = {"perf-kedge-utc", 1}; spec.input_tf = pairing.input; spec.script_tf = pairing.script; + spec.timeframe_undetected = spec.input_tf.empty(); spec.tickerid = "TEST:KEDGE"; spec.timezone = zone; spec.session = session.text; @@ -303,13 +326,12 @@ Outcome drive(const NativeRunSpec& spec, const std::vector& bars, Mode mode case Mode::Batch: host.run(bars.data(), n); break; - case Mode::StreamOpen: - case Mode::StreamFinal: + case Mode::Stream: if (host.stream_begin(bars.data(), warmup, spec.input_tf, spec.script_tf)) { for (int i = warmup; i < n; ++i) { if (!host.stream_push_bar(bars[static_cast(i)])) break; } - (void)host.stream_end(mode == Mode::StreamFinal); + (void)host.stream_end(true); } break; case Mode::Prints: @@ -381,8 +403,7 @@ void test_same_facts_as_the_zone_path() { && (tape == Tape::OffGrid || tape == Tape::InBreak)) continue; const auto bars = tape_for(*calendar, pairing, tape); const int warmup = warmup_for(bars, tape); - for (const Mode mode : {Mode::Batch, Mode::StreamOpen, Mode::StreamFinal, - Mode::Prints}) { + for (const Mode mode : {Mode::Batch, Mode::Stream, Mode::Prints}) { const Outcome utc = drive(spec_for(session, "UTC", pairing, labels), bars, mode, warmup); const Outcome zone = drive(spec_for(session, "UTC0", pairing, labels), bars, @@ -423,6 +444,73 @@ void test_same_facts_as_the_zone_path() { CHECK(tally.ticks > 0 && tally.refills > 0); } +// The other paths a script bar is delivered on: the magnifier's synthesized +// path (deliver_intrabar_script), over a chart and an aggregated bar, a +// 7-minute bucket, which does not divide a session day, so the day's last +// bucket runs past the next origin (its bars at the calendar's own labels), a +// 30-second timeframe, and an undetected timeframe, which takes one bar and +// partitions by its raw label -- batch and stream (the undetected one batch +// only), UTC against UTC0. +void test_other_delivery_paths() { + std::printf("test_other_delivery_paths\n"); + struct Variant { + const char* name; + Pairing pairing; + bool magnifier; + bool calendar_grid; + std::int64_t span; + }; + const Variant variants[] = { + {"magnifier", {"5", "5", 5 * kMinute}, true, false, 2 * kDay + 12 * 60 * kMinute}, + {"magnifier-aggregated", {"5", "15", 5 * kMinute}, true, false, 2 * kDay + 12 * 60 * kMinute}, + {"7-minute", {"7", "7", 7 * kMinute}, false, true, 2 * kDay + 12 * 60 * kMinute}, + {"30-second", {"30S", "30S", 30 * kSecond}, false, false, kDay + 2 * 60 * kMinute}, + {"undetected", {"", "", 15 * kMinute}, false, false, 12 * 60 * kMinute}, + }; + int runs = 0, completed = 0, mismatches = 0; + for (const Session& session : {kSessions[0], kSessions[2], kSessions[3]}) { + const auto calendar = nc::parse_session(session.text, "UTC"); + CHECK(calendar.has_value()); + if (!calendar) continue; + for (const Variant& variant : variants) { + auto bars = variant.calendar_grid + ? calendar_grid_tape(*calendar, variant.pairing.input, variant.span) + : tape_for(*calendar, variant.pairing, Tape::Whole, variant.span); + if (variant.pairing.input[0] == '\0' && bars.size() > 1) bars.resize(1); + for (const auto labels : {NativeSlotLabelPolicy::Canonical, + NativeSlotLabelPolicy::FeedTolerant}) { + for (const Mode mode : {Mode::Batch, Mode::Stream}) { + if (variant.pairing.input[0] == '\0' && mode != Mode::Batch) continue; + auto utc_spec = spec_for(session, "UTC", variant.pairing, labels); + auto zone_spec = spec_for(session, "UTC0", variant.pairing, labels); + if (variant.magnifier) { + utc_spec.intrabar.value = IntrabarPath::synthesized{}; + zone_spec.intrabar.value = IntrabarPath::synthesized{}; + } + const int warmup = static_cast(bars.size()) / 2; + const Outcome utc = drive(utc_spec, bars, mode, warmup); + const Outcome zone = drive(zone_spec, bars, mode, warmup); + ++runs; + const bool same = utc.bar_opens == zone.bar_opens && utc.bars == zone.bars + && utc.refills == zone.refills && utc.applied == zone.applied + && utc.error == zone.error; + CHECK(same); + if (!same && ++mismatches <= 5) { + std::fprintf(stderr, " MISMATCH %s %s %s %s: bars %zu/%zu error '%s' / '%s'\n", + session.name, variant.name, + labels == NativeSlotLabelPolicy::Canonical ? "canonical" : "tolerant", + mode_name(mode), utc.bars.size(), zone.bars.size(), + utc.error.c_str(), zone.error.c_str()); + } + if (utc.error.empty() && !utc.bars.empty()) ++completed; + } + } + } + } + std::printf(" %d runs (%d completed with bars); %d mismatches\n", runs, completed, mismatches); + CHECK(completed * 4 >= runs * 3); +} + // A 60-minute bar whose label sits in a declared break reads the break's // reopening: in session, neither opening nor closing its day. On UTC that // label is out of session, so it takes the interval path inside the shortcut. @@ -445,7 +533,7 @@ void test_break_label_reads_its_reopening() { CHECK(!f.opens); CHECK(!f.closes); } - // Thursday, Friday, Monday and the Tuesday half-day tape each have one. + // Friday through Monday each have one (the session has no weekday mask). CHECK(noon >= 4); } @@ -478,6 +566,7 @@ void test_utc_tolerant_batch_resolves_no_interval_per_bar() { int main() { test_utc_decision(); test_same_facts_as_the_zone_path(); + test_other_delivery_paths(); test_break_label_reads_its_reopening(); test_utc_tolerant_batch_resolves_no_interval_per_bar(); std::printf("\n%d passed, %d failed\n", passed, failed); From 898662054df412bc2e40ec539e078954c935d150 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:29:58 +0800 Subject: [PATCH 57/83] INT26 (ii): the 1.0.0 entry says what waves G and H change for a user REL10's CHANGELOG entry (5476aa1e) covered the history up to its own base, 1a0e7ea1. It now also covers #289 (wave G) and this release's picks, one sentence each that a user can act on, citing no lane: - a new section, "What a native host must handle": the typed UnrepresentableQuantity refusal (C 10) and the opt-in quantity_tolerance with the grid's own quantities; OCA-group absorption, keep_handle re-prices inside a group, and a grid request whose pending deduction its units absorb; aggregated runs' script-bar indices and input_interval_index; mark_native_report_point; final calendar buckets; the IntrabarSample margin check kind (C 4); the callback table's reserved1 marker, refused when nonzero; - in the behaviour list: margin calls on a leveraged opening's entry bar, frozen units for a default-percent stop entry above 100 %, per-sample margin calls under the bar magnifier, a pending entry's trailing exit seeded at its activation, process_orders_on_close stop entries filling at the close that reached them and the pyramiding cap they could bypass, magnified fills dated at the chart bar's open, and the exits a margin call may not revive; - the state-hash bullet names #289, and the -ffp-contract=off bullet the -fbracket-depth=1024 that Clang consumers now receive. CI-FLAKE and PERF-KEDGE change no user-visible value. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0012db0..bd53cf11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,9 +58,10 @@ prerelease included. equality alone does not make a pair. - **State hashes are new, and v19's.** The broker-state hash, the per-bar hash rows, the stream fingerprint and the native continuation hash did not exist - in 0.13.1; their values are the v19 epoch's (#282, #283, #284, #286), and a - value recorded by a development build before 1.0.0 is not comparable. From - 1.0.0 their recipe is fixed for all of 1.x. + in 0.13.1; their values are the v19 epoch's as the release candidates fix + them (#282, #283, #284, #286, #289), and a value recorded by a development + build before 1.0.0 is not comparable. From 1.0.0 their recipe is fixed for + all of 1.x. - **Event retention defaults to a window.** `NativeRunSpec::event_retention` defaults to `NativeEventRetention::Window`, which keeps the command journal only until every reader has consumed it (#283). A C++ host that reads the @@ -70,7 +71,10 @@ prerelease included. every translation unit that links `PineForge::pineforge` or `PineForge::kernel` (#282); 0.13.1 compiled the library alone with it. A build outside CMake compiles generated strategy code with - `-ffp-contract=off` itself (docs/pages/integration-cmake.md). + `-ffp-contract=off` itself (docs/pages/integration-cmake.md). The package + also hands every Clang-compiled consumer `-fbracket-depth=1024`, so the + deeply nested C++ codegen emits for deep Pine compiles; a Clang build + outside CMake adds that flag itself. - **One header moved.** `` is now ``, a source-layer header (e71e589c); new code includes ``. A @@ -86,6 +90,51 @@ prerelease included. `find_package(PineForge 1.0.0 EXACT)` also accepts `1.0.0-rc.N`. The CMake smoke consumer (`cmake/smoke_consumer`) prints `pf_version_string()`. +### What a native host must handle + +- **An unrepresentable quantity is a refusal, not a failed run.** A request + the settlement cannot book exactly in binary64 (a close that would leave a + sub-ulp rest, an opening a surviving dust lot absorbs) is refused with + `MatchRejectReason::UnrepresentableQuantity` (C + `PF_NATIVE_MATCH_REJECT_UNREPRESENTABLE_QUANTITY`, 10) and the run goes on. + Handle it where you handle the other match rejections (#289). +- **Quantity tolerance is opt-in.** `NativeRunSpec::quantity_tolerance` (C: + the sixth `pf_native_run_spec_ext_v1` layout, flag + `PF_NATIVE_SPEC_EXT_QUANTITY_TOLERANCE`) lets a close within that distance + of a FIFO lot boundary end on it; leave it unset and nothing changes. On a + quantity grid, a whole-scope `ScopeFraction` and a close of one lot's own + size are on the grid (#289). +- **OCA groups never stop a run on binary64 dust.** A sibling deduction too + small to move the sibling's units is absorbed and recorded with a zero + deduction (#289). A sibling re-priced with `ReplaceOptions::keep_handle` + inside a group no longer fails the run when another member fills; the run + records what a plain replace records. On a quantity grid, a request whose + pending group deduction its units absorb settles in one fill, so a + whole-scope close closes its lot whole. +- **Aggregated runs count script bars.** When the script timeframe aggregates + the input, `NativeCoordinate::interval_index` and every lot, trade-row and + metric index are script-bar indices; read the input slot from + `NativeCoordinate::input_interval_index` (#289). +- **Report points at the host's cadence.** Under + `NativeReportPolicy::KernelRecordedAtHostMarks`, a C++ host calls + `NativeStrategyHost::mark_native_report_point` from its own callbacks; it + returns `false` outside such a run. The C surface has no such call (#289). +- **Final calendar buckets calculate.** A batch, or `stream_end(true)`, + calculates its last pending script bucket when that bucket's last input + reached the session close, so the final daily bucket of an hourly feed now + calculates; warmup and `stream_end(false)` still carry it (#289). +- **A new margin check kind.** On a continuous intrabar path + (`IntrabarPath::lower_tf` with its default `ContinuousSegments` + eligibility) the kernel checks the margin model at every later sample of + the bar, at that sample's price: + `NativeMarginCheckKind::IntrabarSample` (C + `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE`, 4). A requirement hook sees it; a + host that switches on the kind must handle it. +- **The C callback table's marker must be zero.** A C host built against the + current `pf_native_callbacks_v1` leaves its trailing `reserved1` at 0: + `strategy_native_host_create_v1` returns NULL for any other value. Tables of + the three earlier published lengths keep working (#289). + ### Behaviour that moves results against 0.13.1 These change a strategy's trades or report values; each commit states the @@ -107,4 +156,30 @@ TradingView behaviour it matches. session-day facts, a session ends at the session day, and an aggregated chart and a stream's realtime bar read `session.islastbar` as TradingView does (c68fab7e, b531a8d9, 8b843499, be19463d). -- A margin call never revives an exit the script cancelled (4b00da92, #287). +- A margin call never revives an exit the script cancelled (4b00da92, #287), + whether the exit was live, dormant, or a gapped stop a declined reversal + had parked for the next margin call (#289). +- Margin: a leveraged position (margin below 100 %) is checked on its own + entry bar, so its margin call lands at that bar's low as TradingView books + it, not a bar later or never (outside `process_orders_on_close`, + `calc_on_order_fills`, the bar magnifier and a timestamped FX curve, which + keep their own schedule). A default `percent_of_equity` stop entry + sized above 100 % keeps the quantity it was sized at when placed (at the + signal close, or at the snapped stop level when not yet marketable), as + TradingView does; it was re-sized at the fill. Under the bar magnifier + (without `process_orders_on_close` or `calc_on_order_fills`), a leveraged + position's margin call lands at the first lower-timeframe low that crosses + its line, and again at each later low that crosses the reduced position's + line. +- A trailing `strategy.exit` placed while its entry is still pending starts + its running best at the activation, as TradingView's does; on TradingView's + tapes it exited 1 to 24 bars late. +- `process_orders_on_close`: a stop entry whose stop the placing bar's close + already reached, and the bar's only entry marketable there, fills at that + close (slippage applied), on the chart, an aggregated chart and under the + bar magnifier; it filled at the next open. + An add can no longer exceed `pyramiding` because an opposite entry was + resting from an earlier bar. One corpus probe moves toward its TradingView + tape (`order-deferred-flip-pooc-cross-bar-01`). +- An aggregated chart under the bar magnifier dates every fill at its chart + bar's open, as TradingView does. From e3fa0093560379075c639e0abdf6a3b26d308810 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:47:36 +0800 Subject: [PATCH 58/83] INT26 merge: the acid composite's C port records every magnified margin point H-MEASURE's composite witness (58e0f162, picked as aeb2181a) was written before PAR-MARGIN's NativeMarginCheckKind::IntrabarSample (7c37ae41, picked as b58592c9). On the merged tree the magnified run offers 968 IntrabarSample points beside 88 BarOpen and 16 AfterApplied (1,073 checks, 160 requirement views), and the C port kept only the first 256 checks and 64 views (MAX_CHECKS / MAX_REQS), so its mag.margin.checks / mag.margin.reqs FIELD summaries differed from the C++ ones and CX c-port-shared-fields-identical failed (only-C++ 9, only-C 2). The kernel treats both hosts alike; the record was too short. The C port now keeps 4,096 checks and 1,024 views (host_state is calloc'd): C++ 914 and C 914 FIELD lines, identical; 16 features, 228 checks, 0 failures. The printed (unasserted) intrabar-margin finding said "no per-sample point" and cited the doc sentence as unmet; it now counts the IntrabarSample points separately and names the check kind. The magnified run's other fields move with PAR-MARGIN's intended change (the spike liquidates at its crossing sample): one fewer fill, lot snapshot, margin call, recalculation and trade row, two more call views and requirement rows (914 FIELD lines against main's 915). Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_native_acid_composite.cpp | 12 ++++++------ tests/test_native_acid_composite_c.c | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_native_acid_composite.cpp b/tests/test_native_acid_composite.cpp index 12260435..8986e554 100644 --- a/tests/test_native_acid_composite.cpp +++ b/tests/test_native_acid_composite.cpp @@ -1953,20 +1953,20 @@ void driving_section(const Run& b, const Run& s, const Run& cut) { // Facts this witness measured that no requested feature asserts: printed, not // checked, so the row stays a coverage witness and the finding goes to a lane. void findings(const Run& plain, const Run& mag) { - int bar_open = 0, after = 0, other = 0; + int bar_open = 0, after = 0, sample = 0, other = 0; for (const auto& c : mag.host->checks) { if (c.p.kind == NativeMarginCheckKind::BarOpen) ++bar_open; else if (c.p.kind == NativeMarginCheckKind::AfterApplied) ++after; + else if (c.p.kind == NativeMarginCheckKind::IntrabarSample) ++sample; else ++other; } const auto* pc = plain.host->calls.empty() ? nullptr : &plain.host->calls.front(); const auto* mc = mag.host->calls.empty() ? nullptr : &mag.host->calls.front(); - out(fmt("FINDING intrabar-margin: lower_tf run offers %d BarOpen + %d AfterApplied (+%d other) margin points for %d " - "script bars / %lld samples -- no per-sample point; the spike that crosses L inside bar %d is liquidated " + out(fmt("FINDING intrabar-margin: lower_tf run offers %d BarOpen + %d AfterApplied + %d IntrabarSample (+%d other) " + "margin points for %d script bars / %lld samples; the spike that crosses L inside bar %d is liquidated " "at bar %d, price %s, in %zu slices (unmagnified: bar %d, price %s, %zu slice) " - "[docs claim per-sample re-evaluation: native-engine.md:1261-1262, native_run_spec.hpp:389-391, " - "native_execution_consumer.cpp:3076-3077]", - bar_open, after, other, static_cast(mag.host->bars.size()), + "[per-sample re-evaluation: NativeMarginCheckKind::IntrabarSample, native_host.hpp]", + bar_open, after, sample, other, static_cast(mag.host->bars.size()), (ll)mag.report.magnifier_sample_ticks_total, G_SPIKE, mc ? mc->cursor.point.interval_index : -1, mc ? D(mc->mark).c_str() : "-", mag.host->calls.size(), pc ? pc->cursor.point.interval_index : -1, pc ? D(pc->mark).c_str() : "-", plain.host->calls.size())); diff --git a/tests/test_native_acid_composite_c.c b/tests/test_native_acid_composite_c.c index 52387a6f..2ff73508 100644 --- a/tests/test_native_acid_composite_c.c +++ b/tests/test_native_acid_composite_c.c @@ -61,7 +61,7 @@ static double ceil_tick(double p) { return ceil(p / ACID_TICK) * ACID_TICK; } static double buy_fill(double open) { return ceil_tick(open) + ACID_TICK; } /* ── Records ────────────────────────────────────────────────────────── */ -enum { MAX_FILLS = 32, MAX_TFS = 128, MAX_RECALC = 32, MAX_CHECKS = 256, MAX_REQS = 64, +enum { MAX_FILLS = 32, MAX_TFS = 128, MAX_RECALC = 32, MAX_CHECKS = 4096, MAX_REQS = 1024, MAX_CALLS = 8, MAX_LOT_SNAPS = 16, MAX_LOTS = 8, MAX_WSNAPS = 8, MAX_WROWS = 8, MAX_TAGGED = 16, MAX_REFUSALS = 16, MAX_ANCHORS = 8, MAX_TRAILS = 8, MAX_APPENDS = 4, MAX_XREP = 16 }; From 326d3d5770de8a61a251f805e7a9381462db803b Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 07:09:03 +0800 Subject: [PATCH 59/83] Run the heavy CI jobs on larger runners; fork pull requests stay on standard ones preflight, the build matrix, sanitizers, kernel-only, native-live and both corpus-parity jobs now select the organization's 16-core pf-linux-x64-16, or GitHub's macos-26-xlarge (the macos-26 arm64 image on an M2), for a push, a dispatch, the schedule and a same-repository pull request. A larger runner bills the organization even for a public repository, so a pull request from a fork keeps ubuntu-24.04 and macos-26. The build legs keep their names through an explicit name; the build aggregate stays on the standard runner. Every ci_verify.py call and the corpus parity script take the runner's core count (getconf _NPROCESSORS_ONLN) instead of a fixed 4. ci-workflow-contract now pins each job's runs-on, timeout-minutes and parallelism in ci.yml, native-live.yml and corpus-parity.yml, and keeps docs.yml and promote-baseline.yml on standard runners. test_ci_preflight.py mutates each pin, and runs the stage's own argv in a copied tree whose sanitizers job moved back to the standard runner or let a fork onto the larger one. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 7b4adb0c6707d00b4b76ebbf6d76720b5590d8cd) INT26 pick note (late pick, accepted 2026-09-26 10:50): scripts/ci_preflight.py conflicted beside INT26 item (iv)'s PR_SLOW_ROWS_SHA256 (29 slow rows); the digest stays item (iv)'s and this commit's runner constants follow it. --- .github/workflows/ci.yml | 30 +++++-- .github/workflows/corpus-parity.yml | 25 +++--- .github/workflows/native-live.yml | 8 +- docs/ci.md | 43 ++++++++- scripts/ci_preflight.py | 81 ++++++++++++++++- scripts/test_ci_preflight.py | 133 ++++++++++++++++++++++++++-- 6 files changed, 290 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 107cfe83..5776c98d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,15 @@ env: CCACHE_COMPRESS: "1" CCACHE_MAXSIZE: 500M +# The heavy jobs run on the organization's 16-core larger runner, and the macOS +# legs on GitHub's M2 runner for the macos-26 image, for every event except a +# pull request from a fork: a larger runner bills the organization even for a +# public repository, so a fork's pull request runs on the free standard runner. +# Each verification sizes its parallelism to the runner that took it. +# scripts/ci_preflight.py pins every runner, time limit and job count. jobs: preflight: - runs-on: ubuntu-24.04 + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} timeout-minutes: 10 steps: - uses: actions/checkout@v4 @@ -67,14 +73,23 @@ jobs: if-no-files-found: warn build: + # Each leg keeps its name, build (, ), whatever the matrix carries. + name: build (${{ matrix.os }}, ${{ matrix.build_type }}) timeout-minutes: 45 strategy: fail-fast: false matrix: os: [ubuntu-24.04, macos-26] build_type: [Release, Debug] + include: + - os: ubuntu-24.04 + larger_runner: pf-linux-x64-16 + - os: macos-26 + larger_runner: macos-26-xlarge - runs-on: ${{ matrix.os }} + # matrix.os is the standard runner a fork's pull request gets; its + # larger_runner runs the same image with more cores. + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && matrix.larger_runner || matrix.os }} steps: - uses: actions/checkout@v4 @@ -121,7 +136,7 @@ jobs: # ratio -- for a runner whose CPU accounting is unusable; record the # measured reason here and in docs/ci.md before setting it. PINEFORGE_RUNTIME_BUDGET_CANDIDATE_ONLY: '0' - run: python3 scripts/ci_verify.py ${{ matrix.build_type == 'Release' && 'release' || 'debug' }} --build-dir build --jobs 4 --ccache ${{ github.event_name == 'pull_request' && matrix.build_type == 'Debug' && '--exclude-label slow' || '' }} + run: python3 scripts/ci_verify.py ${{ matrix.build_type == 'Release' && 'release' || 'debug' }} --build-dir build --jobs "$(getconf _NPROCESSORS_ONLN)" --ccache ${{ github.event_name == 'pull_request' && matrix.build_type == 'Debug' && '--exclude-label slow' || '' }} - name: Stage and summarize diagnostics if: always() @@ -142,7 +157,7 @@ jobs: # instrumentation propagates into every test binary (CMakeLists.txt option # PINEFORGE_ENABLE_SANITIZERS). sanitizers: - runs-on: ubuntu-24.04 + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # A full run (push to main, manual dispatch) gives its CTest stage an hour # after a build of about 30 minutes; the PR set keeps its 30-minute stage. timeout-minutes: 120 @@ -173,7 +188,7 @@ jobs: ccache-${{ runner.os }}-${{ runner.arch }}-sanitizers- - name: Verify (sanitizers) - run: python3 scripts/ci_verify.py sanitizers --build-dir build-asan --jobs 4 --ccache ${{ github.event_name == 'pull_request' && '--exclude-label slow' || '' }} + run: python3 scripts/ci_verify.py sanitizers --build-dir build-asan --jobs "$(getconf _NPROCESSORS_ONLN)" --ccache ${{ github.event_name == 'pull_request' && '--exclude-label slow' || '' }} - name: Stage and summarize diagnostics if: always() @@ -194,7 +209,7 @@ jobs: # modules are built from the kernel alone, and every examples/native host # runs as an example_* row asserting both its exit code and its summary line. kernel-only: - runs-on: ubuntu-24.04 + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} timeout-minutes: 45 steps: - uses: actions/checkout@v4 @@ -220,7 +235,7 @@ jobs: ccache-${{ runner.os }}-${{ runner.arch }}-kernel- - name: Verify (kernel) - run: python3 scripts/ci_verify.py kernel --build-dir build-kernel --jobs 4 --ccache + run: python3 scripts/ci_verify.py kernel --build-dir build-kernel --jobs "$(getconf _NPROCESSORS_ONLN)" --ccache - name: Stage and summarize diagnostics if: always() @@ -255,6 +270,7 @@ jobs: name: build needs: [preflight, build, sanitizers, native-live, kernel-only, corpus-parity-subset] if: always() + # A few seconds of shell: the standard runner, for every event. runs-on: ubuntu-24.04 timeout-minutes: 5 steps: diff --git a/.github/workflows/corpus-parity.yml b/.github/workflows/corpus-parity.yml index c1b7691f..716bc91e 100644 --- a/.github/workflows/corpus-parity.yml +++ b/.github/workflows/corpus-parity.yml @@ -17,11 +17,11 @@ name: Corpus parity # the runtime and all 312 corpus strategies build in ~105 s at JOBS=8, and then # the run phase -- which run_corpus.sh executes serially, one ctypes process per # probe over a 221861-bar feed -- took 1792 s, with 94 s more to judge the -# result. The dominant term is single-core serial work, which a 4-vCPU hosted -# runner runs slower rather than faster, so the whole population cannot be held -# to the ~25 min a required pull-request check is budgeted for. It therefore -# runs nightly and on demand, plus on any pull request that touches the pin or -# the parity tooling itself. +# result. The dominant term is single-core serial work, which a runner's extra +# cores do not shorten, so the whole population cannot be held to the ~25 min +# a required pull-request check is budgeted for. It therefore runs nightly and +# on demand, plus on any pull request that touches the pin or the parity +# tooling itself. # # PR EARLY SIGNAL. The `corpus-parity-subset` job below # re-runs the 54 probes scripts/corpus_parity_subset.txt names -- in parallel, @@ -80,12 +80,17 @@ env: # pin is almost entirely cache hits. CCACHE_MAXSIZE: 2G +# Both jobs run on the organization's 16-core larger runner for every event +# except a pull request from a fork (this workflow's own or CI's: in a +# workflow_call the github context is the caller's), which runs on the free +# standard runner. The build uses every core of the runner that took the job. +# scripts/ci_preflight.py pins the runners and limits. jobs: # The whole population. Nightly, on demand, and on a pull request that # touches the pin or the parity tooling. Never reached by `mode: subset`. corpus-parity: if: inputs.mode != 'subset' - runs-on: ubuntu-24.04 + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} timeout-minutes: 120 steps: - uses: actions/checkout@v4 @@ -129,9 +134,8 @@ jobs: shell: bash env: BUILD_DIR: build-corpus-parity - JOBS: "4" CMAKE_CXX_COMPILER_LAUNCHER: ccache - run: ./scripts/check_corpus_parity.sh + run: JOBS="$(getconf _NPROCESSORS_ONLN)" ./scripts/check_corpus_parity.sh - name: Collect the drifted trades if: failure() @@ -160,7 +164,7 @@ jobs: # names, judged against the same pinned sha256 rows. Reached through CI. corpus-parity-subset: if: inputs.mode == 'subset' - runs-on: ubuntu-24.04 + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} timeout-minutes: 30 steps: # Credentials stay persisted, exactly as the nightly job above: the @@ -245,9 +249,8 @@ jobs: shell: bash env: BUILD_DIR: build-corpus-parity-subset - JOBS: "4" CMAKE_CXX_COMPILER_LAUNCHER: ccache - run: ./scripts/check_corpus_parity.sh --subset + run: JOBS="$(getconf _NPROCESSORS_ONLN)" ./scripts/check_corpus_parity.sh --subset - name: Collect the drifted trades if: failure() diff --git a/.github/workflows/native-live.yml b/.github/workflows/native-live.yml index 9249e9f6..d367f1f5 100644 --- a/.github/workflows/native-live.yml +++ b/.github/workflows/native-live.yml @@ -27,9 +27,13 @@ env: CURL_VERSION: "8.14.1" CURL_SHA256: "f4619a1e2474c4bbfedc88a7c2191209c8334b48fa1f4e53fd584cc12e9120dd" +# The organization's 16-core larger runner for every event except a pull +# request from a fork, which runs on the free standard runner. In a +# workflow_call the github context is the caller's, so a fork's pull request +# through CI lands there too. scripts/ci_preflight.py pins the runner and limit. jobs: native-live: - runs-on: ubuntu-24.04 + runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} timeout-minutes: 45 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 @@ -105,7 +109,7 @@ jobs: cmake --install curl-build 2>&1 | tee curl-install.log - name: Verify native live - run: python3 scripts/ci_verify.py native --build-dir build-live --jobs 4 --generator Ninja --curl-dir "${{ github.workspace }}/build-native-deps/curl-install/lib/cmake/CURL" --ccache --require-websocket ${{ inputs.exclude_slow && '--exclude-label slow' || '' }} + run: python3 scripts/ci_verify.py native --build-dir build-live --jobs "$(getconf _NPROCESSORS_ONLN)" --generator Ninja --curl-dir "${{ github.workspace }}/build-native-deps/curl-install/lib/cmake/CURL" --ccache --require-websocket ${{ inputs.exclude_slow && '--exclude-label slow' || '' }} - name: Stage and summarize diagnostics if: always() diff --git a/docs/ci.md b/docs/ci.md index ff87d703..742c23ea 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -35,7 +35,8 @@ the preflight source guard rejects those includes before a build. CI pins Ubuntu 24.04 and macOS 26, the platforms used by the preceding green refactor PR, and selects Python 3.12 explicitly. CMake uses the same interpreter as the verification driver, so its Python checks do not switch to a different system -Python. CTest must discover tests; an empty suite is a failure. +Python. CTest must discover tests; an empty suite is a failure. Which runner +runs each image is in [Runners and time limits](#runners-and-time-limits). The two source-only benchmark CTest rows run the 25 harness unit tests and `benchmarks/check_provenance.py`. The latter reads historical commits (including @@ -665,3 +666,43 @@ imply a wall time above 35 min even after excluding the rows. The 25 min target needs a measured sanitizer build improvement; the current job logs do not separate compile from link time or report a ccache hit rate, so CI-LITE does not assume a build optimization without evidence. + +## Runners and time limits + +The heavy jobs -- `preflight`, the four `build` legs, `sanitizers`, +`kernel-only`, `native-live` and both corpus-parity jobs -- run on larger +runners for a push, a manual dispatch, the nightly schedule and a pull request +from a branch of this repository: the organization's `pf-linux-x64-16` (Ubuntu +24.04 x64, 16 cores, 64 GB) and GitHub's `macos-26-xlarge` (the `macos-26` +arm64 image on an M2, 5 cores, 14 GB). A larger runner bills the organization +even for a public repository, so a pull request from a fork runs the same jobs +on the free standard runners, `ubuntu-24.04` (4 vCPU, 16 GB) and `macos-26` +(M1, 3 cores, 7 GB). Every heavy job selects its runner with one test: any +event but a pull request, or a pull request whose head is in this repository. + +```yaml +runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} +``` + +The `build` matrix pairs each image with its `larger_runner` and names its legs +explicitly, so they stay `build (, )`. `native-live.yml` and +`corpus-parity.yml` apply the test themselves; in a `workflow_call` the +`github` context is the caller's, so a fork's pull request through CI gets the +standard runner there too. The advisory `build` aggregate runs a few seconds of +shell and stays on the standard runner for every event. + +Each `ci_verify.py` call passes `--jobs "$(getconf _NPROCESSORS_ONLN)"` and +`check_corpus_parity.sh` gets the same count as `JOBS`, so build and CTest +parallelism follow the runner: 16 on the larger Linux runner, 5 on the M2, 4 +and 3 on the standard runners. The larger Linux runner runs at most eight jobs +at once, and one CI run starts seven there (preflight, the two Ubuntu `build` +legs, sanitizers, kernel-only, native-live and the parity subset), so a second +run that overlaps it waits for a runner. + +`scripts/ci_preflight.py` (`ci-workflow-contract`) pins every job's runner, +time limit and parallelism in the three workflows a CI run starts, and keeps +`docs.yml` and `promote-baseline.yml`, which a fork's pull request can also +start, on standard runners. A heavy job moved back to the standard runner, a +test that lets a fork's pull request onto a larger runner, a changed time +limit, or a fixed `--jobs 4` fails preflight; +`scripts/test_ci_preflight.py` holds the mutations. diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index 6e21a95e..35db4e04 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -34,6 +34,37 @@ # The names live only in tests/CMakeLists.txt. A digest pins that measured # population without maintaining a second row list in this Python guard. PR_SLOW_ROWS_SHA256 = 'e8bddea797abc46ca0deb671d5d35db3491b67a82e1413d08278530d12f7b67b' +# A larger runner bills the organization even for a public repository, so a +# pull request from a fork runs on the free standard runner. Every other event +# -- a push, a dispatch, the schedule, a same-repository pull request -- runs +# the heavy jobs on the organization's 16-core Linux runner or on GitHub's M2 +# runner for the macos-26 image. +TRUSTED_EVENT = ("github.event_name != 'pull_request' || " + "github.event.pull_request.head.repo.full_name == github.repository") +LINUX_RUNNER = '${{ (' + TRUSTED_EVENT + ") && 'pf-linux-x64-16' || 'ubuntu-24.04' }}" +MATRIX_RUNNER = '${{ (' + TRUSTED_EVENT + ') && matrix.larger_runner || matrix.os }}' +BUILD_MATRIX = (' matrix:\n' + ' os: [ubuntu-24.04, macos-26]\n' + ' build_type: [Release, Debug]\n' + ' include:\n' + ' - os: ubuntu-24.04\n' + ' larger_runner: pf-linux-x64-16\n' + ' - os: macos-26\n' + ' larger_runner: macos-26-xlarge\n') +# Build and CTest parallelism is the core count of whichever runner took the job. +CORES = '"$(getconf _NPROCESSORS_ONLN)"' +# Every job with a runner in the workflows a CI run starts: its runs-on and its +# timeout-minutes, exactly. +JOB_RUNNERS = { + 'ci.yml': {'preflight': (LINUX_RUNNER, 10), 'build': (MATRIX_RUNNER, 45), + 'sanitizers': (LINUX_RUNNER, 120), 'kernel-only': (LINUX_RUNNER, 45), + 'build-gate': ('ubuntu-24.04', 5)}, + 'native-live.yml': {'native-live': (LINUX_RUNNER, 45)}, + 'corpus-parity.yml': {'corpus-parity': (LINUX_RUNNER, 120), + 'corpus-parity-subset': (LINUX_RUNNER, 30)}, +} +# The other workflows a pull request (a fork's included) can start. +STANDARD_RUNNERS = ('ubuntu-24.04', 'ubuntu-latest') def _jobs(workflow: str) -> dict[str, str]: @@ -47,9 +78,48 @@ def _jobs(workflow: str) -> dict[str, str]: for index, match in enumerate(matches)} -def ci_workflow_findings(ci: str, native: str, promote: str, cmake: str) -> list[str]: - """Pin the PR-light/full-event split, parallel start, merge statuses and row home.""" +def runner_findings(workflows: dict[str, str]) -> list[str]: + """Pin every CI job's runner, time limit and parallelism; keep forks off larger runners.""" findings = [] + for name, workflow in workflows.items(): + pinned = JOB_RUNNERS.get(name, {}) + jobs = _jobs(workflow) + findings += [f'{name} is missing job {job}' for job in pinned if job not in jobs] + for job, body in jobs.items(): + runs_on = re.findall(r'^ runs-on: (.*)$', body, re.MULTILINE) + limit = re.findall(r'^ timeout-minutes: (.*)$', body, re.MULTILINE) + if job not in pinned: + if pinned and runs_on: + findings.append(f'{name} job {job} needs a pinned runner and time limit') + elif any(value not in STANDARD_RUNNERS for value in runs_on): + findings.append(f'{name} job {job} must stay on a standard runner') + continue + runner, minutes = pinned[job] + if runs_on != [runner]: + findings.append(f'{name} job {job} must run on {runner}') + if limit != [str(minutes)]: + findings.append(f'{name} job {job} must allow {minutes} minutes') + # A job on 16 cores that still asks for 4 wastes the runner it pays for. + runs = re.findall(r'^ +run: (.*)$', body, re.MULTILINE) + if (re.search(r'--jobs [0-9]|^ +JOBS: ', body, re.MULTILINE) + or any(f'--jobs {CORES}' not in run + for run in runs if 'scripts/ci_verify.py' in run) + or any(not run.startswith(f'JOBS={CORES} ./scripts/check_corpus_parity.sh') + for run in runs if 'check_corpus_parity.sh' in run)): + findings.append(f'{name} job {job} must size its parallelism to the runner') + build = _jobs(workflows.get('ci.yml', '')).get('build', '') + if (BUILD_MATRIX not in build + or ' name: build (${{ matrix.os }}, ${{ matrix.build_type }})\n' not in build): + findings.append('ci.yml build must pair each image with its larger runner') + return findings + + +def ci_workflow_findings(ci: str, native: str, promote: str, cmake: str, + parity: str, docs: str) -> list[str]: + """Pin the PR-light/full-event split, parallel start, merge statuses, row home and runners.""" + findings = runner_findings({'ci.yml': ci, 'native-live.yml': native, + 'corpus-parity.yml': parity, 'docs.yml': docs, + 'promote-baseline.yml': promote}) events = ci.split('\non:\n', 1) events = events[1].split('\npermissions:', 1)[0] if len(events) == 2 else '' for trigger in ('push:\n branches: [main]', @@ -338,11 +408,14 @@ def main() -> int: (ROOT / '.github/workflows/ci.yml').read_text(), (ROOT / '.github/workflows/native-live.yml').read_text(), (ROOT / '.github/workflows/promote-baseline.yml').read_text(), - (ROOT / 'tests/CMakeLists.txt').read_text()) + (ROOT / 'tests/CMakeLists.txt').read_text(), + (ROOT / '.github/workflows/corpus-parity.yml').read_text(), + (ROOT / '.github/workflows/docs.yml').read_text()) for finding in findings: print(finding) if not findings: - print('CI workflow contract: PR exclusions, full events, statuses and slow rows OK') + print('CI workflow contract: PR exclusions, full events, statuses, slow rows, ' + 'runners and time limits OK') return 1 if findings else 0 if args.check_docs_workflow or args.self_test_docs_workflow: workflow = (ROOT / '.github/workflows/docs.yml').read_text() diff --git a/scripts/test_ci_preflight.py b/scripts/test_ci_preflight.py index c6f492e1..e131a6ee 100644 --- a/scripts/test_ci_preflight.py +++ b/scripts/test_ci_preflight.py @@ -4,19 +4,34 @@ import io import json from pathlib import Path +import shutil +import subprocess import sys import tempfile import unittest -from ci_preflight import check_commands, ci_workflow_findings, run_checks +from ci_preflight import (CORES, LINUX_RUNNER, MATRIX_RUNNER, _jobs, check_commands, + ci_workflow_findings, run_checks) + +ROOT = Path(__file__).resolve().parents[1] +# ci_workflow_findings' arguments, in order. +CI_SOURCES = ('.github/workflows/ci.yml', '.github/workflows/native-live.yml', + '.github/workflows/promote-baseline.yml', 'tests/CMakeLists.txt', + '.github/workflows/corpus-parity.yml', '.github/workflows/docs.yml') + + +def in_job(workflow, job, before, after): + """Replace the first `before` inside job `job` (anywhere when job is None).""" + start = 0 if job is None else workflow.index(f'\n {job}:\n') + if job is not None and before not in _jobs(workflow)[job]: + raise AssertionError(f'{before!r} is not in job {job}') + at = workflow.index(before, start) + return workflow[:at] + after + workflow[at + len(before):] class PreflightFailures(unittest.TestCase): def ci_sources(self): - root = Path(__file__).resolve().parents[1] - return [(root / path).read_text() for path in ( - '.github/workflows/ci.yml', '.github/workflows/native-live.yml', - '.github/workflows/promote-baseline.yml', 'tests/CMakeLists.txt')] + return [(ROOT / path).read_text() for path in CI_SOURCES] def test_ci_contract_pins_parallel_pr_light_and_full_events(self): original = self.ci_sources() @@ -79,6 +94,114 @@ def test_ci_contract_pins_parallel_pr_light_and_full_events(self): changed[index] = changed[index].replace(before, after, 1) self.assertNotEqual(ci_workflow_findings(*changed), []) + def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): + original = self.ci_sources() + self.assertEqual(ci_workflow_findings(*original), []) + fork_head = 'github.event.pull_request.head.repo.full_name' + mutations = ( + # A heavy job moved back to the slow standard runner. + (0, 'preflight', LINUX_RUNNER, 'ubuntu-24.04', 'ci.yml job preflight must run on'), + (0, 'sanitizers', LINUX_RUNNER, 'ubuntu-24.04', 'ci.yml job sanitizers must run on'), + (0, 'kernel-only', LINUX_RUNNER, 'ubuntu-24.04', 'ci.yml job kernel-only must run on'), + (0, 'build', MATRIX_RUNNER, '${{ matrix.os }}', 'ci.yml job build must run on'), + (0, 'build', 'larger_runner: macos-26-xlarge', 'larger_runner: macos-26', + 'ci.yml build must pair each image'), + (0, 'build', 'larger_runner: pf-linux-x64-16', 'larger_runner: ubuntu-24.04', + 'ci.yml build must pair each image'), + (1, 'native-live', LINUX_RUNNER, 'ubuntu-24.04', 'native-live.yml job native-live must run on'), + (4, 'corpus-parity', LINUX_RUNNER, 'ubuntu-24.04', 'corpus-parity.yml job corpus-parity must run on'), + (4, 'corpus-parity-subset', LINUX_RUNNER, 'ubuntu-24.04', + 'corpus-parity.yml job corpus-parity-subset must run on'), + # A fork's pull request let onto a larger runner, which bills the org. + (0, 'preflight', LINUX_RUNNER, 'pf-linux-x64-16', 'ci.yml job preflight must run on'), + (0, 'sanitizers', fork_head, 'github.event.pull_request.base.repo.full_name', + 'ci.yml job sanitizers must run on'), + (0, 'kernel-only', f'{fork_head} == github.repository', f"{fork_head} != ''", + 'ci.yml job kernel-only must run on'), + (0, 'build', MATRIX_RUNNER, '${{ matrix.larger_runner }}', 'ci.yml job build must run on'), + (0, 'build', 'larger_runner: macos-26-xlarge', 'larger_runner: macos-26-large', + 'ci.yml build must pair each image'), + (1, 'native-live', "|| 'ubuntu-24.04'", "|| 'pf-linux-x64-16'", + 'native-live.yml job native-live must run on'), + (4, 'corpus-parity-subset', f'runs-on: {LINUX_RUNNER}', 'runs-on:\n group: pineforge-ci-large', + 'corpus-parity.yml job corpus-parity-subset must run on'), + (0, 'build-gate', 'runs-on: ubuntu-24.04', 'runs-on: pf-linux-x64-16', + 'ci.yml job build-gate must run on ubuntu-24.04'), + (5, 'build', 'runs-on: ubuntu-latest', 'runs-on: pf-linux-x64-16', + 'docs.yml job build must stay on a standard runner'), + (2, 'promote', 'runs-on: ubuntu-latest', 'runs-on: macos-26-xlarge', + 'promote-baseline.yml job promote must stay on a standard runner'), + (0, None, ' build-gate:\n', ' extra:\n runs-on: ubuntu-24.04\n steps:\n' + ' - run: "true"\n\n build-gate:\n', 'ci.yml job extra needs a pinned runner'), + # A changed time limit. + (0, 'preflight', 'timeout-minutes: 10', 'timeout-minutes: 30', + 'ci.yml job preflight must allow 10 minutes'), + (0, 'build', 'timeout-minutes: 45', 'timeout-minutes: 75', 'ci.yml job build must allow 45'), + (0, 'sanitizers', 'timeout-minutes: 120', 'timeout-minutes: 60', + 'ci.yml job sanitizers must allow 120'), + (0, 'kernel-only', 'timeout-minutes: 45', 'timeout-minutes: 60', + 'ci.yml job kernel-only must allow 45'), + (0, 'build-gate', ' timeout-minutes: 5\n', '', 'ci.yml job build-gate must allow 5'), + (1, 'native-live', 'timeout-minutes: 45', 'timeout-minutes: 60', + 'native-live.yml job native-live must allow 45'), + (4, 'corpus-parity', 'timeout-minutes: 120', 'timeout-minutes: 30', + 'corpus-parity.yml job corpus-parity must allow 120'), + (4, 'corpus-parity-subset', 'timeout-minutes: 30', 'timeout-minutes: 10', + 'corpus-parity.yml job corpus-parity-subset must allow 30'), + # Four jobs asked of a 16-core runner. + (0, 'build', f'--jobs {CORES}', '--jobs 4', 'ci.yml job build must size'), + (0, 'sanitizers', f'--jobs {CORES}', '--jobs 4', 'ci.yml job sanitizers must size'), + (0, 'kernel-only', f'--jobs {CORES}', '--jobs "$(nproc)"', 'ci.yml job kernel-only must size'), + (1, 'native-live', f'--jobs {CORES}', '--jobs 4', 'native-live.yml job native-live must size'), + (4, 'corpus-parity', f'JOBS={CORES} ./scripts', './scripts', + 'corpus-parity.yml job corpus-parity must size'), + (4, 'corpus-parity-subset', ' BUILD_DIR: build-corpus-parity-subset\n', + ' BUILD_DIR: build-corpus-parity-subset\n JOBS: "4"\n', + 'corpus-parity.yml job corpus-parity-subset must size'), + # The build legs keep their names: build (, ). + (0, 'build', ' name: build (${{ matrix.os }}, ${{ matrix.build_type }})\n', '', + 'ci.yml build must pair each image'), + ) + for index, job, before, after, finding in mutations: + with self.subTest(file=CI_SOURCES[index], job=job, after=after): + changed = original.copy() + changed[index] = in_job(changed[index], job, before, after) + findings = ci_workflow_findings(*changed) + self.assertTrue(any(finding in line for line in findings), findings) + + def contract_stage(self, mutation): + """Run the ci-workflow-contract stage's argv in a copy of the tree.""" + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + tree = Path(directory.name) + # A Python row CTest runs beside this one writes its bytecode through + # a temporary file that can vanish mid-copy: never copy bytecode. + shutil.copytree(ROOT / 'scripts', tree / 'scripts', + ignore=shutil.ignore_patterns('__pycache__', '*.pyc')) + for path in CI_SOURCES: + (tree / path).parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ROOT / path, tree / path) + if mutation: + ci = tree / '.github/workflows/ci.yml' + ci.write_text(in_job(ci.read_text(), *mutation)) + result = subprocess.run( + [sys.executable, str(tree / 'scripts/ci_preflight.py'), '--check-ci-workflow'], + cwd=tree, capture_output=True, text=True, timeout=120) + return result.returncode, result.stdout + result.stderr + + def test_the_preflight_stage_fails_a_slow_or_fork_reachable_runner(self): + code, output = self.contract_stage(None) + self.assertEqual(code, 0, output) + self.assertIn('runners and time limits OK', output) + for mutation, finding in ( + (('sanitizers', LINUX_RUNNER, 'ubuntu-24.04'), 'ci.yml job sanitizers must run on'), + (('sanitizers', 'pull_request.head.repo', 'pull_request.base.repo'), + 'ci.yml job sanitizers must run on')): + with self.subTest(mutation=mutation): + code, output = self.contract_stage(mutation) + self.assertEqual(code, 1, output) + self.assertIn(finding, output) + def run_preflight(self, commands): directory = tempfile.TemporaryDirectory() self.addCleanup(directory.cleanup) From 9061e981e339065ade83c856fa5fd6d19ba72aed Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 07:11:52 +0800 Subject: [PATCH 60/83] Size CI time limits to the runners each job can land on Each timeout-minutes is now at least twice the slowest time measured for the job on a runner it can land on: the standard runners over the eight CI runs from 8cf3be58 to 0d76a099 (a fork's pull request still gets them), and the maintainers' x86-64 verification hosts standing in for the larger Linux runner until its own runs accumulate. - preflight 10 -> 30: main's run at 0d76a099 was cancelled at 10.3 min, still inside verifier-tests, which took 394-543 s before and passed 570 s. One preflight stage is now bounded at 1500 s (was 900 s), inside the job. - build 45 -> 75: a Release leg runs its full population on every event, a fork's pull request included, and took 30.3 and 32.0 min at 0d76a099; the full macOS Debug leg took 37.0 min on the M1 that bounds the M2. - kernel-only 45 -> 60: 22.3 min at 0d76a099 for the whole kernel set, which every event runs. - native-live 45 -> 60: the full set waits on the test_ci_verify CTest row (1034 s at 0d76a099), which more cores do not shorten. - sanitizers 120, corpus-parity 120, corpus-parity-subset 30 and the build aggregate 5 hold at least twice their measured times and stay. ci-workflow-contract pins the new values, test_ci_preflight.py keeps the stage bound inside the preflight job, and docs/ci.md gives the table with each limit's basis. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 6419c7fd822429bc44c3c34263467a1bd2b9b758) --- .github/workflows/ci.yml | 20 ++++++++++++++----- .github/workflows/corpus-parity.yml | 2 ++ .github/workflows/native-live.yml | 4 +++- docs/ci.md | 31 ++++++++++++++++++++++++++++- scripts/ci_preflight.py | 20 +++++++++++-------- scripts/test_ci_preflight.py | 27 ++++++++++++++++--------- 6 files changed, 80 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5776c98d..57cf3abc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,11 +27,14 @@ env: # pull request from a fork: a larger runner bills the organization even for a # public repository, so a fork's pull request runs on the free standard runner. # Each verification sizes its parallelism to the runner that took it. -# scripts/ci_preflight.py pins every runner, time limit and job count. +# scripts/ci_preflight.py pins every runner, time limit and job count, and +# docs/ci.md gives each limit's measured basis. jobs: preflight: runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} - timeout-minutes: 10 + # The verifier self-tests alone took 394-543 s on the standard runner and + # passed 570 s at 0d76a099, where ten minutes cancelled main's run. + timeout-minutes: 30 steps: - uses: actions/checkout@v4 with: @@ -75,7 +78,11 @@ jobs: build: # Each leg keeps its name, build (, ), whatever the matrix carries. name: build (${{ matrix.os }}, ${{ matrix.build_type }}) - timeout-minutes: 45 + # A full-set leg waits on the test_ci_verify CTest row, 976-1288 s on the + # standard runners at 0d76a099, which more cores do not shorten. There the + # Release legs, which a fork's pull request runs in full, took 30.3 and + # 32.0 min, and the macOS Debug leg 37.0 min. + timeout-minutes: 75 strategy: fail-fast: false matrix: @@ -159,7 +166,8 @@ jobs: sanitizers: runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # A full run (push to main, manual dispatch) gives its CTest stage an hour - # after a build of about 30 minutes; the PR set keeps its 30-minute stage. + # after the build; the PR set keeps its 30-minute stage, after a build of + # about 30 minutes on the standard runner a fork's pull request gets. timeout-minutes: 120 env: ASAN_OPTIONS: detect_leaks=1:halt_on_error=1:abort_on_error=1 @@ -210,7 +218,9 @@ jobs: # runs as an example_* row asserting both its exit code and its summary line. kernel-only: runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} - timeout-minutes: 45 + # 22.3 min on the standard runner at 0d76a099, where a fork's pull request + # still runs the whole kernel set. + timeout-minutes: 60 steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/corpus-parity.yml b/.github/workflows/corpus-parity.yml index 716bc91e..b3f13322 100644 --- a/.github/workflows/corpus-parity.yml +++ b/.github/workflows/corpus-parity.yml @@ -91,6 +91,7 @@ jobs: corpus-parity: if: inputs.mode != 'subset' runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + # 16.3-24.4 min on the standard runner a fork's pull request still gets. timeout-minutes: 120 steps: - uses: actions/checkout@v4 @@ -165,6 +166,7 @@ jobs: corpus-parity-subset: if: inputs.mode == 'subset' runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + # 1.8-4.7 min on the standard runner. timeout-minutes: 30 steps: # Credentials stay persisted, exactly as the nightly job above: the diff --git a/.github/workflows/native-live.yml b/.github/workflows/native-live.yml index d367f1f5..0010966b 100644 --- a/.github/workflows/native-live.yml +++ b/.github/workflows/native-live.yml @@ -34,7 +34,9 @@ env: jobs: native-live: runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} - timeout-minutes: 45 + # The full set waits on the test_ci_verify CTest row, 1034 s on the + # standard runner at 0d76a099, which more cores do not shorten. + timeout-minutes: 60 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: diff --git a/docs/ci.md b/docs/ci.md index 742c23ea..6763e0cb 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -36,7 +36,8 @@ CI pins Ubuntu 24.04 and macOS 26, the platforms used by the preceding green refactor PR, and selects Python 3.12 explicitly. CMake uses the same interpreter as the verification driver, so its Python checks do not switch to a different system Python. CTest must discover tests; an empty suite is a failure. Which runner -runs each image is in [Runners and time limits](#runners-and-time-limits). +runs each image, and each job's time limit, is in +[Runners and time limits](#runners-and-time-limits). The two source-only benchmark CTest rows run the 25 harness unit tests and `benchmarks/check_provenance.py`. The latter reads historical commits (including @@ -706,3 +707,31 @@ start, on standard runners. A heavy job moved back to the standard runner, a test that lets a fork's pull request onto a larger runner, a changed time limit, or a fixed `--jobs 4` fails preflight; `scripts/test_ci_preflight.py` holds the mutations. + +More cores do not shorten every job. The `test_ci_verify` CTest row +(`scripts/test_ci_verify.py`) is one Python process, and in the main run at +0d76a099 it was the whole CTest wall time of every full-set leg but +sanitizers: 976-1288 s on the standard runners, against 455-620 s at 8cf3be58 +the day before. Preflight's `verifier-tests` stage runs the same suite: +394-543 s on the standard runner, and past 570 s at 0d76a099, when the old +ten-minute limit cancelled main's preflight. The full corpus sweep's run phase +is serial as well. + +Each limit below is at least twice the slowest time measured for the job on a +runner it can land on. The standard-runner times are the eight CI runs from +main's 8cf3be58 (2026-09-24) to 0d76a099 (2026-09-25), pull requests included, +and the corpus-parity workflow's own runs of 2026-09-23 to 2026-09-25. +Until the larger runners' own runs accumulate, the maintainers' x86-64 +verification hosts at 8 to 12 jobs stand in for the Linux one, and the 3-core +M1 bounds the M2 from above. + +| Job | Limit (min) | Measured basis | +| --- | --- | --- | +| `preflight` | 30 (was 10) | 7.3-9.8 min on the standard runner; cancelled at 10.3 min at 0d76a099, still in `verifier-tests`. 4.8-8.2 min on the verification hosts. Each stage is bounded at 25 min inside it. | +| `build` | 75 (was 45) | A Release leg runs its full population on every event, a fork's pull request included: 30.3 min Ubuntu and 32.0 min macOS on the standard runners at 0d76a099, where the full Debug legs took 43.5 and 37.0 min. Pull-request Debug sets 17.2-17.7 min. 11.1-18.4 min on the verification hosts. | +| `sanitizers` | 120 | The verifier bounds a full run's CTest stage at 60 min after the build, a pull-request set's at 30 min after a standard-runner build of about 30 min. Pull-request set 35.2 min and full runs 34.0-60.0 min on the standard runner; full runs 18.2-19.0 min on the verification hosts. | +| `kernel-only` | 60 (was 45) | Every event runs the whole kernel set: 7.9-22.3 min on the standard runner, 6.5-9.8 min on the verification hosts. | +| `native-live` | 60 (was 45) | Only the larger runner runs the full set, which waits on `test_ci_verify` (1034 s as a row on the standard runner at 0d76a099); the verification hosts' release profile, the nearest they run, took 11.2-18.4 min. A fork's pull request runs the set without that row: 3.4 min on the standard runner. | +| `corpus-parity` | 120 | 16.3-24.4 min on the standard runner, 10.1-10.3 min on the verification hosts. | +| `corpus-parity-subset` | 30 | 1.8-4.7 min on the standard runner, 2.4-3.8 min on the verification hosts. | +| `build` (aggregate) | 5 | Seconds. | diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index 35db4e04..eaed7dfa 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -54,17 +54,23 @@ # Build and CTest parallelism is the core count of whichever runner took the job. CORES = '"$(getconf _NPROCESSORS_ONLN)"' # Every job with a runner in the workflows a CI run starts: its runs-on and its -# timeout-minutes, exactly. +# timeout-minutes, exactly. docs/ci.md gives each limit's measured basis. JOB_RUNNERS = { - 'ci.yml': {'preflight': (LINUX_RUNNER, 10), 'build': (MATRIX_RUNNER, 45), - 'sanitizers': (LINUX_RUNNER, 120), 'kernel-only': (LINUX_RUNNER, 45), + 'ci.yml': {'preflight': (LINUX_RUNNER, 30), 'build': (MATRIX_RUNNER, 75), + 'sanitizers': (LINUX_RUNNER, 120), 'kernel-only': (LINUX_RUNNER, 60), 'build-gate': ('ubuntu-24.04', 5)}, - 'native-live.yml': {'native-live': (LINUX_RUNNER, 45)}, + 'native-live.yml': {'native-live': (LINUX_RUNNER, 60)}, 'corpus-parity.yml': {'corpus-parity': (LINUX_RUNNER, 120), 'corpus-parity-subset': (LINUX_RUNNER, 30)}, } # The other workflows a pull request (a fork's included) can start. STANDARD_RUNNERS = ('ubuntu-24.04', 'ubuntu-latest') +# One stage's bound. The verifier self-tests (test_ci_verify.py) drive the real +# literal-aware parity, receipt and submodule guards (L8d) and took 394-543 s +# on the standard hosted runner, past 570 s at 0d76a099. The bound stays inside +# the preflight job's time limit, so a stuck stage is logged here rather than +# cut off with the job. +STAGE_TIMEOUT_SECONDS = 1500 def _jobs(workflow: str) -> dict[str, str]: @@ -369,10 +375,8 @@ def record() -> None: summary['stages'].append(stage) record() try: - # 900 s: the verifier self-tests (test_ci_verify.py) now drive the real - # literal-aware parity, receipt and submodule guards (L8d) and take - # ~80 s locally, >180 s on the hosted runner. - result = subprocess.run(argv, cwd=source, capture_output=True, timeout=900) + result = subprocess.run(argv, cwd=source, capture_output=True, + timeout=STAGE_TIMEOUT_SECONDS) code, log = result.returncode, result.stdout + result.stderr except subprocess.TimeoutExpired as error: code = 124 diff --git a/scripts/test_ci_preflight.py b/scripts/test_ci_preflight.py index e131a6ee..decff141 100644 --- a/scripts/test_ci_preflight.py +++ b/scripts/test_ci_preflight.py @@ -10,7 +10,8 @@ import tempfile import unittest -from ci_preflight import (CORES, LINUX_RUNNER, MATRIX_RUNNER, _jobs, check_commands, +from ci_preflight import (CORES, JOB_RUNNERS, LINUX_RUNNER, MATRIX_RUNNER, + STAGE_TIMEOUT_SECONDS, _jobs, check_commands, ci_workflow_findings, run_checks) ROOT = Path(__file__).resolve().parents[1] @@ -133,17 +134,17 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): 'promote-baseline.yml job promote must stay on a standard runner'), (0, None, ' build-gate:\n', ' extra:\n runs-on: ubuntu-24.04\n steps:\n' ' - run: "true"\n\n build-gate:\n', 'ci.yml job extra needs a pinned runner'), - # A changed time limit. - (0, 'preflight', 'timeout-minutes: 10', 'timeout-minutes: 30', - 'ci.yml job preflight must allow 10 minutes'), - (0, 'build', 'timeout-minutes: 45', 'timeout-minutes: 75', 'ci.yml job build must allow 45'), + # A time limit changed away from its measured basis. + (0, 'preflight', 'timeout-minutes: 30', 'timeout-minutes: 10', + 'ci.yml job preflight must allow 30 minutes'), + (0, 'build', 'timeout-minutes: 75', 'timeout-minutes: 45', 'ci.yml job build must allow 75'), (0, 'sanitizers', 'timeout-minutes: 120', 'timeout-minutes: 60', 'ci.yml job sanitizers must allow 120'), - (0, 'kernel-only', 'timeout-minutes: 45', 'timeout-minutes: 60', - 'ci.yml job kernel-only must allow 45'), + (0, 'kernel-only', 'timeout-minutes: 60', 'timeout-minutes: 45', + 'ci.yml job kernel-only must allow 60'), (0, 'build-gate', ' timeout-minutes: 5\n', '', 'ci.yml job build-gate must allow 5'), - (1, 'native-live', 'timeout-minutes: 45', 'timeout-minutes: 60', - 'native-live.yml job native-live must allow 45'), + (1, 'native-live', 'timeout-minutes: 60', 'timeout-minutes: 45', + 'native-live.yml job native-live must allow 60'), (4, 'corpus-parity', 'timeout-minutes: 120', 'timeout-minutes: 30', 'corpus-parity.yml job corpus-parity must allow 120'), (4, 'corpus-parity-subset', 'timeout-minutes: 30', 'timeout-minutes: 10', @@ -169,6 +170,14 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): findings = ci_workflow_findings(*changed) self.assertTrue(any(finding in line for line in findings), findings) + def test_a_stage_times_out_inside_the_preflight_job(self): + # Five minutes stay for the runner setup and the other stages, so the + # stage's own timeout, with its log, fires before the job's. + job_seconds = JOB_RUNNERS['ci.yml']['preflight'][1] * 60 + self.assertLessEqual(STAGE_TIMEOUT_SECONDS + 300, job_seconds) + # And at least twice the slowest verifier-tests stage measured: 570 s. + self.assertGreaterEqual(STAGE_TIMEOUT_SECONDS, 2 * 570) + def contract_stage(self, mutation): """Run the ci-workflow-contract stage's argv in a copy of the tree.""" directory = tempfile.TemporaryDirectory() From a81dfff11525e94558e088c0518329d7fe85ed69 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 07:52:05 +0800 Subject: [PATCH 61/83] Close the runner contract's gaps a fork's pull request could slip through An independent review reproduced edits that put a fork's pull request on a larger runner, or a heavy job back on four jobs, with no finding: - a runs-on block value (group: / a label list), which the one-line regex never saw -- in docs.yml, promote-baseline.yml or a new ci.yml job; - an extra build include entry: a leg whose os is a larger runner, or one that overrides a leg's larger_runner (the matrix was pinned as a substring); - a job id with an underscore, or text before the first job, which the job parser dropped; - a multi-line run block, --jobs=N, or a later --jobs, which the single-line run scan missed; and a comment naming --jobs 4 was a false finding. The contract now folds a runs-on block value onto one line, pins the build job's whole strategy block and its name separately, parses every job id GitHub accepts (a header comment included) and reports any jobs line it cannot read, scans every non-comment line of a pinned job for --jobs and JOBS, and applies the standard-runner rule to every workflow file a pull request can start; one only a push, the schedule or a dispatch starts (release.yml) is exempt. Findings are deduplicated. test_ci_preflight.py adds each reproduced edit as a mutation, the comment and header-comment cases as no-finding checks, and the other-workflow rule; the stage test now runs on a copy of the whole workflows directory. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 6f744e87390d532572cf13aabb9ed13a50c2085b) INT26 pick note: the docs/ci.md sentence this commit rewrites was this lane's own first commit's (7b4adb0). --- docs/ci.md | 15 +-- scripts/ci_preflight.py | 191 +++++++++++++++++++++++++++-------- scripts/test_ci_preflight.py | 87 ++++++++++++++-- 3 files changed, 237 insertions(+), 56 deletions(-) diff --git a/docs/ci.md b/docs/ci.md index 6763e0cb..7eac0a58 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -701,12 +701,15 @@ legs, sanitizers, kernel-only, native-live and the parity subset), so a second run that overlaps it waits for a runner. `scripts/ci_preflight.py` (`ci-workflow-contract`) pins every job's runner, -time limit and parallelism in the three workflows a CI run starts, and keeps -`docs.yml` and `promote-baseline.yml`, which a fork's pull request can also -start, on standard runners. A heavy job moved back to the standard runner, a -test that lets a fork's pull request onto a larger runner, a changed time -limit, or a fixed `--jobs 4` fails preflight; -`scripts/test_ci_preflight.py` holds the mutations. +time limit and parallelism in the three workflows a CI run starts, the `build` +strategy block included, and keeps every other workflow a pull request can +start -- `docs.yml`, `promote-baseline.yml` and any new one -- on standard +runners; one that only a push, the schedule or a dispatch starts +(`release.yml`) is exempt. A heavy job moved back to the standard runner, a +test or matrix entry that lets a fork's pull request onto a larger runner, a +changed time limit, or any other job count fails preflight, and so does a +jobs line the check cannot read; `scripts/test_ci_preflight.py` holds the +mutations. More cores do not shorten every job. The `test_ci_verify` CTest row (`scripts/test_ci_verify.py`) is one Python process, and in the main run at diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index eaed7dfa..857906ec 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -43,14 +43,19 @@ "github.event.pull_request.head.repo.full_name == github.repository") LINUX_RUNNER = '${{ (' + TRUSTED_EVENT + ") && 'pf-linux-x64-16' || 'ubuntu-24.04' }}" MATRIX_RUNNER = '${{ (' + TRUSTED_EVENT + ') && matrix.larger_runner || matrix.os }}' -BUILD_MATRIX = (' matrix:\n' - ' os: [ubuntu-24.04, macos-26]\n' - ' build_type: [Release, Debug]\n' - ' include:\n' - ' - os: ubuntu-24.04\n' - ' larger_runner: pf-linux-x64-16\n' - ' - os: macos-26\n' - ' larger_runner: macos-26-xlarge\n') +# The build job's whole strategy, exactly: an include entry more (a leg whose +# os is a larger runner, or one that overrides a larger_runner) is a finding. +BUILD_STRATEGY = (' strategy:\n' + ' fail-fast: false\n' + ' matrix:\n' + ' os: [ubuntu-24.04, macos-26]\n' + ' build_type: [Release, Debug]\n' + ' include:\n' + ' - os: ubuntu-24.04\n' + ' larger_runner: pf-linux-x64-16\n' + ' - os: macos-26\n' + ' larger_runner: macos-26-xlarge\n') +BUILD_NAME = 'build (${{ matrix.os }}, ${{ matrix.build_type }})' # Build and CTest parallelism is the core count of whichever runner took the job. CORES = '"$(getconf _NPROCESSORS_ONLN)"' # Every job with a runner in the workflows a CI run starts: its runs-on and its @@ -63,37 +68,126 @@ 'corpus-parity.yml': {'corpus-parity': (LINUX_RUNNER, 120), 'corpus-parity-subset': (LINUX_RUNNER, 30)}, } -# The other workflows a pull request (a fork's included) can start. +# Every other workflow a pull request (a fork's included) can start keeps its +# jobs on these. One that only a push, the schedule or a dispatch starts is free. STANDARD_RUNNERS = ('ubuntu-24.04', 'ubuntu-latest') +TRUSTED_ONLY_EVENTS = {'push', 'schedule', 'workflow_dispatch'} # One stage's bound. The verifier self-tests (test_ci_verify.py) drive the real -# literal-aware parity, receipt and submodule guards (L8d) and took 394-543 s -# on the standard hosted runner, past 570 s at 0d76a099. The bound stays inside -# the preflight job's time limit, so a stuck stage is logged here rather than -# cut off with the job. +# literal-aware parity, receipt and submodule guards and took 394-543 s on the +# standard hosted runner, past 570 s at 0d76a099. The bound stays inside the +# preflight job's time limit, so a stuck stage is logged here rather than cut +# off with the job. STAGE_TIMEOUT_SECONDS = 1500 +# A job's header -- any id GitHub accepts -- and the top-level jobs and on keys. +_JOB_HEADER = re.compile(r'^ ([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(?:#.*)?$', re.MULTILINE) +_JOBS_KEY = re.compile(r'^jobs:[ \t]*(?:#.*)?$', re.MULTILINE) +_ON_KEY = re.compile(r'^(?:on|"on"|\'on\'):[ \t]*([^#\n]*)', re.MULTILINE) + + +def _jobs_text(workflow: str) -> str | None: + """The jobs mapping: from the jobs key to the next top-level key.""" + key = _JOBS_KEY.search(workflow) + if not key: + return None + text = workflow[key.end():] + after = re.search(r'^[^\s#]', text, re.MULTILINE) + return text[:after.start()] if after else text def _jobs(workflow: str) -> dict[str, str]: - body = workflow.split('\njobs:\n', 1) - if len(body) != 2: - return {} - matches = list(re.finditer(r'^ ([a-z][a-z0-9-]*):\s*$', body[1], re.MULTILINE)) - return {match.group(1): body[1][match.end(): - matches[index + 1].start() if index + 1 < len(matches) - else len(body[1])] + text = _jobs_text(workflow) or '' + matches = list(_JOB_HEADER.finditer(text)) + return {match.group(1): text[match.end(): + matches[index + 1].start() if index + 1 < len(matches) + else len(text)] for index, match in enumerate(matches)} +def _code(text: str) -> list[str]: + return [line for line in text.split('\n') + if line.strip() and not line.lstrip().startswith('#')] + + +def _indent(line: str) -> int: + return len(line) - len(line.lstrip(' ')) + + +def _unparsed(workflow: str) -> list[str]: + """Lines of the jobs mapping no job header accounts for: the parse fails closed.""" + text = _jobs_text(workflow) + if text is None: + return ['jobs:'] + first = _JOB_HEADER.search(text) + stray = _code(text[:first.start()] if first else text) + for body in _jobs(workflow).values(): + stray += [line for line in _code(body) if _indent(line) <= 2] + return stray + + +def _job_values(body: str, key: str) -> list[str]: + """Every value of a job-level key, a block value's lines folded onto one.""" + lines = body.split('\n') + code = _code(body) + if not code: + return [] + depth = _indent(code[0]) + values = [] + for index, line in enumerate(lines): + if _indent(line) != depth or not line.lstrip().startswith(key + ':'): + continue + parts = [line.strip()[len(key) + 1:]] + for follow in lines[index + 1:]: + if follow.strip() and _indent(follow) <= depth: + break + parts.append(follow) + values.append(' '.join(re.sub(r'\s+#.*$', '', part).strip() for part in parts + if part.strip() and not part.lstrip().startswith('#'))) + return values + + +def _job_block(body: str, key: str) -> str: + """A job-level key's line and every line under it, as written.""" + lines = body.split('\n') + code = _code(body) + depth = _indent(code[0]) if code else 0 + for index, line in enumerate(lines): + if line == ' ' * depth + key + ':': + block = [line] + for follow in lines[index + 1:]: + if follow.strip() and _indent(follow) <= depth: + break + block.append(follow) + return '\n'.join(block).rstrip() + '\n' + return '' + + +def _events(workflow: str) -> set[str] | None: + """The events that start a workflow, or None when the on key cannot be read.""" + match = _ON_KEY.search(workflow) + if not match: + return None + if match.group(1).strip(): + return set(re.findall(r'[A-Za-z_]+', match.group(1))) + block = workflow[match.end():] + after = re.search(r'^[^\s#]', block, re.MULTILINE) + block = block[:after.start()] if after else block + return set(re.findall(r'^ ([A-Za-z_]+):', block, re.MULTILINE)) or None + + def runner_findings(workflows: dict[str, str]) -> list[str]: """Pin every CI job's runner, time limit and parallelism; keep forks off larger runners.""" findings = [] for name, workflow in workflows.items(): pinned = JOB_RUNNERS.get(name, {}) + events = _events(workflow) + if not pinned and events is not None and events <= TRUSTED_ONLY_EVENTS: + continue + if _unparsed(workflow): + findings.append(f'{name} has jobs lines the runner contract cannot read') jobs = _jobs(workflow) findings += [f'{name} is missing job {job}' for job in pinned if job not in jobs] for job, body in jobs.items(): - runs_on = re.findall(r'^ runs-on: (.*)$', body, re.MULTILINE) - limit = re.findall(r'^ timeout-minutes: (.*)$', body, re.MULTILINE) + runs_on = _job_values(body, 'runs-on') if job not in pinned: if pinned and runs_on: findings.append(f'{name} job {job} needs a pinned runner and time limit') @@ -103,29 +197,37 @@ def runner_findings(workflows: dict[str, str]) -> list[str]: runner, minutes = pinned[job] if runs_on != [runner]: findings.append(f'{name} job {job} must run on {runner}') - if limit != [str(minutes)]: + if _job_values(body, 'timeout-minutes') != [str(minutes)]: findings.append(f'{name} job {job} must allow {minutes} minutes') # A job on 16 cores that still asks for 4 wastes the runner it pays for. - runs = re.findall(r'^ +run: (.*)$', body, re.MULTILINE) - if (re.search(r'--jobs [0-9]|^ +JOBS: ', body, re.MULTILINE) - or any(f'--jobs {CORES}' not in run - for run in runs if 'scripts/ci_verify.py' in run) - or any(not run.startswith(f'JOBS={CORES} ./scripts/check_corpus_parity.sh') - for run in runs if 'check_corpus_parity.sh' in run)): - findings.append(f'{name} job {job} must size its parallelism to the runner') + # Every line counts, a multi-line run block's included; comments do not. + for line in _code(body): + command = re.sub(r'^\s*(?:-\s+)?run:\s*', '', line) + if (line.count('--jobs') != line.count(f'--jobs {CORES}') + or ('scripts/ci_verify.py' in line and f'--jobs {CORES}' not in line) + or ((re.search(r'\bJOBS\s*[:=]', line) + or 'check_corpus_parity.sh' in line) + and not command.startswith( + f'JOBS={CORES} ./scripts/check_corpus_parity.sh'))): + findings.append(f'{name} job {job} must size its parallelism to the runner') + break build = _jobs(workflows.get('ci.yml', '')).get('build', '') - if (BUILD_MATRIX not in build - or ' name: build (${{ matrix.os }}, ${{ matrix.build_type }})\n' not in build): - findings.append('ci.yml build must pair each image with its larger runner') + if _job_block(build, 'strategy') != BUILD_STRATEGY: + findings.append('ci.yml build must pair each image with its larger runner, exactly') + if _job_values(build, 'name') != [BUILD_NAME]: + findings.append(f'ci.yml build legs must keep their names: {BUILD_NAME}') return findings def ci_workflow_findings(ci: str, native: str, promote: str, cmake: str, - parity: str, docs: str) -> list[str]: - """Pin the PR-light/full-event split, parallel start, merge statuses, row home and runners.""" + parity: str, docs: str, others: dict[str, str] | None = None) -> list[str]: + """Pin the PR-light/full-event split, parallel start, merge statuses, row home and runners. + + ``others`` holds every other workflow file by name, for the runner rule. + """ findings = runner_findings({'ci.yml': ci, 'native-live.yml': native, 'corpus-parity.yml': parity, 'docs.yml': docs, - 'promote-baseline.yml': promote}) + 'promote-baseline.yml': promote, **(others or {})}) events = ci.split('\non:\n', 1) events = events[1].split('\npermissions:', 1)[0] if len(events) == 2 else '' for trigger in ('push:\n branches: [main]', @@ -220,7 +322,7 @@ def ci_workflow_findings(ci: str, native: str, promote: str, cmake: str, or 'set_property(TEST ${_pf_slow_test} APPEND PROPERTY LABELS slow)' not in cmake or 'if(PINEFORGE_BUILD_SOURCE_LAYER AND NOT TEST ${_pf_slow_test})' not in cmake): findings.append('measured slow rows must keep one pinned CMake label list') - return findings + return list(dict.fromkeys(findings)) def docs_workflow_findings(workflow: str) -> list[str]: @@ -408,13 +510,18 @@ def main() -> int: parser.add_argument('--check-ci-workflow', action='store_true') args = parser.parse_args() if args.check_ci_workflow: + workflows = ROOT / '.github/workflows' + named = ('ci.yml', 'native-live.yml', 'promote-baseline.yml', 'corpus-parity.yml', + 'docs.yml') findings = ci_workflow_findings( - (ROOT / '.github/workflows/ci.yml').read_text(), - (ROOT / '.github/workflows/native-live.yml').read_text(), - (ROOT / '.github/workflows/promote-baseline.yml').read_text(), + (workflows / 'ci.yml').read_text(), + (workflows / 'native-live.yml').read_text(), + (workflows / 'promote-baseline.yml').read_text(), (ROOT / 'tests/CMakeLists.txt').read_text(), - (ROOT / '.github/workflows/corpus-parity.yml').read_text(), - (ROOT / '.github/workflows/docs.yml').read_text()) + (workflows / 'corpus-parity.yml').read_text(), + (workflows / 'docs.yml').read_text(), + {path.name: path.read_text() for path in sorted(workflows.iterdir()) + if path.suffix in ('.yml', '.yaml') and path.name not in named}) for finding in findings: print(finding) if not findings: diff --git a/scripts/test_ci_preflight.py b/scripts/test_ci_preflight.py index decff141..a88869ca 100644 --- a/scripts/test_ci_preflight.py +++ b/scripts/test_ci_preflight.py @@ -23,7 +23,7 @@ def in_job(workflow, job, before, after): """Replace the first `before` inside job `job` (anywhere when job is None).""" - start = 0 if job is None else workflow.index(f'\n {job}:\n') + start = 0 if job is None else workflow.index(f'\n {job}:') if job is not None and before not in _jobs(workflow)[job]: raise AssertionError(f'{before!r} is not in job {job}') at = workflow.index(before, start) @@ -34,6 +34,13 @@ class PreflightFailures(unittest.TestCase): def ci_sources(self): return [(ROOT / path).read_text() for path in CI_SOURCES] + def other_sources(self): + """Every other workflow file, by name: ci_workflow_findings' ``others``.""" + named = {Path(path).name for path in CI_SOURCES} + return {path.name: path.read_text() + for path in sorted((ROOT / '.github/workflows').iterdir()) + if path.suffix in ('.yml', '.yaml') and path.name not in named} + def test_ci_contract_pins_parallel_pr_light_and_full_events(self): original = self.ci_sources() self.assertEqual(ci_workflow_findings(*original), []) @@ -96,8 +103,8 @@ def test_ci_contract_pins_parallel_pr_light_and_full_events(self): self.assertNotEqual(ci_workflow_findings(*changed), []) def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): - original = self.ci_sources() - self.assertEqual(ci_workflow_findings(*original), []) + original, others = self.ci_sources(), self.other_sources() + self.assertEqual(ci_workflow_findings(*original, others=others), []) fork_head = 'github.event.pull_request.head.repo.full_name' mutations = ( # A heavy job moved back to the slow standard runner. @@ -109,6 +116,11 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): 'ci.yml build must pair each image'), (0, 'build', 'larger_runner: pf-linux-x64-16', 'larger_runner: ubuntu-24.04', 'ci.yml build must pair each image'), + # A later include entry overrides one leg's larger runner. + (0, 'build', ' larger_runner: macos-26-xlarge\n', + ' larger_runner: macos-26-xlarge\n - os: ubuntu-24.04\n' + ' build_type: Debug\n larger_runner: ubuntu-24.04\n', + 'ci.yml build must pair each image'), (1, 'native-live', LINUX_RUNNER, 'ubuntu-24.04', 'native-live.yml job native-live must run on'), (4, 'corpus-parity', LINUX_RUNNER, 'ubuntu-24.04', 'corpus-parity.yml job corpus-parity must run on'), (4, 'corpus-parity-subset', LINUX_RUNNER, 'ubuntu-24.04', @@ -122,6 +134,21 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): (0, 'build', MATRIX_RUNNER, '${{ matrix.larger_runner }}', 'ci.yml job build must run on'), (0, 'build', 'larger_runner: macos-26-xlarge', 'larger_runner: macos-26-large', 'ci.yml build must pair each image'), + # A leg whose os is a larger runner: the fork's fallback is the larger runner. + (0, 'build', ' larger_runner: macos-26-xlarge\n', + ' larger_runner: macos-26-xlarge\n - os: pf-linux-x64-16\n' + ' build_type: Release\n', 'ci.yml build must pair each image'), + # Block forms of runs-on, and job ids the parser must still see. + (5, 'build', ' runs-on: ubuntu-latest\n', ' runs-on:\n group: pineforge-ci-large\n', + 'docs.yml job build must stay on a standard runner'), + (2, 'promote', ' runs-on: ubuntu-latest\n', ' runs-on:\n - macos-26-xlarge\n', + 'promote-baseline.yml job promote must stay on a standard runner'), + (0, None, ' build-gate:\n', ' extra:\n runs-on:\n group: pineforge-ci-large\n' + ' steps:\n - run: "true"\n\n build-gate:\n', 'ci.yml job extra needs a pinned runner'), + (5, None, '\njobs:\n', '\njobs:\n pre_build:\n runs-on: macos-26-xlarge\n steps:\n' + ' - run: "true"\n\n', 'docs.yml job pre_build must stay on a standard runner'), + (5, None, '\njobs:\n', '\njobs:\n "pre-build":\n runs-on: macos-26-xlarge\n steps:\n' + ' - run: "true"\n\n', 'docs.yml has jobs lines the runner contract cannot read'), (1, 'native-live', "|| 'ubuntu-24.04'", "|| 'pf-linux-x64-16'", 'native-live.yml job native-live must run on'), (4, 'corpus-parity-subset', f'runs-on: {LINUX_RUNNER}', 'runs-on:\n group: pineforge-ci-large', @@ -159,17 +186,61 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): (4, 'corpus-parity-subset', ' BUILD_DIR: build-corpus-parity-subset\n', ' BUILD_DIR: build-corpus-parity-subset\n JOBS: "4"\n', 'corpus-parity.yml job corpus-parity-subset must size'), + # ... through a multi-line run block, an = spelling, or a later flag. + (0, 'kernel-only', f'run: python3 scripts/ci_verify.py kernel --build-dir build-kernel --jobs {CORES}', + 'run: |\n python3 scripts/ci_verify.py kernel --build-dir build-kernel', + 'ci.yml job kernel-only must size'), + (0, 'sanitizers', f'--jobs {CORES}', '--jobs=4', 'ci.yml job sanitizers must size'), + (0, 'build', f'--jobs {CORES} --ccache', f'--jobs {CORES} --ccache --jobs=4', + 'ci.yml job build must size'), + (4, 'corpus-parity-subset', f'run: JOBS={CORES} ./scripts/check_corpus_parity.sh --subset', + 'run: |\n JOBS=4 ./scripts/check_corpus_parity.sh --subset', + 'corpus-parity.yml job corpus-parity-subset must size'), + (4, 'corpus-parity', f'run: JOBS={CORES} ./scripts/check_corpus_parity.sh', + 'run: |\n export JOBS=4\n ./scripts/check_corpus_parity.sh', + 'corpus-parity.yml job corpus-parity must size'), # The build legs keep their names: build (, ). (0, 'build', ' name: build (${{ matrix.os }}, ${{ matrix.build_type }})\n', '', - 'ci.yml build must pair each image'), + 'ci.yml build legs must keep their names'), ) for index, job, before, after, finding in mutations: with self.subTest(file=CI_SOURCES[index], job=job, after=after): changed = original.copy() changed[index] = in_job(changed[index], job, before, after) - findings = ci_workflow_findings(*changed) + findings = ci_workflow_findings(*changed, others=others) self.assertTrue(any(finding in line for line in findings), findings) + def test_comments_and_commented_headers_are_not_findings(self): + changed, others = self.ci_sources(), self.other_sources() + changed[0] = in_job(changed[0], 'kernel-only', ' steps:\n', + ' # Was --jobs 4, and JOBS: "4", on the standard runner.\n' + ' steps:\n') + changed[0] = changed[0].replace('\n preflight:\n', '\n preflight: # the fast gate\n', 1) + self.assertEqual(ci_workflow_findings(*changed, others=others), []) + + def test_every_workflow_a_pull_request_can_start_stays_on_standard_runners(self): + original, others = self.ci_sources(), self.other_sources() + # release.yml starts only on a dispatch, so no pull request reaches its runners. + self.assertIn('release.yml', others) + self.assertEqual(ci_workflow_findings(*original, others=others), []) + opened = others['release.yml'].replace('on:\n workflow_dispatch:\n', + 'on:\n pull_request:\n workflow_dispatch:\n', 1) + self.assertNotEqual(opened, others['release.yml']) + self.assertIn('release.yml job prebuilt must stay on a standard runner', + ci_workflow_findings(*original, others=dict(others, **{'release.yml': opened}))) + new = 'name: lint\non: {on}\njobs:\n lint:\n runs-on: {runner}\n steps:\n - run: "true"\n' + for on, runner, refused in ( + ('[pull_request]', 'pf-linux-x64-16', True), + ('pull_request_target', 'macos-26-xlarge', True), + ('\n pull_request:\n', '\n group: pineforge-ci-large', True), + ('[pull_request]', 'ubuntu-latest', False), + ('\n workflow_dispatch:\n', 'pf-linux-x64-16', False)): + with self.subTest(on=on, runner=runner): + findings = ci_workflow_findings( + *original, others=dict(others, **{'lint.yml': new.format(on=on, runner=runner)})) + self.assertEqual('lint.yml job lint must stay on a standard runner' in findings, + refused, findings) + def test_a_stage_times_out_inside_the_preflight_job(self): # Five minutes stay for the runner setup and the other stages, so the # stage's own timeout, with its log, fires before the job's. @@ -187,9 +258,9 @@ def contract_stage(self, mutation): # a temporary file that can vanish mid-copy: never copy bytecode. shutil.copytree(ROOT / 'scripts', tree / 'scripts', ignore=shutil.ignore_patterns('__pycache__', '*.pyc')) - for path in CI_SOURCES: - (tree / path).parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(ROOT / path, tree / path) + shutil.copytree(ROOT / '.github/workflows', tree / '.github/workflows') + (tree / 'tests').mkdir() + shutil.copyfile(ROOT / 'tests/CMakeLists.txt', tree / 'tests/CMakeLists.txt') if mutation: ci = tree / '.github/workflows/ci.yml' ci.write_text(in_job(ci.read_text(), *mutation)) From eb594e88afd17b4811e9118236b154c1f30f7732 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 07:55:28 +0800 Subject: [PATCH 62/83] Size preflight from verifier-tests' measured standard-runner time preflight 30 -> 45 min, its per-stage bound 1500 -> 2400 s. The 570 s and 10.3 min the first sizing used were a cut-off: main's run at 0d76a099 was cancelled 593 s into verifier-tests. That stage is one serial process. At this tree it takes 458-475 s on the maintainers' verification hosts, which ran it 1.93-1.94 times as fast as the standard runner on the same trees (91d65ad6, 53d36551), so about 15 min on the standard runner a fork's pull request gets, and more cores do not shorten it on the larger one. 45 min is about 2.8x the resulting 16-min preflight, 2400 s about 2.6x the stage, and the stage bound stays 300 s inside the job. The measured bases are corrected where the review found them short: the full corpus sweep took 16.3-26.2 min over its ten runs, the full sanitizers runs 34.0-69.4 min on the standard runner (they now land only on the larger one), and a fork's sanitizers compile uses 1277-1335 s of the verifier's 1800-s build bound; docs/ci.md also loses its stale 4-vCPU sentence. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 1b5a30fc06b8a7f056a1c0f4e3ebcb14bbefc462) INT26 pick note: the docs/ci.md sentence this commit rewrites was this lane's own time-limit commit's (6419c7f). --- .github/workflows/ci.yml | 8 +++--- .github/workflows/corpus-parity.yml | 2 +- docs/ci.md | 39 ++++++++++++++++------------- scripts/ci_preflight.py | 14 ++++++----- scripts/test_ci_preflight.py | 9 ++++--- 5 files changed, 41 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57cf3abc..0c5db128 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,11 @@ env: jobs: preflight: runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} - # The verifier self-tests alone took 394-543 s on the standard runner and - # passed 570 s at 0d76a099, where ten minutes cancelled main's run. - timeout-minutes: 30 + # Its verifier-tests stage is one serial process: 458-475 s at 0d76a099's + # tree on the maintainers' verification hosts, which ran it 1.93-1.94 + # times as fast as the standard runner on the same trees, so about 15 min + # here. Ten minutes cancelled main's run at 0d76a099 593 s into it. + timeout-minutes: 45 steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/corpus-parity.yml b/.github/workflows/corpus-parity.yml index b3f13322..54c935e6 100644 --- a/.github/workflows/corpus-parity.yml +++ b/.github/workflows/corpus-parity.yml @@ -91,7 +91,7 @@ jobs: corpus-parity: if: inputs.mode != 'subset' runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} - # 16.3-24.4 min on the standard runner a fork's pull request still gets. + # 16.3-26.2 min on the standard runner a fork's pull request still gets. timeout-minutes: 120 steps: - uses: actions/checkout@v4 diff --git a/docs/ci.md b/docs/ci.md index 7eac0a58..eabd5efc 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -331,8 +331,8 @@ all 312 corpus strategies 90 s at `JOBS=8` — and then the run phase, which process per probe over a 221 861-bar feed, took **1792 s** (about 5.7 s per probe, machine load average 100–160 from sibling builds). Judging the result (byte-identity plus the verifier) takes a further 94 s. That is ~32 min here, -and the dominant term is single-core serial work that a 4-vCPU hosted runner -runs slower, not faster. +and the dominant term is single-core serial work, which a runner's extra cores +do not shorten. So the whole population cannot be held to the ~25 min pull-request CI target, and it runs nightly (01:30 UTC), on `workflow_dispatch`, @@ -697,8 +697,9 @@ Each `ci_verify.py` call passes `--jobs "$(getconf _NPROCESSORS_ONLN)"` and parallelism follow the runner: 16 on the larger Linux runner, 5 on the M2, 4 and 3 on the standard runners. The larger Linux runner runs at most eight jobs at once, and one CI run starts seven there (preflight, the two Ubuntu `build` -legs, sanitizers, kernel-only, native-live and the parity subset), so a second -run that overlaps it waits for a runner. +legs, sanitizers, kernel-only, native-live and the parity subset); a pull +request that touches the corpus pin or the parity tooling also starts the whole +sweep there, an eighth. A second run that overlaps them waits for a runner. `scripts/ci_preflight.py` (`ci-workflow-contract`) pins every job's runner, time limit and parallelism in the three workflows a CI run starts, the `build` @@ -714,27 +715,31 @@ mutations. More cores do not shorten every job. The `test_ci_verify` CTest row (`scripts/test_ci_verify.py`) is one Python process, and in the main run at 0d76a099 it was the whole CTest wall time of every full-set leg but -sanitizers: 976-1288 s on the standard runners, against 455-620 s at 8cf3be58 -the day before. Preflight's `verifier-tests` stage runs the same suite: -394-543 s on the standard runner, and past 570 s at 0d76a099, when the old -ten-minute limit cancelled main's preflight. The full corpus sweep's run phase -is serial as well. +sanitizers: 976-1288 s on the standard runners, against 341-621 s at 8cf3be58 +the day before. Preflight's `verifier-tests` stage runs the same suite on its +own: 394-542 s on the standard runner through 1a0e7ea1, and still running +593 s in at 0d76a099, when the old ten-minute limit cancelled main's +preflight. At that tree it takes 458-475 s on the maintainers' verification +hosts, which ran it 1.93-1.94 times as fast as the standard runner on the +same trees (91d65ad6, 53d36551): about 15 min there. The full corpus sweep's +run phase is serial as well. Each limit below is at least twice the slowest time measured for the job on a runner it can land on. The standard-runner times are the eight CI runs from main's 8cf3be58 (2026-09-24) to 0d76a099 (2026-09-25), pull requests included, -and the corpus-parity workflow's own runs of 2026-09-23 to 2026-09-25. -Until the larger runners' own runs accumulate, the maintainers' x86-64 -verification hosts at 8 to 12 jobs stand in for the Linux one, and the 3-core -M1 bounds the M2 from above. +and the corpus-parity workflow's ten runs of 2026-09-22 to 2026-09-25. Until +the larger runners' own runs accumulate, the maintainers' x86-64 verification +hosts at 8 to 12 jobs stand in for the Linux one on parallel work, the +standard runner's speed stands in for it on serial work, and the 3-core M1 +bounds the M2 from above. | Job | Limit (min) | Measured basis | | --- | --- | --- | -| `preflight` | 30 (was 10) | 7.3-9.8 min on the standard runner; cancelled at 10.3 min at 0d76a099, still in `verifier-tests`. 4.8-8.2 min on the verification hosts. Each stage is bounded at 25 min inside it. | +| `preflight` | 45 (was 10) | 7.3-9.8 min on the standard runner through 1a0e7ea1; cancelled at 10.3 min at 0d76a099, 593 s into `verifier-tests`, which takes about 15 min there at that tree (above), so preflight about 16 min. 4.8-8.2 min on the verification hosts. Each stage is bounded at 40 min (2400 s) inside it. | | `build` | 75 (was 45) | A Release leg runs its full population on every event, a fork's pull request included: 30.3 min Ubuntu and 32.0 min macOS on the standard runners at 0d76a099, where the full Debug legs took 43.5 and 37.0 min. Pull-request Debug sets 17.2-17.7 min. 11.1-18.4 min on the verification hosts. | -| `sanitizers` | 120 | The verifier bounds a full run's CTest stage at 60 min after the build, a pull-request set's at 30 min after a standard-runner build of about 30 min. Pull-request set 35.2 min and full runs 34.0-60.0 min on the standard runner; full runs 18.2-19.0 min on the verification hosts. | +| `sanitizers` | 120 | The verifier bounds a full run's CTest stage at 60 min after the build, a pull-request set's at 30 min. A fork's pull-request set took 35.2 min on the standard runner, whose compile alone took 1277-1335 s of the verifier's 1800-s build bound (53d36551, 0d76a099). Full runs now land only on the larger runner; on the standard runner they took 34.0-69.4 min (at 0d76a099 30 min of build and ABI providers, then 39 min of CTest), and 18.2-19.0 min on the verification hosts. | | `kernel-only` | 60 (was 45) | Every event runs the whole kernel set: 7.9-22.3 min on the standard runner, 6.5-9.8 min on the verification hosts. | -| `native-live` | 60 (was 45) | Only the larger runner runs the full set, which waits on `test_ci_verify` (1034 s as a row on the standard runner at 0d76a099); the verification hosts' release profile, the nearest they run, took 11.2-18.4 min. A fork's pull request runs the set without that row: 3.4 min on the standard runner. | -| `corpus-parity` | 120 | 16.3-24.4 min on the standard runner, 10.1-10.3 min on the verification hosts. | +| `native-live` | 60 (was 45) | Only the larger runner runs the full set, which is its build plus the serial `test_ci_verify` row (1034 s on the standard runner at 0d76a099): about 25 min. The verification hosts' release profile, the nearest they run, took 11.2-18.4 min. A fork's pull request runs the set without that row: 3.4 min on the standard runner. | +| `corpus-parity` | 120 | 16.3-26.2 min on the standard runner, 10.1-10.3 min on the verification hosts. | | `corpus-parity-subset` | 30 | 1.8-4.7 min on the standard runner, 2.4-3.8 min on the verification hosts. | | `build` (aggregate) | 5 | Seconds. | diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index 857906ec..ae15022f 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -61,7 +61,7 @@ # Every job with a runner in the workflows a CI run starts: its runs-on and its # timeout-minutes, exactly. docs/ci.md gives each limit's measured basis. JOB_RUNNERS = { - 'ci.yml': {'preflight': (LINUX_RUNNER, 30), 'build': (MATRIX_RUNNER, 75), + 'ci.yml': {'preflight': (LINUX_RUNNER, 45), 'build': (MATRIX_RUNNER, 75), 'sanitizers': (LINUX_RUNNER, 120), 'kernel-only': (LINUX_RUNNER, 60), 'build-gate': ('ubuntu-24.04', 5)}, 'native-live.yml': {'native-live': (LINUX_RUNNER, 60)}, @@ -73,11 +73,13 @@ STANDARD_RUNNERS = ('ubuntu-24.04', 'ubuntu-latest') TRUSTED_ONLY_EVENTS = {'push', 'schedule', 'workflow_dispatch'} # One stage's bound. The verifier self-tests (test_ci_verify.py) drive the real -# literal-aware parity, receipt and submodule guards and took 394-543 s on the -# standard hosted runner, past 570 s at 0d76a099. The bound stays inside the -# preflight job's time limit, so a stuck stage is logged here rather than cut -# off with the job. -STAGE_TIMEOUT_SECONDS = 1500 +# literal-aware parity, receipt and submodule guards in one serial process. At +# 0d76a099's tree they take 458-475 s on the maintainers' verification hosts, +# which ran them 1.93-1.94 times as fast as the standard hosted runner on the +# same trees (91d65ad6, 53d36551): about 920 s there. The bound stays inside +# the preflight job's time limit, so a stuck stage is logged here rather than +# cut off with the job. +STAGE_TIMEOUT_SECONDS = 2400 # A job's header -- any id GitHub accepts -- and the top-level jobs and on keys. _JOB_HEADER = re.compile(r'^ ([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(?:#.*)?$', re.MULTILINE) _JOBS_KEY = re.compile(r'^jobs:[ \t]*(?:#.*)?$', re.MULTILINE) diff --git a/scripts/test_ci_preflight.py b/scripts/test_ci_preflight.py index a88869ca..0b863fc4 100644 --- a/scripts/test_ci_preflight.py +++ b/scripts/test_ci_preflight.py @@ -162,8 +162,8 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): (0, None, ' build-gate:\n', ' extra:\n runs-on: ubuntu-24.04\n steps:\n' ' - run: "true"\n\n build-gate:\n', 'ci.yml job extra needs a pinned runner'), # A time limit changed away from its measured basis. - (0, 'preflight', 'timeout-minutes: 30', 'timeout-minutes: 10', - 'ci.yml job preflight must allow 30 minutes'), + (0, 'preflight', 'timeout-minutes: 45', 'timeout-minutes: 30', + 'ci.yml job preflight must allow 45 minutes'), (0, 'build', 'timeout-minutes: 75', 'timeout-minutes: 45', 'ci.yml job build must allow 75'), (0, 'sanitizers', 'timeout-minutes: 120', 'timeout-minutes: 60', 'ci.yml job sanitizers must allow 120'), @@ -246,8 +246,9 @@ def test_a_stage_times_out_inside_the_preflight_job(self): # stage's own timeout, with its log, fires before the job's. job_seconds = JOB_RUNNERS['ci.yml']['preflight'][1] * 60 self.assertLessEqual(STAGE_TIMEOUT_SECONDS + 300, job_seconds) - # And at least twice the slowest verifier-tests stage measured: 570 s. - self.assertGreaterEqual(STAGE_TIMEOUT_SECONDS, 2 * 570) + # And at least twice the stage's standard-runner time at 0d76a099's + # tree: 475 s on the verification hosts, 1.94 times as fast. + self.assertGreaterEqual(STAGE_TIMEOUT_SECONDS, 2 * 475 * 1.94) def contract_stage(self, mutation): """Run the ci-workflow-contract stage's argv in a copy of the tree.""" From 1797a35f8eed0923d418fc6d592cbc608dc9e264 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 08:33:18 +0800 Subject: [PATCH 63/83] Trust only named events for the larger runners, and read every runner spelling The runner test trusted every event but pull_request, so a trigger a fork can raise -- pull_request_target, a review, a comment, a workflow_run -- added to CI or to a workflow that calls native-live.yml or corpus-parity.yml would have run on the larger runner. It now names what it trusts: a push, the schedule, a dispatch, or a pull request event whose head is this repository. Every other event, merge_group included, gets the standard runner. A second review also reproduced spellings the contract read as absent: quoted or spaced keys ("runs-on":, runs-on :), an on: key or event it exempted by mistake ("pull_request":, a multi-line flow sequence), --job and --jo (argparse accepts both for --jobs), a ci_verify.py call run from scripts/, and a verify command moved into a variable. The contract now reads quoted and spaced keys, treats an unreadable on: block or runs-on as a finding, counts every --jobs abbreviation, requires each heavy job's own verification to carry the core count, and refuses a job that calls a workflow outside .github/workflows/. Harmless edits it flagged no longer are: a comment or blank line in the build strategy, a trailing comment, a backslash-continued command, the parity command inside a run block, and a quoted standard label. test_ci_preflight.py adds each reproduced spelling and each harmless edit, and checks a missing job is reported once. docs/ci.md states the test, the larger runners' billing precondition, the verifier's own bounds that bind first, and the estimates behind the full-set limits. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit bd8db19f4449141413aad133fd8beb6102ea615f) INT26 pick note: scripts/ci_preflight.py conflicted on the line beside INT26 item (iv)'s PR_SLOW_ROWS_SHA256; the digest stays item (iv)'s, every other line is this commit's. INT26 pick note: the docs/ci.md sentence this commit rewrites was this lane's own time-limit commit's (6419c7f). --- .github/workflows/ci.yml | 28 +++--- .github/workflows/corpus-parity.yml | 15 +-- .github/workflows/native-live.yml | 9 +- docs/ci.md | 58 +++++++---- scripts/ci_preflight.py | 149 +++++++++++++++++++--------- scripts/test_ci_preflight.py | 75 ++++++++++++-- 6 files changed, 229 insertions(+), 105 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c5db128..488b89cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,19 +23,20 @@ env: CCACHE_MAXSIZE: 500M # The heavy jobs run on the organization's 16-core larger runner, and the macOS -# legs on GitHub's M2 runner for the macos-26 image, for every event except a -# pull request from a fork: a larger runner bills the organization even for a -# public repository, so a fork's pull request runs on the free standard runner. +# legs on GitHub's M2 runner for the macos-26 image, for a push, the schedule, a +# dispatch and a pull request from a branch of this repository. A larger runner +# bills the organization even for a public repository, so every other event -- +# a fork's pull request above all -- runs on the free standard runner. # Each verification sizes its parallelism to the runner that took it. # scripts/ci_preflight.py pins every runner, time limit and job count, and # docs/ci.md gives each limit's measured basis. jobs: preflight: - runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} - # Its verifier-tests stage is one serial process: 458-475 s at 0d76a099's - # tree on the maintainers' verification hosts, which ran it 1.93-1.94 - # times as fast as the standard runner on the same trees, so about 15 min - # here. Ten minutes cancelled main's run at 0d76a099 593 s into it. + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + # Its verifier-tests stage is one serial process: 475 s at 0d76a099's tree + # on the maintainers' verification hosts, which ran it 1.93-1.94 times as + # fast as the standard runner on the same trees, so about 15 min here. Ten + # minutes cancelled main's run at 0d76a099 593 s into it. timeout-minutes: 45 steps: - uses: actions/checkout@v4 @@ -98,7 +99,7 @@ jobs: # matrix.os is the standard runner a fork's pull request gets; its # larger_runner runs the same image with more cores. - runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && matrix.larger_runner || matrix.os }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && matrix.larger_runner || matrix.os }} steps: - uses: actions/checkout@v4 @@ -166,10 +167,11 @@ jobs: # instrumentation propagates into every test binary (CMakeLists.txt option # PINEFORGE_ENABLE_SANITIZERS). sanitizers: - runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # A full run (push to main, manual dispatch) gives its CTest stage an hour - # after the build; the PR set keeps its 30-minute stage, after a build of - # about 30 minutes on the standard runner a fork's pull request gets. + # after the build; the PR set keeps its 30-minute stage, after about 30 + # minutes of build and ABI providers on the standard runner a fork's pull + # request gets. timeout-minutes: 120 env: ASAN_OPTIONS: detect_leaks=1:halt_on_error=1:abort_on_error=1 @@ -219,7 +221,7 @@ jobs: # modules are built from the kernel alone, and every examples/native host # runs as an example_* row asserting both its exit code and its summary line. kernel-only: - runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # 22.3 min on the standard runner at 0d76a099, where a fork's pull request # still runs the whole kernel set. timeout-minutes: 60 diff --git a/.github/workflows/corpus-parity.yml b/.github/workflows/corpus-parity.yml index 54c935e6..ee7fbc7d 100644 --- a/.github/workflows/corpus-parity.yml +++ b/.github/workflows/corpus-parity.yml @@ -80,17 +80,18 @@ env: # pin is almost entirely cache hits. CCACHE_MAXSIZE: 2G -# Both jobs run on the organization's 16-core larger runner for every event -# except a pull request from a fork (this workflow's own or CI's: in a -# workflow_call the github context is the caller's), which runs on the free -# standard runner. The build uses every core of the runner that took the job. -# scripts/ci_preflight.py pins the runners and limits. +# Both jobs run on the organization's 16-core larger runner for a push, the +# schedule, a dispatch and a pull request from a branch of this repository; +# every other event, a fork's pull request above all (this workflow's own or +# CI's: in a workflow_call the github context is the caller's), runs on the +# free standard runner. The build uses every core of the runner that took the +# job. scripts/ci_preflight.py pins the runners and limits. jobs: # The whole population. Nightly, on demand, and on a pull request that # touches the pin or the parity tooling. Never reached by `mode: subset`. corpus-parity: if: inputs.mode != 'subset' - runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # 16.3-26.2 min on the standard runner a fork's pull request still gets. timeout-minutes: 120 steps: @@ -165,7 +166,7 @@ jobs: # names, judged against the same pinned sha256 rows. Reached through CI. corpus-parity-subset: if: inputs.mode == 'subset' - runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # 1.8-4.7 min on the standard runner. timeout-minutes: 30 steps: diff --git a/.github/workflows/native-live.yml b/.github/workflows/native-live.yml index 0010966b..7ad8dbfc 100644 --- a/.github/workflows/native-live.yml +++ b/.github/workflows/native-live.yml @@ -27,13 +27,14 @@ env: CURL_VERSION: "8.14.1" CURL_SHA256: "f4619a1e2474c4bbfedc88a7c2191209c8334b48fa1f4e53fd584cc12e9120dd" -# The organization's 16-core larger runner for every event except a pull -# request from a fork, which runs on the free standard runner. In a -# workflow_call the github context is the caller's, so a fork's pull request +# The organization's 16-core larger runner for a push, the schedule, a +# dispatch and a pull request from a branch of this repository; every other +# event, a fork's pull request above all, runs on the free standard runner. In +# a workflow_call the github context is the caller's, so a fork's pull request # through CI lands there too. scripts/ci_preflight.py pins the runner and limit. jobs: native-live: - runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # The full set waits on the test_ci_verify CTest row, 1034 s on the # standard runner at 0d76a099, which more cores do not shorten. timeout-minutes: 60 diff --git a/docs/ci.md b/docs/ci.md index eabd5efc..dbdaa23f 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -678,11 +678,15 @@ from a branch of this repository: the organization's `pf-linux-x64-16` (Ubuntu arm64 image on an M2, 5 cores, 14 GB). A larger runner bills the organization even for a public repository, so a pull request from a fork runs the same jobs on the free standard runners, `ubuntu-24.04` (4 vCPU, 16 GB) and `macos-26` -(M1, 3 cores, 7 GB). Every heavy job selects its runner with one test: any -event but a pull request, or a pull request whose head is in this repository. +(M1, 3 cores, 7 GB). Every heavy job selects its runner with one test that +names what it trusts: a push, the schedule, a manual dispatch, or a pull +request event whose head is in this repository. Every other event gets the +standard runner, whether a fork can raise it (`pull_request`, +`pull_request_target`, a review, a comment, a `workflow_run`) or it is merely +unlisted (`merge_group`). ```yaml -runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} +runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} ``` The `build` matrix pairs each image with its `larger_runner` and names its legs @@ -695,11 +699,14 @@ shell and stays on the standard runner for every event. Each `ci_verify.py` call passes `--jobs "$(getconf _NPROCESSORS_ONLN)"` and `check_corpus_parity.sh` gets the same count as `JOBS`, so build and CTest parallelism follow the runner: 16 on the larger Linux runner, 5 on the M2, 4 -and 3 on the standard runners. The larger Linux runner runs at most eight jobs -at once, and one CI run starts seven there (preflight, the two Ubuntu `build` -legs, sanitizers, kernel-only, native-live and the parity subset); a pull -request that touches the corpus pin or the parity tooling also starts the whole -sweep there, an eighth. A second run that overlaps them waits for a runner. +and 3 on the standard runners. The larger Linux runner is configured for at +most eight jobs at once, and one CI run starts seven there (preflight, the two +Ubuntu `build` legs, sanitizers, kernel-only, native-live and the parity +subset); a pull request that touches the corpus pin or the parity tooling also +starts the whole sweep there, an eighth. A second run that overlaps them waits +for a runner. Larger runners bill the organization's Actions budget: with no +payment method or a spending limit of zero, their jobs wait for a runner that +never comes, since GitHub does not fall back to a standard one. `scripts/ci_preflight.py` (`ci-workflow-contract`) pins every job's runner, time limit and parallelism in the three workflows a CI run starts, the `build` @@ -708,9 +715,10 @@ start -- `docs.yml`, `promote-baseline.yml` and any new one -- on standard runners; one that only a push, the schedule or a dispatch starts (`release.yml`) is exempt. A heavy job moved back to the standard runner, a test or matrix entry that lets a fork's pull request onto a larger runner, a -changed time limit, or any other job count fails preflight, and so does a -jobs line the check cannot read; `scripts/test_ci_preflight.py` holds the -mutations. +call to a workflow outside `.github/workflows/`, a changed time limit, or any +`--jobs` or `JOBS` value but the core count fails preflight, and so does a +jobs line or `runs-on` the check cannot read; `scripts/test_ci_preflight.py` +holds the mutations. More cores do not shorten every job. The `test_ci_verify` CTest row (`scripts/test_ci_verify.py`) is one Python process, and in the main run at @@ -719,13 +727,21 @@ sanitizers: 976-1288 s on the standard runners, against 341-621 s at 8cf3be58 the day before. Preflight's `verifier-tests` stage runs the same suite on its own: 394-542 s on the standard runner through 1a0e7ea1, and still running 593 s in at 0d76a099, when the old ten-minute limit cancelled main's -preflight. At that tree it takes 458-475 s on the maintainers' verification -hosts, which ran it 1.93-1.94 times as fast as the standard runner on the -same trees (91d65ad6, 53d36551): about 15 min there. The full corpus sweep's -run phase is serial as well. - -Each limit below is at least twice the slowest time measured for the job on a -runner it can land on. The standard-runner times are the eight CI runs from +preflight. At that tree it takes 475 s on the maintainers' verification hosts +(458 s at this change's first commit, the same suite), which ran it 1.93-1.94 +times as fast as the standard runner on the same trees (91d65ad6, 53d36551): +about 15 min there. The full corpus sweep's run phase is serial as well. + +The verifier's own bounds bind before these limits. CTest gets 30 minutes in +every run but a full sanitizers one, and `test_ci_verify` alone took up to +1288 s of them at 0d76a099 (Ubuntu Debug); a fork's sanitizers compile took +1277-1335 s of the build stage's 30 minutes on the standard runner. The row's +growth (341-621 s a day earlier) is the next limit the whole CI meets, on any +runner. + +Each limit below is at least twice the job's slowest time on a runner it can +land on: measured where it has run there, and estimated for the full sets the +larger runners take over. The standard-runner times are the eight CI runs from main's 8cf3be58 (2026-09-24) to 0d76a099 (2026-09-25), pull requests included, and the corpus-parity workflow's ten runs of 2026-09-22 to 2026-09-25. Until the larger runners' own runs accumulate, the maintainers' x86-64 verification @@ -736,10 +752,10 @@ bounds the M2 from above. | Job | Limit (min) | Measured basis | | --- | --- | --- | | `preflight` | 45 (was 10) | 7.3-9.8 min on the standard runner through 1a0e7ea1; cancelled at 10.3 min at 0d76a099, 593 s into `verifier-tests`, which takes about 15 min there at that tree (above), so preflight about 16 min. 4.8-8.2 min on the verification hosts. Each stage is bounded at 40 min (2400 s) inside it. | -| `build` | 75 (was 45) | A Release leg runs its full population on every event, a fork's pull request included: 30.3 min Ubuntu and 32.0 min macOS on the standard runners at 0d76a099, where the full Debug legs took 43.5 and 37.0 min. Pull-request Debug sets 17.2-17.7 min. 11.1-18.4 min on the verification hosts. | -| `sanitizers` | 120 | The verifier bounds a full run's CTest stage at 60 min after the build, a pull-request set's at 30 min. A fork's pull-request set took 35.2 min on the standard runner, whose compile alone took 1277-1335 s of the verifier's 1800-s build bound (53d36551, 0d76a099). Full runs now land only on the larger runner; on the standard runner they took 34.0-69.4 min (at 0d76a099 30 min of build and ABI providers, then 39 min of CTest), and 18.2-19.0 min on the verification hosts. | +| `build` | 75 (was 45) | A Release leg runs its full population on every event, a fork's pull request included: 30.3 min Ubuntu and 32.0 min macOS on the standard runners at 0d76a099, where the full Debug legs took 43.5 and 37.0 min. Pull-request Debug sets 17.2-17.7 min. On the larger Linux runner a full leg is its build plus the serial `test_ci_verify` row, 976-1288 s: about 30 min. The M1's 37.0 min bounds the M2. 11.1-18.4 min on the verification hosts. | +| `sanitizers` | 120 | The verifier bounds a full run's CTest stage at 60 min after the build, a pull-request set's at 30 min. A fork's pull-request set took 35.2 min on the standard runner, whose compile alone took 1277-1335 s of the verifier's 1800-s build bound (53d36551, 0d76a099). Full runs now land only on the larger runner; on the standard runner they took 34.0-69.4 min (at 0d76a099 30 min of build and ABI providers, then 39 min of CTest, 1768 s of it the serial `test_ci_verify` row), so about 40 min there with a faster build, and 18.2-19.0 min on the verification hosts. | | `kernel-only` | 60 (was 45) | Every event runs the whole kernel set: 7.9-22.3 min on the standard runner, 6.5-9.8 min on the verification hosts. | -| `native-live` | 60 (was 45) | Only the larger runner runs the full set, which is its build plus the serial `test_ci_verify` row (1034 s on the standard runner at 0d76a099): about 25 min. The verification hosts' release profile, the nearest they run, took 11.2-18.4 min. A fork's pull request runs the set without that row: 3.4 min on the standard runner. | +| `native-live` | 60 (was 45) | Only the larger runner runs the full set, which is its build plus the serial `test_ci_verify` row (1034 s on the standard runner at 0d76a099): about 25 min, against 14.1-31.4 min for the full set on the standard runner. The verification hosts' release profile, the nearest they run, took 11.2-18.4 min. A fork's pull request runs the set without that row: 3.4 min on the standard runner. | | `corpus-parity` | 120 | 16.3-26.2 min on the standard runner, 10.1-10.3 min on the verification hosts. | | `corpus-parity-subset` | 30 | 1.8-4.7 min on the standard runner, 2.4-3.8 min on the verification hosts. | | `build` (aggregate) | 5 | Seconds. | diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index ae15022f..e5af729c 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -34,17 +34,21 @@ # The names live only in tests/CMakeLists.txt. A digest pins that measured # population without maintaining a second row list in this Python guard. PR_SLOW_ROWS_SHA256 = 'e8bddea797abc46ca0deb671d5d35db3491b67a82e1413d08278530d12f7b67b' -# A larger runner bills the organization even for a public repository, so a -# pull request from a fork runs on the free standard runner. Every other event -# -- a push, a dispatch, the schedule, a same-repository pull request -- runs -# the heavy jobs on the organization's 16-core Linux runner or on GitHub's M2 -# runner for the macos-26 image. -TRUSTED_EVENT = ("github.event_name != 'pull_request' || " +# A larger runner bills the organization even for a public repository, so only +# an event no fork can raise -- a push, the schedule, a maintainer's dispatch -- +# or a pull request event whose head is this repository runs the heavy jobs on +# the organization's 16-core Linux runner or GitHub's M2 runner for the +# macos-26 image. Any other event, a fork's pull_request or pull_request_target, +# a comment, a review or a workflow_run among them, gets the standard runner. +# In a workflow_call the github context is the caller's. +TRUSTED_EVENT = ("github.event_name == 'push' || github.event_name == 'schedule' || " + "github.event_name == 'workflow_dispatch' || " "github.event.pull_request.head.repo.full_name == github.repository") LINUX_RUNNER = '${{ (' + TRUSTED_EVENT + ") && 'pf-linux-x64-16' || 'ubuntu-24.04' }}" MATRIX_RUNNER = '${{ (' + TRUSTED_EVENT + ') && matrix.larger_runner || matrix.os }}' -# The build job's whole strategy, exactly: an include entry more (a leg whose -# os is a larger runner, or one that overrides a larger_runner) is a finding. +# The build job's whole strategy, exactly (comments and blank lines aside): an +# include entry more -- a leg whose os is a larger runner, or one that +# overrides a larger_runner -- is a finding. BUILD_STRATEGY = (' strategy:\n' ' fail-fast: false\n' ' matrix:\n' @@ -58,32 +62,40 @@ BUILD_NAME = 'build (${{ matrix.os }}, ${{ matrix.build_type }})' # Build and CTest parallelism is the core count of whichever runner took the job. CORES = '"$(getconf _NPROCESSORS_ONLN)"' -# Every job with a runner in the workflows a CI run starts: its runs-on and its -# timeout-minutes, exactly. docs/ci.md gives each limit's measured basis. +VERIFY, PARITY = 'ci_verify.py', 'check_corpus_parity.sh' +PARITY_COMMAND = f'JOBS={CORES} ./scripts/{PARITY}' +# Every job with a runner in the workflows a CI run starts: its runs-on, its +# timeout-minutes, exactly, and the command that must carry the core count. +# docs/ci.md gives each limit's measured basis. JOB_RUNNERS = { - 'ci.yml': {'preflight': (LINUX_RUNNER, 45), 'build': (MATRIX_RUNNER, 75), - 'sanitizers': (LINUX_RUNNER, 120), 'kernel-only': (LINUX_RUNNER, 60), - 'build-gate': ('ubuntu-24.04', 5)}, - 'native-live.yml': {'native-live': (LINUX_RUNNER, 60)}, - 'corpus-parity.yml': {'corpus-parity': (LINUX_RUNNER, 120), - 'corpus-parity-subset': (LINUX_RUNNER, 30)}, + 'ci.yml': {'preflight': (LINUX_RUNNER, 45, None), + 'build': (MATRIX_RUNNER, 75, VERIFY), + 'sanitizers': (LINUX_RUNNER, 120, VERIFY), + 'kernel-only': (LINUX_RUNNER, 60, VERIFY), + 'build-gate': ('ubuntu-24.04', 5, None)}, + 'native-live.yml': {'native-live': (LINUX_RUNNER, 60, VERIFY)}, + 'corpus-parity.yml': {'corpus-parity': (LINUX_RUNNER, 120, PARITY), + 'corpus-parity-subset': (LINUX_RUNNER, 30, PARITY)}, } # Every other workflow a pull request (a fork's included) can start keeps its # jobs on these. One that only a push, the schedule or a dispatch starts is free. STANDARD_RUNNERS = ('ubuntu-24.04', 'ubuntu-latest') TRUSTED_ONLY_EVENTS = {'push', 'schedule', 'workflow_dispatch'} # One stage's bound. The verifier self-tests (test_ci_verify.py) drive the real -# literal-aware parity, receipt and submodule guards in one serial process. At -# 0d76a099's tree they take 458-475 s on the maintainers' verification hosts, -# which ran them 1.93-1.94 times as fast as the standard hosted runner on the -# same trees (91d65ad6, 53d36551): about 920 s there. The bound stays inside -# the preflight job's time limit, so a stuck stage is logged here rather than -# cut off with the job. +# literal-aware parity, receipt and submodule guards in one serial process. On +# the maintainers' verification hosts they took 475 s at 0d76a099's tree (458 s +# at this change's first commit, the same suite), and those hosts ran them +# 1.93-1.94 times as fast as the standard hosted runner on the same trees +# (91d65ad6, 53d36551): about 920 s there. The stage starts within a minute of +# the preflight job, so its bound fits inside the job's limit and a stuck +# verifier-tests is logged here rather than cut off with the job. STAGE_TIMEOUT_SECONDS = 2400 # A job's header -- any id GitHub accepts -- and the top-level jobs and on keys. _JOB_HEADER = re.compile(r'^ ([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(?:#.*)?$', re.MULTILINE) _JOBS_KEY = re.compile(r'^jobs:[ \t]*(?:#.*)?$', re.MULTILINE) -_ON_KEY = re.compile(r'^(?:on|"on"|\'on\'):[ \t]*([^#\n]*)', re.MULTILINE) +_ON_KEY = re.compile(r'^(?:on|"on"|\'on\')[ \t]*:[ \t]*([^#\n]*)', re.MULTILINE) +# --jobs and every abbreviation argparse accepts for it in ci_verify.py. +_JOBS_FLAG = re.compile(r'(? str | None: @@ -126,6 +138,12 @@ def _unparsed(workflow: str) -> list[str]: return stray +def _key_value(line: str, key: str) -> str | None: + """The value after `key:` on this line, the key bare or quoted; else None.""" + match = re.match(r'(["\']?)' + re.escape(key) + r'\1[ \t]*:(?:[ \t]+(.*)|$)', line.strip()) + return None if match is None else match.group(2) or '' + + def _job_values(body: str, key: str) -> list[str]: """Every value of a job-level key, a block value's lines folded onto one.""" lines = body.split('\n') @@ -135,32 +153,49 @@ def _job_values(body: str, key: str) -> list[str]: depth = _indent(code[0]) values = [] for index, line in enumerate(lines): - if _indent(line) != depth or not line.lstrip().startswith(key + ':'): + first = _key_value(line, key) if _indent(line) == depth else None + if first is None: continue - parts = [line.strip()[len(key) + 1:]] + parts = [first] for follow in lines[index + 1:]: if follow.strip() and _indent(follow) <= depth: break parts.append(follow) - values.append(' '.join(re.sub(r'\s+#.*$', '', part).strip() for part in parts - if part.strip() and not part.lstrip().startswith('#'))) + value = ' '.join(re.sub(r'\s+#.*$', '', part).strip() for part in parts + if part.strip() and not part.lstrip().startswith('#')) + quoted = re.fullmatch(r'"([^"]*)"|\'([^\']*)\'', value) + values.append(next(group for group in quoted.groups() if group is not None) + if quoted else value) return values -def _job_block(body: str, key: str) -> str: - """A job-level key's line and every line under it, as written.""" +def _job_block(body: str, key: str) -> list[str]: + """A job-level key's code lines, its own and every one under it.""" lines = body.split('\n') code = _code(body) depth = _indent(code[0]) if code else 0 for index, line in enumerate(lines): - if line == ' ' * depth + key + ':': + if _indent(line) == depth and _key_value(line, key) == '': block = [line] for follow in lines[index + 1:]: if follow.strip() and _indent(follow) <= depth: break block.append(follow) - return '\n'.join(block).rstrip() + '\n' - return '' + return _code('\n'.join(block)) + return [] + + +def _commands(body: str) -> list[str]: + """A job's code lines, trailing comments dropped and continuations joined.""" + lines, pending = [], '' + for line in _code(body): + line = re.sub(r'\s+#.*$', '', line) + if line.endswith('\\'): + pending += line[:-1] + ' ' + continue + lines.append(pending + line) + pending = '' + return lines + ([pending] if pending else []) def _events(workflow: str) -> set[str] | None: @@ -168,12 +203,19 @@ def _events(workflow: str) -> set[str] | None: match = _ON_KEY.search(workflow) if not match: return None - if match.group(1).strip(): - return set(re.findall(r'[A-Za-z_]+', match.group(1))) + inline = match.group(1).strip() + if inline: + if inline[0] in '[{' and inline[-1] != {'[': ']', '{': '}'}[inline[0]]: + return None + return set(re.findall(r'[A-Za-z_]+', inline)) block = workflow[match.end():] after = re.search(r'^[^\s#]', block, re.MULTILINE) block = block[:after.start()] if after else block - return set(re.findall(r'^ ([A-Za-z_]+):', block, re.MULTILINE)) or None + keys = [line for line in _code(block) if _indent(line) <= 2] + events = [re.fullmatch(r' ([A-Za-z_]+):(?:[ \t].*)?', line) for line in keys] + if not keys or not all(events): + return None + return {event.group(1) for event in events} def runner_findings(workflows: dict[str, str]) -> list[str]: @@ -189,32 +231,41 @@ def runner_findings(workflows: dict[str, str]) -> list[str]: jobs = _jobs(workflow) findings += [f'{name} is missing job {job}' for job in pinned if job not in jobs] for job, body in jobs.items(): - runs_on = _job_values(body, 'runs-on') + runs_on, uses = _job_values(body, 'runs-on'), _job_values(body, 'uses') + if any(not value.startswith('./.github/workflows/') for value in uses): + findings.append(f'{name} job {job} calls a workflow the runner contract ' + 'cannot see; call one under ./.github/workflows/') if job not in pinned: if pinned and runs_on: findings.append(f'{name} job {job} needs a pinned runner and time limit') + elif not uses and len(runs_on) != 1: + findings.append(f'{name} job {job} has a runs-on the runner contract ' + 'cannot read') elif any(value not in STANDARD_RUNNERS for value in runs_on): - findings.append(f'{name} job {job} must stay on a standard runner') + findings.append(f'{name} job {job} must stay on a standard runner ' + f'({" or ".join(STANDARD_RUNNERS)})') continue - runner, minutes = pinned[job] + runner, minutes, command = pinned[job] if runs_on != [runner]: findings.append(f'{name} job {job} must run on {runner}') if _job_values(body, 'timeout-minutes') != [str(minutes)]: findings.append(f'{name} job {job} must allow {minutes} minutes') # A job on 16 cores that still asks for 4 wastes the runner it pays for. # Every line counts, a multi-line run block's included; comments do not. - for line in _code(body): - command = re.sub(r'^\s*(?:-\s+)?run:\s*', '', line) - if (line.count('--jobs') != line.count(f'--jobs {CORES}') - or ('scripts/ci_verify.py' in line and f'--jobs {CORES}' not in line) - or ((re.search(r'\bJOBS\s*[:=]', line) - or 'check_corpus_parity.sh' in line) - and not command.startswith( - f'JOBS={CORES} ./scripts/check_corpus_parity.sh'))): - findings.append(f'{name} job {job} must size its parallelism to the runner') - break + lines = _commands(body) + started = [re.sub(r'^(?:-\s+)?run:\s*', '', line.strip()).strip() for line in lines] + sized = [line for line in lines if VERIFY in line and f'--jobs {CORES}' in line] + if (any(len(_JOBS_FLAG.findall(line)) != line.count(f'--jobs {CORES}') + or (VERIFY in line and not _JOBS_FLAG.search(line)) for line in lines) + or any((re.search(r'\bJOBS\s*[:=]', line) or PARITY in line) + and not start.startswith(PARITY_COMMAND) + for line, start in zip(lines, started)) + or (command == VERIFY and not sized) + or (command == PARITY + and not any(start.startswith(PARITY_COMMAND) for start in started))): + findings.append(f'{name} job {job} must size its parallelism to the runner') build = _jobs(workflows.get('ci.yml', '')).get('build', '') - if _job_block(build, 'strategy') != BUILD_STRATEGY: + if _job_block(build, 'strategy') != _code(BUILD_STRATEGY): findings.append('ci.yml build must pair each image with its larger runner, exactly') if _job_values(build, 'name') != [BUILD_NAME]: findings.append(f'ci.yml build legs must keep their names: {BUILD_NAME}') diff --git a/scripts/test_ci_preflight.py b/scripts/test_ci_preflight.py index 0b863fc4..aae99169 100644 --- a/scripts/test_ci_preflight.py +++ b/scripts/test_ci_preflight.py @@ -131,6 +131,18 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): 'ci.yml job sanitizers must run on'), (0, 'kernel-only', f'{fork_head} == github.repository', f"{fork_head} != ''", 'ci.yml job kernel-only must run on'), + # Only named events no fork can raise are trusted; everything else a + # fork can start -- pull_request_target, a comment, a workflow_run -- + # falls to the standard runner. + (0, 'preflight', "github.event_name == 'push' || github.event_name == 'schedule' || " + "github.event_name == 'workflow_dispatch'", "github.event_name != 'pull_request'", + 'ci.yml job preflight must run on'), + (1, 'native-live', "github.event_name == 'workflow_dispatch'", + "github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target'", + 'native-live.yml job native-live must run on'), + (0, 'native-live', 'uses: ./.github/workflows/native-live.yml', + 'uses: someone/else/.github/workflows/native-live.yml@main', + 'ci.yml job native-live calls a workflow the runner contract cannot see'), (0, 'build', MATRIX_RUNNER, '${{ matrix.larger_runner }}', 'ci.yml job build must run on'), (0, 'build', 'larger_runner: macos-26-xlarge', 'larger_runner: macos-26-large', 'ci.yml build must pair each image'), @@ -145,6 +157,16 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): 'promote-baseline.yml job promote must stay on a standard runner'), (0, None, ' build-gate:\n', ' extra:\n runs-on:\n group: pineforge-ci-large\n' ' steps:\n - run: "true"\n\n build-gate:\n', 'ci.yml job extra needs a pinned runner'), + (0, None, ' build-gate:\n', ' extra:\n "runs-on": pf-linux-x64-16\n' + ' steps:\n - run: "true"\n\n build-gate:\n', 'ci.yml job extra needs a pinned runner'), + (5, 'build', ' runs-on: ubuntu-latest\n', ' "runs-on": pf-linux-x64-16\n', + 'docs.yml job build must stay on a standard runner'), + (5, 'build', ' runs-on: ubuntu-latest\n', ' runs-on : macos-26-xlarge\n', + 'docs.yml job build must stay on a standard runner'), + (2, 'promote', ' runs-on: ubuntu-latest\n', " 'runs-on': macos-26-xlarge\n", + 'promote-baseline.yml job promote must stay on a standard runner'), + (5, 'build', ' runs-on: ubuntu-latest\n', '', + 'docs.yml job build has a runs-on the runner contract cannot read'), (5, None, '\njobs:\n', '\njobs:\n pre_build:\n runs-on: macos-26-xlarge\n steps:\n' ' - run: "true"\n\n', 'docs.yml job pre_build must stay on a standard runner'), (5, None, '\njobs:\n', '\njobs:\n "pre-build":\n runs-on: macos-26-xlarge\n steps:\n' @@ -193,6 +215,15 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): (0, 'sanitizers', f'--jobs {CORES}', '--jobs=4', 'ci.yml job sanitizers must size'), (0, 'build', f'--jobs {CORES} --ccache', f'--jobs {CORES} --ccache --jobs=4', 'ci.yml job build must size'), + (0, 'kernel-only', f'--jobs {CORES} --ccache', f'--jobs {CORES} --ccache --job 4', + 'ci.yml job kernel-only must size'), + (0, 'sanitizers', f'--jobs {CORES} --ccache', f'--jobs {CORES} --ccache --jo=4', + 'ci.yml job sanitizers must size'), + (0, 'kernel-only', f'run: python3 scripts/ci_verify.py kernel --build-dir build-kernel --jobs {CORES} --ccache', + 'working-directory: scripts\n run: python3 ci_verify.py kernel --build-dir ../build-kernel --ccache', + 'ci.yml job kernel-only must size'), + (0, 'kernel-only', f'run: python3 scripts/ci_verify.py kernel --build-dir build-kernel --jobs {CORES} --ccache', + 'run: $VERIFY', 'ci.yml job kernel-only must size'), (4, 'corpus-parity-subset', f'run: JOBS={CORES} ./scripts/check_corpus_parity.sh --subset', 'run: |\n JOBS=4 ./scripts/check_corpus_parity.sh --subset', 'corpus-parity.yml job corpus-parity-subset must size'), @@ -210,36 +241,58 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): findings = ci_workflow_findings(*changed, others=others) self.assertTrue(any(finding in line for line in findings), findings) - def test_comments_and_commented_headers_are_not_findings(self): + def test_harmless_spellings_are_not_findings(self): changed, others = self.ci_sources(), self.other_sources() - changed[0] = in_job(changed[0], 'kernel-only', ' steps:\n', - ' # Was --jobs 4, and JOBS: "4", on the standard runner.\n' - ' steps:\n') + verify = f'run: python3 scripts/ci_verify.py kernel --build-dir build-kernel --jobs {CORES} --ccache' + for index, job, before, after in ( + (0, 'kernel-only', ' steps:\n', + ' # Was --jobs 4, and JOBS: "4", on the standard runner.\n steps:\n'), + (0, 'build', ' include:\n', ' include:\n\n # one per image\n'), + (0, 'build', "&& '--exclude-label slow' || '' }}\n", + "&& '--exclude-label slow' || '' }} # was --jobs 4\n"), + (0, 'kernel-only', verify, + 'run: |\n python3 scripts/ci_verify.py kernel --build-dir build-kernel \\\n' + f' --jobs {CORES} --ccache'), + (4, 'corpus-parity-subset', f'run: JOBS={CORES} ./scripts/check_corpus_parity.sh --subset', + f'run: |\n JOBS={CORES} ./scripts/check_corpus_parity.sh --subset'), + (5, 'build', ' runs-on: ubuntu-latest\n', ' runs-on: "ubuntu-latest"\n'), + (2, 'promote', ' runs-on: ubuntu-latest\n', ' runs-on:\n ubuntu-latest\n')): + changed[index] = in_job(changed[index], job, before, after) changed[0] = changed[0].replace('\n preflight:\n', '\n preflight: # the fast gate\n', 1) self.assertEqual(ci_workflow_findings(*changed, others=others), []) + def test_a_finding_is_reported_once(self): + changed = self.ci_sources() + changed[0] = changed[0].replace('\n kernel-only:\n', '\n kernel-only-renamed:\n', 1) + findings = ci_workflow_findings(*changed, others=self.other_sources()) + self.assertEqual(findings.count('ci.yml is missing job kernel-only'), 1, findings) + def test_every_workflow_a_pull_request_can_start_stays_on_standard_runners(self): original, others = self.ci_sources(), self.other_sources() # release.yml starts only on a dispatch, so no pull request reaches its runners. self.assertIn('release.yml', others) self.assertEqual(ci_workflow_findings(*original, others=others), []) - opened = others['release.yml'].replace('on:\n workflow_dispatch:\n', - 'on:\n pull_request:\n workflow_dispatch:\n', 1) - self.assertNotEqual(opened, others['release.yml']) - self.assertIn('release.yml job prebuilt must stay on a standard runner', - ci_workflow_findings(*original, others=dict(others, **{'release.yml': opened}))) + for spelling in ('on:\n pull_request:\n workflow_dispatch:\n', + 'on:\n "pull_request":\n workflow_dispatch:\n', + 'on:\n pull_request :\n workflow_dispatch:\n'): + with self.subTest(spelling=spelling): + opened = others['release.yml'].replace('on:\n workflow_dispatch:\n', spelling, 1) + findings = ci_workflow_findings(*original, others=dict(others, **{'release.yml': opened})) + self.assertTrue(any('release.yml job prebuilt must stay on a standard runner' in line + for line in findings), findings) new = 'name: lint\non: {on}\njobs:\n lint:\n runs-on: {runner}\n steps:\n - run: "true"\n' for on, runner, refused in ( ('[pull_request]', 'pf-linux-x64-16', True), ('pull_request_target', 'macos-26-xlarge', True), ('\n pull_request:\n', '\n group: pineforge-ci-large', True), + ('[push,\n pull_request]', 'pf-linux-x64-16', True), ('[pull_request]', 'ubuntu-latest', False), ('\n workflow_dispatch:\n', 'pf-linux-x64-16', False)): with self.subTest(on=on, runner=runner): findings = ci_workflow_findings( *original, others=dict(others, **{'lint.yml': new.format(on=on, runner=runner)})) - self.assertEqual('lint.yml job lint must stay on a standard runner' in findings, - refused, findings) + self.assertEqual(any('lint.yml job lint must stay on a standard runner' in line + for line in findings), refused, findings) def test_a_stage_times_out_inside_the_preflight_job(self): # Five minutes stay for the runner setup and the other stages, so the From 219e5e1d2308011af9ef1cba384fc951fc81d0f3 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 08:55:29 +0800 Subject: [PATCH 64/83] Trust a pull request's head only for a pull_request event A third review found the head test trusted any event whose payload names a pull request: a review or review comment on a pull request from a branch of this repository -- which anyone can post on a public repository -- would have run on the larger runner in a workflow that listened for it. The head test now counts only for a pull_request event; every review, comment and pull_request_target run gets the standard runner. It also reproduced readers that still failed open or flagged harmless text: an on: block scalar (on: >-) or a decoy on: line inside a multi-line string exempted a workflow, and a ci_verify module run (python3 -m ci_verify) or a command text in a step name satisfied the core-count rule. The on: key is now read once and only as a plain event list; the core-count rules read the shell lines a job's run steps execute (a folded block as one command), count the --jobs= spelling, and leave cache keys and step names alone; trailing comments in the build strategy no longer count. The docs name the trees behind the 1.93-1.95 ratio (91d65ad6, 1a0e7ea1), the pull-request run behind the sanitizers compile time, and the case a fork editing the workflows itself: preflight reports it, and the repository's approval rule for outside contributors' workflow runs is what bounds it. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit a95aa4a2af9c8a2e94e0db2dbcb562c603800723) --- .github/workflows/ci.yml | 10 +-- .github/workflows/corpus-parity.yml | 4 +- .github/workflows/native-live.yml | 2 +- docs/ci.md | 28 +++++--- scripts/ci_preflight.py | 106 +++++++++++++++++++--------- scripts/test_ci_preflight.py | 35 ++++++++- 6 files changed, 132 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 488b89cf..c2f3e63c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,9 @@ env: # docs/ci.md gives each limit's measured basis. jobs: preflight: - runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # Its verifier-tests stage is one serial process: 475 s at 0d76a099's tree - # on the maintainers' verification hosts, which ran it 1.93-1.94 times as + # on the maintainers' verification hosts, which ran it 1.93-1.95 times as # fast as the standard runner on the same trees, so about 15 min here. Ten # minutes cancelled main's run at 0d76a099 593 s into it. timeout-minutes: 45 @@ -99,7 +99,7 @@ jobs: # matrix.os is the standard runner a fork's pull request gets; its # larger_runner runs the same image with more cores. - runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && matrix.larger_runner || matrix.os }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && matrix.larger_runner || matrix.os }} steps: - uses: actions/checkout@v4 @@ -167,7 +167,7 @@ jobs: # instrumentation propagates into every test binary (CMakeLists.txt option # PINEFORGE_ENABLE_SANITIZERS). sanitizers: - runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # A full run (push to main, manual dispatch) gives its CTest stage an hour # after the build; the PR set keeps its 30-minute stage, after about 30 # minutes of build and ABI providers on the standard runner a fork's pull @@ -221,7 +221,7 @@ jobs: # modules are built from the kernel alone, and every examples/native host # runs as an example_* row asserting both its exit code and its summary line. kernel-only: - runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # 22.3 min on the standard runner at 0d76a099, where a fork's pull request # still runs the whole kernel set. timeout-minutes: 60 diff --git a/.github/workflows/corpus-parity.yml b/.github/workflows/corpus-parity.yml index ee7fbc7d..37dbd414 100644 --- a/.github/workflows/corpus-parity.yml +++ b/.github/workflows/corpus-parity.yml @@ -91,7 +91,7 @@ jobs: # touches the pin or the parity tooling. Never reached by `mode: subset`. corpus-parity: if: inputs.mode != 'subset' - runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # 16.3-26.2 min on the standard runner a fork's pull request still gets. timeout-minutes: 120 steps: @@ -166,7 +166,7 @@ jobs: # names, judged against the same pinned sha256 rows. Reached through CI. corpus-parity-subset: if: inputs.mode == 'subset' - runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # 1.8-4.7 min on the standard runner. timeout-minutes: 30 steps: diff --git a/.github/workflows/native-live.yml b/.github/workflows/native-live.yml index 7ad8dbfc..1b3d32c0 100644 --- a/.github/workflows/native-live.yml +++ b/.github/workflows/native-live.yml @@ -34,7 +34,7 @@ env: # through CI lands there too. scripts/ci_preflight.py pins the runner and limit. jobs: native-live: - runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} + runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} # The full set waits on the test_ci_verify CTest row, 1034 s on the # standard runner at 0d76a099, which more cores do not shorten. timeout-minutes: 60 diff --git a/docs/ci.md b/docs/ci.md index dbdaa23f..3fd6a829 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -679,14 +679,15 @@ arm64 image on an M2, 5 cores, 14 GB). A larger runner bills the organization even for a public repository, so a pull request from a fork runs the same jobs on the free standard runners, `ubuntu-24.04` (4 vCPU, 16 GB) and `macos-26` (M1, 3 cores, 7 GB). Every heavy job selects its runner with one test that -names what it trusts: a push, the schedule, a manual dispatch, or a pull -request event whose head is in this repository. Every other event gets the -standard runner, whether a fork can raise it (`pull_request`, -`pull_request_target`, a review, a comment, a `workflow_run`) or it is merely -unlisted (`merge_group`). +names what it trusts: a push, the schedule, a manual dispatch, or a +`pull_request` whose head is a branch of this repository. Every other event +gets the standard runner, whether a fork can raise it (a fork's pull request, +`pull_request_target`, a review or review comment -- anyone can post one on a +public repository's pull request -- a comment, a `workflow_run`) or it is +merely unlisted (`merge_group`). ```yaml -runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} +runs-on: ${{ (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) && 'pf-linux-x64-16' || 'ubuntu-24.04' }} ``` The `build` matrix pairs each image with its `larger_runner` and names its legs @@ -706,7 +707,11 @@ subset); a pull request that touches the corpus pin or the parity tooling also starts the whole sweep there, an eighth. A second run that overlaps them waits for a runner. Larger runners bill the organization's Actions budget: with no payment method or a spending limit of zero, their jobs wait for a runner that -never comes, since GitHub does not fall back to a standard one. +never comes, since GitHub does not fall back to a standard one. The test keeps +a fork's pull request that leaves the workflows alone off the larger runners; +one that edits them runs its own copy, which preflight reports but does not +stop, so the repository's approval rule for workflow runs from outside +contributors is what bounds that case. `scripts/ci_preflight.py` (`ci-workflow-contract`) pins every job's runner, time limit and parallelism in the three workflows a CI run starts, the `build` @@ -728,9 +733,10 @@ the day before. Preflight's `verifier-tests` stage runs the same suite on its own: 394-542 s on the standard runner through 1a0e7ea1, and still running 593 s in at 0d76a099, when the old ten-minute limit cancelled main's preflight. At that tree it takes 475 s on the maintainers' verification hosts -(458 s at this change's first commit, the same suite), which ran it 1.93-1.94 -times as fast as the standard runner on the same trees (91d65ad6, 53d36551): -about 15 min there. The full corpus sweep's run phase is serial as well. +(458 s in this change's own run, which leaves the suite alone), which ran it +1.93-1.95 times as fast as the standard runner on the same trees (91d65ad6, +1a0e7ea1): about 15 min there. The full corpus sweep's run phase is serial as +well. The verifier's own bounds bind before these limits. CTest gets 30 minutes in every run but a full sanitizers one, and `test_ci_verify` alone took up to @@ -753,7 +759,7 @@ bounds the M2 from above. | --- | --- | --- | | `preflight` | 45 (was 10) | 7.3-9.8 min on the standard runner through 1a0e7ea1; cancelled at 10.3 min at 0d76a099, 593 s into `verifier-tests`, which takes about 15 min there at that tree (above), so preflight about 16 min. 4.8-8.2 min on the verification hosts. Each stage is bounded at 40 min (2400 s) inside it. | | `build` | 75 (was 45) | A Release leg runs its full population on every event, a fork's pull request included: 30.3 min Ubuntu and 32.0 min macOS on the standard runners at 0d76a099, where the full Debug legs took 43.5 and 37.0 min. Pull-request Debug sets 17.2-17.7 min. On the larger Linux runner a full leg is its build plus the serial `test_ci_verify` row, 976-1288 s: about 30 min. The M1's 37.0 min bounds the M2. 11.1-18.4 min on the verification hosts. | -| `sanitizers` | 120 | The verifier bounds a full run's CTest stage at 60 min after the build, a pull-request set's at 30 min. A fork's pull-request set took 35.2 min on the standard runner, whose compile alone took 1277-1335 s of the verifier's 1800-s build bound (53d36551, 0d76a099). Full runs now land only on the larger runner; on the standard runner they took 34.0-69.4 min (at 0d76a099 30 min of build and ABI providers, then 39 min of CTest, 1768 s of it the serial `test_ci_verify` row), so about 40 min there with a faster build, and 18.2-19.0 min on the verification hosts. | +| `sanitizers` | 120 | The verifier bounds a full run's CTest stage at 60 min after the build, a pull-request set's at 30 min. A fork's pull-request set took 35.2 min on the standard runner, whose compile alone took 1277-1335 s of the verifier's 1800-s build bound (pull-request run 36139176292, and main at 0d76a099). Full runs now land only on the larger runner; on the standard runner they took 34.0-69.4 min (at 0d76a099 30 min of build and ABI providers, then 39 min of CTest, 1768 s of it the serial `test_ci_verify` row), so about 40 min there with a faster build, and 18.2-19.0 min on the verification hosts. | | `kernel-only` | 60 (was 45) | Every event runs the whole kernel set: 7.9-22.3 min on the standard runner, 6.5-9.8 min on the verification hosts. | | `native-live` | 60 (was 45) | Only the larger runner runs the full set, which is its build plus the serial `test_ci_verify` row (1034 s on the standard runner at 0d76a099): about 25 min, against 14.1-31.4 min for the full set on the standard runner. The verification hosts' release profile, the nearest they run, took 11.2-18.4 min. A fork's pull request runs the set without that row: 3.4 min on the standard runner. | | `corpus-parity` | 120 | 16.3-26.2 min on the standard runner, 10.1-10.3 min on the verification hosts. | diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index e5af729c..1129303d 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -36,14 +36,16 @@ PR_SLOW_ROWS_SHA256 = 'e8bddea797abc46ca0deb671d5d35db3491b67a82e1413d08278530d12f7b67b' # A larger runner bills the organization even for a public repository, so only # an event no fork can raise -- a push, the schedule, a maintainer's dispatch -- -# or a pull request event whose head is this repository runs the heavy jobs on -# the organization's 16-core Linux runner or GitHub's M2 runner for the -# macos-26 image. Any other event, a fork's pull_request or pull_request_target, -# a comment, a review or a workflow_run among them, gets the standard runner. -# In a workflow_call the github context is the caller's. +# or a pull_request whose head is a branch of this repository runs the heavy +# jobs on the organization's 16-core Linux runner or GitHub's M2 runner for the +# macos-26 image. Any other event -- a fork's pull request, any +# pull_request_target, a review or review comment (anyone can post one on a +# public repository's pull request), a comment, a workflow_run -- gets the +# standard runner. In a workflow_call the github context is the caller's. TRUSTED_EVENT = ("github.event_name == 'push' || github.event_name == 'schedule' || " "github.event_name == 'workflow_dispatch' || " - "github.event.pull_request.head.repo.full_name == github.repository") + "(github.event_name == 'pull_request' && " + "github.event.pull_request.head.repo.full_name == github.repository)") LINUX_RUNNER = '${{ (' + TRUSTED_EVENT + ") && 'pf-linux-x64-16' || 'ubuntu-24.04' }}" MATRIX_RUNNER = '${{ (' + TRUSTED_EVENT + ') && matrix.larger_runner || matrix.os }}' # The build job's whole strategy, exactly (comments and blank lines aside): an @@ -84,9 +86,9 @@ # One stage's bound. The verifier self-tests (test_ci_verify.py) drive the real # literal-aware parity, receipt and submodule guards in one serial process. On # the maintainers' verification hosts they took 475 s at 0d76a099's tree (458 s -# at this change's first commit, the same suite), and those hosts ran them -# 1.93-1.94 times as fast as the standard hosted runner on the same trees -# (91d65ad6, 53d36551): about 920 s there. The stage starts within a minute of +# in this change's own run, which leaves the suite alone), and those hosts ran +# them 1.93-1.95 times as fast as the standard hosted runner on the same trees +# (91d65ad6, 1a0e7ea1): about 925 s there. The stage starts within a minute of # the preflight job, so its bound fits inside the job's limit and a stuck # verifier-tests is logged here rather than cut off with the job. STAGE_TIMEOUT_SECONDS = 2400 @@ -141,7 +143,10 @@ def _unparsed(workflow: str) -> list[str]: def _key_value(line: str, key: str) -> str | None: """The value after `key:` on this line, the key bare or quoted; else None.""" match = re.match(r'(["\']?)' + re.escape(key) + r'\1[ \t]*:(?:[ \t]+(.*)|$)', line.strip()) - return None if match is None else match.group(2) or '' + if match is None: + return None + value = match.group(2) or '' + return '' if value.startswith('#') else value def _job_values(body: str, key: str) -> list[str]: @@ -181,33 +186,69 @@ def _job_block(body: str, key: str) -> list[str]: if follow.strip() and _indent(follow) <= depth: break block.append(follow) - return _code('\n'.join(block)) + return [re.sub(r'\s+#.*$', '', line) for line in _code('\n'.join(block))] return [] def _commands(body: str) -> list[str]: - """A job's code lines, trailing comments dropped and continuations joined.""" - lines, pending = [], '' - for line in _code(body): - line = re.sub(r'\s+#.*$', '', line) - if line.endswith('\\'): - pending += line[:-1] + ' ' + """The shell lines a job's run steps execute, as the shell reads them. + + A run value on its key's line is one command; a literal block's lines are + commands, backslash continuations joined; a folded block is one command. + Comments are dropped. + """ + lines, commands, index = body.split('\n'), [], 0 + while index < len(lines): + match = re.match(r'^(\s*(?:-\s+)?)run:[ \t]*(.*)$', lines[index]) + index += 1 + if not match: + continue + value = re.sub(r'\s+#.*$', '', match.group(2)).strip() + if value[:1] not in ('|', '>'): + commands.append(value) + continue + column, block = len(match.group(1)), [] + while index < len(lines) and (not lines[index].strip() or _indent(lines[index]) > column): + block.append(lines[index]) + index += 1 + shell = [re.sub(r'\s+#.*$', '', line).strip() for line in _code('\n'.join(block))] + if value[0] == '>': + commands.append(' '.join(shell)) continue - lines.append(pending + line) pending = '' - return lines + ([pending] if pending else []) + for line in shell: + if line.endswith('\\'): + pending += line[:-1] + ' ' + else: + commands.append(pending + line) + pending = '' + commands += [pending] if pending else [] + return commands + + +def _runs_verify(command: str) -> bool: + """Whether a shell line runs ci_verify: as a script, a path, or a module.""" + return bool(re.search(r'\bpython[0-9.]*\s+(?:-\S+\s+)*(?:\S*\bci_verify\.py\b|-m\s+ci_verify\b)' + r'|^\S*\bci_verify\.py\b', command)) + + +def _sized(line: str) -> int: + """How many times a line asks ci_verify for the runner's core count.""" + return line.count(f'--jobs {CORES}') + line.count(f'--jobs={CORES}') def _events(workflow: str) -> set[str] | None: """The events that start a workflow, or None when the on key cannot be read.""" - match = _ON_KEY.search(workflow) - if not match: + matches = list(_ON_KEY.finditer(workflow)) + if len(matches) != 1: return None + match = matches[0] inline = match.group(1).strip() if inline: - if inline[0] in '[{' and inline[-1] != {'[': ']', '{': '}'}[inline[0]]: + if inline[0] in '>|&*!' or (inline[0] in '[{' + and inline[-1] != {'[': ']', '{': '}'}[inline[0]]): return None - return set(re.findall(r'[A-Za-z_]+', inline)) + return set(re.findall(r'[A-Za-z_]+', inline)) or None block = workflow[match.end():] after = re.search(r'^[^\s#]', block, re.MULTILINE) block = block[:after.start()] if after else block @@ -252,17 +293,18 @@ def runner_findings(workflows: dict[str, str]) -> list[str]: findings.append(f'{name} job {job} must allow {minutes} minutes') # A job on 16 cores that still asks for 4 wastes the runner it pays for. # Every line counts, a multi-line run block's included; comments do not. - lines = _commands(body) - started = [re.sub(r'^(?:-\s+)?run:\s*', '', line.strip()).strip() for line in lines] - sized = [line for line in lines if VERIFY in line and f'--jobs {CORES}' in line] - if (any(len(_JOBS_FLAG.findall(line)) != line.count(f'--jobs {CORES}') - or (VERIFY in line and not _JOBS_FLAG.search(line)) for line in lines) - or any((re.search(r'\bJOBS\s*[:=]', line) or PARITY in line) - and not start.startswith(PARITY_COMMAND) - for line, start in zip(lines, started)) + code = [re.sub(r'\s+#.*$', '', line) for line in _code(body)] + commands = _commands(body) + sized = [line for line in commands + if _runs_verify(line) and _sized(line) and len(_JOBS_FLAG.findall(line)) == 1] + if (any(len(_JOBS_FLAG.findall(line)) != _sized(line) for line in code + commands) + or any(_runs_verify(line) and not _sized(line) for line in commands) + or any(re.search(r'\bJOBS\s*:', line) for line in code) + or any((re.search(r'\bJOBS\s*=', line) or PARITY in line) + and not line.startswith(PARITY_COMMAND) for line in commands) or (command == VERIFY and not sized) or (command == PARITY - and not any(start.startswith(PARITY_COMMAND) for start in started))): + and not any(line.startswith(PARITY_COMMAND) for line in commands))): findings.append(f'{name} job {job} must size its parallelism to the runner') build = _jobs(workflows.get('ci.yml', '')).get('build', '') if _job_block(build, 'strategy') != _code(BUILD_STRATEGY): diff --git a/scripts/test_ci_preflight.py b/scripts/test_ci_preflight.py index aae99169..5f611a6e 100644 --- a/scripts/test_ci_preflight.py +++ b/scripts/test_ci_preflight.py @@ -140,6 +140,11 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): (1, 'native-live', "github.event_name == 'workflow_dispatch'", "github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target'", 'native-live.yml job native-live must run on'), + # The head test counts only for a pull_request: anyone can review one. + (0, 'sanitizers', "(github.event_name == 'pull_request' && " + "github.event.pull_request.head.repo.full_name == github.repository)", + 'github.event.pull_request.head.repo.full_name == github.repository', + 'ci.yml job sanitizers must run on'), (0, 'native-live', 'uses: ./.github/workflows/native-live.yml', 'uses: someone/else/.github/workflows/native-live.yml@main', 'ci.yml job native-live calls a workflow the runner contract cannot see'), @@ -224,6 +229,15 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): 'ci.yml job kernel-only must size'), (0, 'kernel-only', f'run: python3 scripts/ci_verify.py kernel --build-dir build-kernel --jobs {CORES} --ccache', 'run: $VERIFY', 'ci.yml job kernel-only must size'), + (0, 'kernel-only', f'run: python3 scripts/ci_verify.py kernel --build-dir build-kernel --jobs {CORES} --ccache\n', + f'run: python3 scripts/ci_verify.py kernel --build-dir build-kernel --jobs {CORES} --ccache\n\n' + ' - run: PYTHONPATH=scripts python3 -m ci_verify kernel --build-dir build-kernel --ccache\n', + 'ci.yml job kernel-only must size'), + (0, 'kernel-only', f' - name: Verify (kernel)\n run: python3 scripts/ci_verify.py kernel ' + f'--build-dir build-kernel --jobs {CORES} --ccache', + f' - name: python3 scripts/ci_verify.py kernel --jobs {CORES}\n' + ' run: python3 -m ci_verify kernel --build-dir build-kernel --ccache', + 'ci.yml job kernel-only must size'), (4, 'corpus-parity-subset', f'run: JOBS={CORES} ./scripts/check_corpus_parity.sh --subset', 'run: |\n JOBS=4 ./scripts/check_corpus_parity.sh --subset', 'corpus-parity.yml job corpus-parity-subset must size'), @@ -256,6 +270,17 @@ def test_harmless_spellings_are_not_findings(self): (4, 'corpus-parity-subset', f'run: JOBS={CORES} ./scripts/check_corpus_parity.sh --subset', f'run: |\n JOBS={CORES} ./scripts/check_corpus_parity.sh --subset'), (5, 'build', ' runs-on: ubuntu-latest\n', ' runs-on: "ubuntu-latest"\n'), + (0, 'build', ' os: [ubuntu-24.04, macos-26]\n', + ' os: [ubuntu-24.04, macos-26] # the images\n'), + (0, 'build', ' strategy:\n', ' strategy: # the legs\n'), + (0, 'build', '-${{ matrix.build_type }}-${{ github.sha }}', + "-${{ matrix.build_type }}-${{ hashFiles('scripts/ci_verify.py') }}-${{ github.sha }}"), + (0, 'kernel-only', ' - name: Verify (kernel)\n', + ' - name: Verify (kernel) with scripts/ci_verify.py\n'), + (0, 'sanitizers', f'run: python3 scripts/ci_verify.py sanitizers --build-dir build-asan --jobs {CORES}', + 'run: >-\n python3 scripts/ci_verify.py sanitizers --build-dir build-asan\n' + f' --jobs {CORES}'), + (1, 'native-live', f'--jobs {CORES}', f'--jobs={CORES}'), (2, 'promote', ' runs-on: ubuntu-latest\n', ' runs-on:\n ubuntu-latest\n')): changed[index] = in_job(changed[index], job, before, after) changed[0] = changed[0].replace('\n preflight:\n', '\n preflight: # the fast gate\n', 1) @@ -286,6 +311,7 @@ def test_every_workflow_a_pull_request_can_start_stays_on_standard_runners(self) ('pull_request_target', 'macos-26-xlarge', True), ('\n pull_request:\n', '\n group: pineforge-ci-large', True), ('[push,\n pull_request]', 'pf-linux-x64-16', True), + ('>-\n pull_request', 'macos-26-xlarge', True), ('[pull_request]', 'ubuntu-latest', False), ('\n workflow_dispatch:\n', 'pf-linux-x64-16', False)): with self.subTest(on=on, runner=runner): @@ -293,6 +319,11 @@ def test_every_workflow_a_pull_request_can_start_stays_on_standard_runners(self) *original, others=dict(others, **{'lint.yml': new.format(on=on, runner=runner)})) self.assertEqual(any('lint.yml job lint must stay on a standard runner' in line for line in findings), refused, findings) + # A decoy on key inside a multi-line string is no exemption. + decoy = ('name: "lint\non: push"\non:\n pull_request:\njobs:\n lint:\n' + ' runs-on: pf-linux-x64-16\n steps:\n - run: "true"\n') + self.assertTrue(any('lint.yml job lint must stay on a standard runner' in line for line in + ci_workflow_findings(*original, others=dict(others, **{'lint.yml': decoy})))) def test_a_stage_times_out_inside_the_preflight_job(self): # Five minutes stay for the runner setup and the other stages, so the @@ -300,8 +331,8 @@ def test_a_stage_times_out_inside_the_preflight_job(self): job_seconds = JOB_RUNNERS['ci.yml']['preflight'][1] * 60 self.assertLessEqual(STAGE_TIMEOUT_SECONDS + 300, job_seconds) # And at least twice the stage's standard-runner time at 0d76a099's - # tree: 475 s on the verification hosts, 1.94 times as fast. - self.assertGreaterEqual(STAGE_TIMEOUT_SECONDS, 2 * 475 * 1.94) + # tree: 475 s on the verification hosts, up to 1.95 times as fast. + self.assertGreaterEqual(STAGE_TIMEOUT_SECONDS, 2 * 475 * 1.95) def contract_stage(self, mutation): """Run the ci-workflow-contract stage's argv in a copy of the tree.""" From 45dd63b0d3c7dda420c8c5bb2b741352212db40a Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 09:35:26 +0800 Subject: [PATCH 65/83] Count core-count flags per job, and exempt only release.yml by name A fourth review found the core-count rule still read commands line by line or step by step, so a second, unsized ci_verify call escaped when it shared a line with the sized one (&& or ;), sat after a blank line in a folded block, or rode in an action input (with: command:); a parity command with a second JOBS=4 part escaped the same way. The rule is now one ordered count over the whole job's code (comments, step names and hashFiles() arguments aside): every ci_verify call, as a script or a module, carries exactly one core-count --jobs before the next call, no other --jobs spelling appears, and every JOBS and parity-script mention is part of the one parity command. Chaining, blocks and action inputs no longer matter, and the command parser it replaces is gone. The same review showed on: spellings a text reader cannot see (a decoy line inside a string, an escaped key) could still exempt a new workflow. The exemption is now by name: release.yml, while its on: block names only events no fork can raise (a dispatch alone today). Any other workflow file is checked whatever starts it. test_ci_preflight.py adds each escape (a ; chain, an interpreter flag and a $pythonLocation call each on their own), a sized call inside an action input, a step name mentioning --jobs 4 and a wrapped run value as no-finding cases (each applied alone and composed), and refuses a new dispatch-only workflow on a larger runner. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit f313049f596ba86945c4f91eadee8b02769190bc) --- docs/ci.md | 25 +++++----- scripts/ci_preflight.py | 94 ++++++++++++++---------------------- scripts/test_ci_preflight.py | 61 ++++++++++++++++++++--- 3 files changed, 103 insertions(+), 77 deletions(-) diff --git a/docs/ci.md b/docs/ci.md index 3fd6a829..78b7b41e 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -713,17 +713,20 @@ one that edits them runs its own copy, which preflight reports but does not stop, so the repository's approval rule for workflow runs from outside contributors is what bounds that case. -`scripts/ci_preflight.py` (`ci-workflow-contract`) pins every job's runner, -time limit and parallelism in the three workflows a CI run starts, the `build` -strategy block included, and keeps every other workflow a pull request can -start -- `docs.yml`, `promote-baseline.yml` and any new one -- on standard -runners; one that only a push, the schedule or a dispatch starts -(`release.yml`) is exempt. A heavy job moved back to the standard runner, a -test or matrix entry that lets a fork's pull request onto a larger runner, a -call to a workflow outside `.github/workflows/`, a changed time limit, or any -`--jobs` or `JOBS` value but the core count fails preflight, and so does a -jobs line or `runs-on` the check cannot read; `scripts/test_ci_preflight.py` -holds the mutations. +`scripts/ci_preflight.py` (`ci-workflow-contract`) pins every job's runner, time +limit and parallelism in the three workflows a CI run starts, the `build` +strategy block included, and keeps every other workflow -- `docs.yml`, +`promote-baseline.yml` and any new one -- on standard runners. Only +`release.yml` is exempt, by name and while its `on:` block names only events no +fork can raise (a push, the schedule, a dispatch; a dispatch alone today). A +heavy job moved back to the standard runner, a test or matrix entry that lets a +fork's pull request onto a larger runner, a call to a workflow outside +`.github/workflows/`, a changed time limit, a `ci_verify.py` call without +exactly one core-count `--jobs` (counted over the whole job, however its +commands are chained, blocked or carried, with the flag and its value on one +line), or any other `--jobs` or `JOBS` value fails preflight, and so does a jobs +line or `runs-on` the check cannot read; `scripts/test_ci_preflight.py` holds +the mutations. More cores do not shorten every job. The `test_ci_verify` CTest row (`scripts/test_ci_verify.py`) is one Python process, and in the main run at diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index 1129303d..a4bcce3a 100644 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -79,9 +79,12 @@ 'corpus-parity.yml': {'corpus-parity': (LINUX_RUNNER, 120, PARITY), 'corpus-parity-subset': (LINUX_RUNNER, 30, PARITY)}, } -# Every other workflow a pull request (a fork's included) can start keeps its -# jobs on these. One that only a push, the schedule or a dispatch starts is free. +# Every other workflow keeps its jobs on these. release.yml is exempt by name, +# while its on: block names only events no fork can raise (a push, the +# schedule, a dispatch; a dispatch alone today); any other workflow file is +# checked, whatever starts it. STANDARD_RUNNERS = ('ubuntu-24.04', 'ubuntu-latest') +TRUSTED_ONLY_WORKFLOWS = ('release.yml',) TRUSTED_ONLY_EVENTS = {'push', 'schedule', 'workflow_dispatch'} # One stage's bound. The verifier self-tests (test_ci_verify.py) drive the real # literal-aware parity, receipt and submodule guards in one serial process. On @@ -96,8 +99,10 @@ _JOB_HEADER = re.compile(r'^ ([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(?:#.*)?$', re.MULTILINE) _JOBS_KEY = re.compile(r'^jobs:[ \t]*(?:#.*)?$', re.MULTILINE) _ON_KEY = re.compile(r'^(?:on|"on"|\'on\')[ \t]*:[ \t]*([^#\n]*)', re.MULTILINE) -# --jobs and every abbreviation argparse accepts for it in ci_verify.py. +# --jobs and every abbreviation argparse accepts for it in ci_verify.py, and a +# call of ci_verify itself, as a script or a module. _JOBS_FLAG = re.compile(r'(? str | None: @@ -190,51 +195,33 @@ def _job_block(body: str, key: str) -> list[str]: return [] -def _commands(body: str) -> list[str]: - """The shell lines a job's run steps execute, as the shell reads them. +def _counted_text(body: str) -> str: + """A job's code for the core-count rule: comments, step names and the + hashFiles() arguments of cache keys -- names of files, not commands -- out.""" + lines = [re.sub(r'\s+#.*$', '', line) for line in _code(body) + if not re.match(r'\s*(?:-\s+)?name\s*:', line)] + return re.sub(r'hashFiles\([^)]*\)', '', '\n'.join(lines)) - A run value on its key's line is one command; a literal block's lines are - commands, backslash continuations joined; a folded block is one command. - Comments are dropped. - """ - lines, commands, index = body.split('\n'), [], 0 - while index < len(lines): - match = re.match(r'^(\s*(?:-\s+)?)run:[ \t]*(.*)$', lines[index]) - index += 1 - if not match: - continue - value = re.sub(r'\s+#.*$', '', match.group(2)).strip() - if value[:1] not in ('|', '>'): - commands.append(value) - continue - column, block = len(match.group(1)), [] - while index < len(lines) and (not lines[index].strip() or _indent(lines[index]) > column): - block.append(lines[index]) - index += 1 - shell = [re.sub(r'\s+#.*$', '', line).strip() for line in _code('\n'.join(block))] - if value[0] == '>': - commands.append(' '.join(shell)) - continue - pending = '' - for line in shell: - if line.endswith('\\'): - pending += line[:-1] + ' ' - else: - commands.append(pending + line) - pending = '' - commands += [pending] if pending else [] - return commands +def _sized(text: str, command: str | None) -> bool: + """Whether a job's work runs at the runner's core count. -def _runs_verify(command: str) -> bool: - """Whether a shell line runs ci_verify: as a script, a path, or a module.""" - return bool(re.search(r'\bpython[0-9.]*\s+(?:-\S+\s+)*(?:\S*\bci_verify\.py\b|-m\s+ci_verify\b)' - r'|^\S*\bci_verify\.py\b', command)) - - -def _sized(line: str) -> int: - """How many times a line asks ci_verify for the runner's core count.""" - return line.count(f'--jobs {CORES}') + line.count(f'--jobs={CORES}') + Counted over the whole job, in order, so no line, block or chaining form + matters: every ci_verify call carries one core-count --jobs before the next + call, no other --jobs spelling appears, and every JOBS and parity-script + mention is part of the one parity command. + """ + marks = sorted([(match.start(), 'C') for match in _VERIFY_CALL.finditer(text)] + + [(match.start(), 'F' if text.startswith( + (f'--jobs {CORES}', f'--jobs={CORES}'), match.start()) else 'U') + for match in _JOBS_FLAG.finditer(text)]) + calls = ''.join(kind for _, kind in marks) + parity = text.count(PARITY_COMMAND) + return (re.fullmatch(r'(?:CF)*', calls) is not None + and len(re.findall(r'\bJOBS\s*[:=]', text)) == parity + and text.count(PARITY) == parity + and (command != VERIFY or bool(calls)) + and (command != PARITY or parity > 0)) def _events(workflow: str) -> set[str] | None: @@ -265,7 +252,8 @@ def runner_findings(workflows: dict[str, str]) -> list[str]: for name, workflow in workflows.items(): pinned = JOB_RUNNERS.get(name, {}) events = _events(workflow) - if not pinned and events is not None and events <= TRUSTED_ONLY_EVENTS: + if (not pinned and name in TRUSTED_ONLY_WORKFLOWS and events is not None + and events <= TRUSTED_ONLY_EVENTS): continue if _unparsed(workflow): findings.append(f'{name} has jobs lines the runner contract cannot read') @@ -292,19 +280,7 @@ def runner_findings(workflows: dict[str, str]) -> list[str]: if _job_values(body, 'timeout-minutes') != [str(minutes)]: findings.append(f'{name} job {job} must allow {minutes} minutes') # A job on 16 cores that still asks for 4 wastes the runner it pays for. - # Every line counts, a multi-line run block's included; comments do not. - code = [re.sub(r'\s+#.*$', '', line) for line in _code(body)] - commands = _commands(body) - sized = [line for line in commands - if _runs_verify(line) and _sized(line) and len(_JOBS_FLAG.findall(line)) == 1] - if (any(len(_JOBS_FLAG.findall(line)) != _sized(line) for line in code + commands) - or any(_runs_verify(line) and not _sized(line) for line in commands) - or any(re.search(r'\bJOBS\s*:', line) for line in code) - or any((re.search(r'\bJOBS\s*=', line) or PARITY in line) - and not line.startswith(PARITY_COMMAND) for line in commands) - or (command == VERIFY and not sized) - or (command == PARITY - and not any(line.startswith(PARITY_COMMAND) for line in commands))): + if not _sized(_counted_text(body), command): findings.append(f'{name} job {job} must size its parallelism to the runner') build = _jobs(workflows.get('ci.yml', '')).get('build', '') if _job_block(build, 'strategy') != _code(BUILD_STRATEGY): diff --git a/scripts/test_ci_preflight.py b/scripts/test_ci_preflight.py index 5f611a6e..d58a01ce 100644 --- a/scripts/test_ci_preflight.py +++ b/scripts/test_ci_preflight.py @@ -15,6 +15,8 @@ ci_workflow_findings, run_checks) ROOT = Path(__file__).resolve().parents[1] +KERNEL_VERIFY = ('run: python3 scripts/ci_verify.py kernel --build-dir build-kernel ' + '--jobs "$(getconf _NPROCESSORS_ONLN)" --ccache') # ci_workflow_findings' arguments, in order. CI_SOURCES = ('.github/workflows/ci.yml', '.github/workflows/native-live.yml', '.github/workflows/promote-baseline.yml', 'tests/CMakeLists.txt', @@ -233,6 +235,34 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): f'run: python3 scripts/ci_verify.py kernel --build-dir build-kernel --jobs {CORES} --ccache\n\n' ' - run: PYTHONPATH=scripts python3 -m ci_verify kernel --build-dir build-kernel --ccache\n', 'ci.yml job kernel-only must size'), + # A second, unsized call however it is chained, blocked or carried. + (0, 'kernel-only', KERNEL_VERIFY, + f'run: >-\n python3 scripts/ci_verify.py kernel --build-dir build-kernel --jobs {CORES} --ccache' + '\n\n python3 scripts/ci_verify.py release --build-dir build-rel --ccache', + 'ci.yml job kernel-only must size'), + (0, 'kernel-only', KERNEL_VERIFY + '\n', + KERNEL_VERIFY + '\n\n - uses: nick-fields/retry@v3\n with:\n' + ' command: python3 scripts/ci_verify.py release --build-dir build-rel --ccache\n', + 'ci.yml job kernel-only must size'), + (0, 'kernel-only', KERNEL_VERIFY, + KERNEL_VERIFY + ' && python3 scripts/ci_verify.py release --build-dir build-rel --ccache', + 'ci.yml job kernel-only must size'), + (0, 'kernel-only', KERNEL_VERIFY, + KERNEL_VERIFY + '; python3 scripts/ci_verify.py release --build-dir build-rel --ccache', + 'ci.yml job kernel-only must size'), + (0, 'kernel-only', KERNEL_VERIFY + '\n', + KERNEL_VERIFY + '\n\n - run: python3 -X dev scripts/ci_verify.py release --build-dir build-rel --ccache\n', + 'ci.yml job kernel-only must size'), + (0, 'kernel-only', KERNEL_VERIFY + '\n', + KERNEL_VERIFY + '\n\n - run: $pythonLocation/bin/python3 scripts/ci_verify.py release ' + '--build-dir build-rel --ccache\n', + 'ci.yml job kernel-only must size'), + (0, 'kernel-only', KERNEL_VERIFY, KERNEL_VERIFY + " ${{ '--jobs 4' }}", + 'ci.yml job kernel-only must size'), + (4, 'corpus-parity-subset', f'run: JOBS={CORES} ./scripts/check_corpus_parity.sh --subset', + f'run: JOBS={CORES} ./scripts/check_corpus_parity.sh --subset' + ' && JOBS=4 ./scripts/check_corpus_parity.sh --subset', + 'corpus-parity.yml job corpus-parity-subset must size'), (0, 'kernel-only', f' - name: Verify (kernel)\n run: python3 scripts/ci_verify.py kernel ' f'--build-dir build-kernel --jobs {CORES} --ccache', f' - name: python3 scripts/ci_verify.py kernel --jobs {CORES}\n' @@ -256,9 +286,9 @@ def test_ci_contract_pins_runners_time_limits_and_the_fork_guard(self): self.assertTrue(any(finding in line for line in findings), findings) def test_harmless_spellings_are_not_findings(self): - changed, others = self.ci_sources(), self.other_sources() - verify = f'run: python3 scripts/ci_verify.py kernel --build-dir build-kernel --jobs {CORES} --ccache' - for index, job, before, after in ( + original, others = self.ci_sources(), self.other_sources() + verify = KERNEL_VERIFY + edits = ( (0, 'kernel-only', ' steps:\n', ' # Was --jobs 4, and JOBS: "4", on the standard runner.\n steps:\n'), (0, 'build', ' include:\n', ' include:\n\n # one per image\n'), @@ -281,9 +311,24 @@ def test_harmless_spellings_are_not_findings(self): 'run: >-\n python3 scripts/ci_verify.py sanitizers --build-dir build-asan\n' f' --jobs {CORES}'), (1, 'native-live', f'--jobs {CORES}', f'--jobs={CORES}'), - (2, 'promote', ' runs-on: ubuntu-latest\n', ' runs-on:\n ubuntu-latest\n')): - changed[index] = in_job(changed[index], job, before, after) - changed[0] = changed[0].replace('\n preflight:\n', '\n preflight: # the fast gate\n', 1) + (0, 'kernel-only', ' - name: Verify (kernel)\n', + ' - name: Verify (kernel), no longer --jobs 4\n'), + (0, 'kernel-only', KERNEL_VERIFY, + 'uses: nick-fields/retry@v3\n with:\n command: python3 scripts/ci_verify.py ' + f'kernel --build-dir build-kernel --jobs {CORES} --ccache'), + (2, 'promote', ' runs-on: ubuntu-latest\n', ' runs-on:\n ubuntu-latest\n'), + (0, None, '\n preflight:\n', '\n preflight: # the fast gate\n')) + # Each edit alone, then every one that composes (a line one edit + # rewrites is left to the edit that came first). + for index, job, before, after in edits: + with self.subTest(file=CI_SOURCES[index], job=job, after=after): + changed = original.copy() + changed[index] = in_job(changed[index], job, before, after) + self.assertEqual(ci_workflow_findings(*changed, others=others), []) + changed = original.copy() + for index, job, before, after in edits: + if before in (changed[index] if job is None else _jobs(changed[index]).get(job, '')): + changed[index] = in_job(changed[index], job, before, after) self.assertEqual(ci_workflow_findings(*changed, others=others), []) def test_a_finding_is_reported_once(self): @@ -313,7 +358,9 @@ def test_every_workflow_a_pull_request_can_start_stays_on_standard_runners(self) ('[push,\n pull_request]', 'pf-linux-x64-16', True), ('>-\n pull_request', 'macos-26-xlarge', True), ('[pull_request]', 'ubuntu-latest', False), - ('\n workflow_dispatch:\n', 'pf-linux-x64-16', False)): + # Only release.yml is exempt, by name: a new workflow is checked + # whatever starts it, so no on: spelling can exempt one. + ('\n workflow_dispatch:\n', 'pf-linux-x64-16', True)): with self.subTest(on=on, runner=runner): findings = ci_workflow_findings( *original, others=dict(others, **{'lint.yml': new.format(on=on, runner=runner)})) From 86dd0673020076120d149dc2072572a25994716d Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 06:43:16 +0800 Subject: [PATCH 66/83] Source: strategy.margin_liquidation_price asks the kernel's native_liquidation_price() (R5 lane H-THIN, X15 a) The Pine host kept a second closed-form solve of the liquidation level (compute_liquidation_price) beside the kernel's own NativeStrategyHost::native_liquidation_price(), which project() already configures for it: the adapter declares the maintenance-only margin model with NativeLiquidationLevelBase::RealizedOnly so that the kernel's level is solved from closed money alone, exactly as the Pine formula was. The host now reads the kernel's level and keeps only TradingView's spelling (design row MG8): the level rounded onto the tick grid, down for a long, up for a short. compute_liquidation_price is deleted. Measured (probe_liq_sweep, $EV/exec/H-THIN-scratch/probes: 1,200 seeded configurations -- long/short, margins 1..150 %, percent / cash-per-order / cash-per-contract fees, ticks 0 / 1e-5 / 0.01 / 0.25 / 0.5 / 1, account FX, one to three lots with a partial close -- 12,000 printed values at %.17g, the base's library against this one): every value of a run that has a margin model is bit-identical (10,506 lines). The 1,494 lines that differ all belong to runs whose margin calls the host switched off (set_margin_call_enabled(false), a PineForge switch TradingView has no counterpart for): the kernel then has no margin model and the value is na, where the deleted solve answered the level of a liquidation the run never performs. No corpus probe, public benchmark slot or witness reads it. Nothing hashed moves: the value is a query. tests/test_margin_call_l4a.cpp's switched-off row now expects na (its expectation corrected), and the twin's pinned rewrite evidence for test_margin_call (tests/twin_parity_inventory.json, whose sha256 scripts/check_twin_parity.py pins) records the change and its reason. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit abd7978cd1560a6638ff39197297bcae8ee7fcad) --- .../pineforge/source/pine_strategy_host.hpp | 5 ++-- scripts/check_twin_parity.py | 2 +- src/source/pine_adapter.cpp | 2 +- src/source/pine_strategy_host.cpp | 27 +++++++------------ tests/test_margin_call_l4a.cpp | 7 ++++- tests/twin_parity_inventory.json | 4 +-- 6 files changed, 21 insertions(+), 26 deletions(-) diff --git a/include/pineforge/source/pine_strategy_host.hpp b/include/pineforge/source/pine_strategy_host.hpp index 09c223b0..8627980b 100644 --- a/include/pineforge/source/pine_strategy_host.hpp +++ b/include/pineforge/source/pine_strategy_host.hpp @@ -589,8 +589,8 @@ class PineStrategyHost : public NativeStrategyHost, public BrokerStateHashProvid const Series& fallback) const { return source_input_series(key, fallback); } - // Generated strategy.margin_liquidation_price reads this Pine-specific - // projection over the inherited native position state. + // Generated strategy.margin_liquidation_price reads this: the kernel's + // native_liquidation_price() in TradingView's tick spelling. double margin_liquidation_price() const; void fixture_publish_source_series(const Bar& bar, bool new_history_slot) { scheduler_.fixture_publish_source_series(bar, new_history_slot); @@ -759,7 +759,6 @@ class PineStrategyHost : public NativeStrategyHost, public BrokerStateHashProvid void scheduler_publish_source_bar(const Bar&, bool first_tick, bool advance_source_index = true); void scheduler_publish_suppressed_tail(const Bar&); - double compute_liquidation_price() const; void project_short_seed_report_rows(const native_order::ExecutionAppliedEvent&); bool scheduler_coof_enabled() const noexcept { return config_.calc_on_order_fills; } // The range-end row an applied execution may complete. It stays ordered diff --git a/scripts/check_twin_parity.py b/scripts/check_twin_parity.py index 5449d864..f8b4be58 100644 --- a/scripts/check_twin_parity.py +++ b/scripts/check_twin_parity.py @@ -29,7 +29,7 @@ # executable code means a ledger/inventory rewrite cannot silently redefine # what the guard proves. BASE_MANIFEST_SHA256 = "594d23c87a6581dd8ea4a6ca57c8b3d2402034232d873355f839027564b6914f" -INVENTORY_SHA256 = "264aef4af2e0ed04e6666ee01d51dd55c2bc10e55314872a942eda5c6b9e77ad" +INVENTORY_SHA256 = "dbb0bb4a5bb7e3ea7b213b6a135962c6ed8baae825dc3980381ce1145a8daef6" LEDGER_SHA256 = "c568999da8777ff967cdb3fcf007f382f88902bf78f9e02c2c5d7d15de22e9ba" APPENDIX_HEADING = "## Appendix 5 — CHECK-parity unobservable literal ledger" diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index aa28e876..e87d5f9c 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -2419,7 +2419,7 @@ NativeRunSpec PineExecutionAdapter::project(const PineStrategyConfig& config, && config.commission_value > 0.0) ? NativeMarginEquityBasis::MarkedEquity : NativeMarginEquityBasis::MarkedEquityBeforeOpenCommission; - // compute_liquidation_price() solves from closed money alone. + // margin_liquidation_price() reads this level: closed money alone. margin.level_base = NativeLiquidationLevelBase::RealizedOnly; // TradingView's own liquidation ticket. The adapter's host classifies // a row as MARGIN_CALL from exactly this exit id (R5 lane L12, 2.ii l: diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index 2e2991fc..d0b05b32 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -185,24 +185,15 @@ std::uint64_t source::PineStrategyHost::broker_state_hash_projection() const { } double source::PineStrategyHost::margin_liquidation_price() const { - return compute_liquidation_price(); -} - -double source::PineStrategyHost::compute_liquidation_price() const { - if (position_side_ == PositionSide::FLAT) return na(); - const double point_value = syminfo_.pointvalue; - const double quantity = position_qty_; - if (!(quantity > 0.0) || !(point_value > 0.0)) return na(); - const double direction = position_side_ == PositionSide::LONG ? 1.0 : -1.0; - const double margin_pct = position_side_ == PositionSide::LONG - ? config_.margin_long : config_.margin_short; - const double denominator = (margin_pct / 100.0) - direction; - if (std::abs(denominator) < 1e-12) return na(); - const double equity_basis = - (initial_capital_ + net_profit_sum_) / active_account_currency_fx(); - double liquidation = - (equity_basis / (quantity * point_value) - direction * position_entry_price_) - / denominator; + // The level is the kernel's (native_liquidation_price, solved from closed + // money alone: project() declares NativeLiquidationLevelBase::RealizedOnly), + // and na where it has none -- a flat book, a run without a margin model. + // TradingView's spelling is the one thing kept here (design row MG8): the + // level rounded onto the tick grid away from the position, down for a + // long, up for a short. + const std::optional level = native_liquidation_price(); + if (!level) return na(); + double liquidation = *level; if (syminfo_mintick_ > 0.0) { liquidation = position_side_ == PositionSide::SHORT ? std::ceil(liquidation / syminfo_mintick_) * syminfo_mintick_ diff --git a/tests/test_margin_call_l4a.cpp b/tests/test_margin_call_l4a.cpp index 7bbcc0c6..6d7f71b3 100644 --- a/tests/test_margin_call_l4a.cpp +++ b/tests/test_margin_call_l4a.cpp @@ -168,7 +168,12 @@ void test_margin_switch_is_observable() { CHECK(margin_rows(host) == 0); CHECK(near(host.position(), -10.0)); CHECK(near(host.average(), 100.0)); - CHECK(std::isfinite(host.liquidation_price())); + // expectation corrected (R5 lane H-THIN, X15 a): the value is the kernel's + // native_liquidation_price() in TradingView's tick spelling, and a run whose + // margin calls are switched off declares no margin model, so it has no + // liquidation level: na, where the deleted Pine solve answered the level of + // a liquidation this run never performs (was std::isfinite). + CHECK(std::isnan(host.liquidation_price())); } void test_grid_floor_before_four_x() { diff --git a/tests/twin_parity_inventory.json b/tests/twin_parity_inventory.json index 7d7f1278..87e97b5d 100644 --- a/tests/twin_parity_inventory.json +++ b/tests/twin_parity_inventory.json @@ -279,9 +279,9 @@ }, "test_margin_call": { "baseAssertions": 414, - "reason": "owner-seeded margin scenarios are rewritten to public trade and liquidation rows", + "reason": "owner-seeded margin scenarios are rewritten to public trade and liquidation rows; the switched-off run's liquidation-price row follows lane H-THIN (X15 a), where the value is the kernel's native_liquidation_price() and a run that declares no margin model has none", "twin": "test_margin_call_l4a.cpp", - "twinAssertionSha256": "e63d76789c3496f779b727a7a971674926dbe0ef4e4287e9e428c730a0f1e2df", + "twinAssertionSha256": "5fd3bc55d5be90041fc35f45a26d9e5d2f38e45563576655d57a3d1a080dfcbe", "twinAssertions": 54 }, "test_order_birth_provenance": { From 0afaa480e100a70a99dd391298ba517bce950799 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 06:53:27 +0800 Subject: [PATCH 67/83] Kernel: one civil-date helper -- native_calendar::native_civil_date / native_civil_days (R5 lane H-THIN, X15 e) Howard Hinnant's civil_from_days / days_from_civil were restated six times: the calendar's pair (src/native_calendar.cpp), the time-of-day fields and their inverse (src/session_time.cpp civil_fields / days_from_civil), the timeframe helpers' inverse (src/timeframe.cpp), the report's two UTC month keys (src/engine_metrics.cpp month_key_utc / keep_utc_month) and the Pine adapter's chart-day key (src/source/pine_adapter.cpp civil_chart_day_key, audit AUDIT4 section 6 row G2-38). The arithmetic now lives once, as two constexpr functions of the installed calendar header, pineforge::native_calendar::native_civil_date(days) and native_civil_days(year, month, day), and every one of those sites reads them: the calendar's two keep their own types as one-line forwards, the month keys read the civil month and the first day of the next, the time-of-day fields take the day of year as days minus the year's January 1, and the adapter's chart-day key reads day and month. ADR-0001 rule 2 ruling: (a) mechanism -- proleptic-Gregorian civil arithmetic, the same for every host and venue, with no platform word; (b) knob -- none, a pure function of its arguments. Byte-identical by construction: integer arithmetic over the same ranges (|days| below 2^40 / 86400 on every caller), each result equal to the one it replaces; test_chart_day_key_arithmetic and test_utc_month_key_arithmetic sweep the adapter's key and the month key against gmtime_r and pass, as do the calendar, session, timeframe and time-of-day rows (31 of 31 on macOS). The kernel residual gate reads 0 findings. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 5e5a3ddb25d3b650afff04086b6eb8cd8a54391c) --- include/pineforge/native_calendar.hpp | 36 +++++++++++++++++++++++++++ src/engine_metrics.cpp | 36 ++++++++------------------- src/native_calendar.cpp | 24 +++++------------- src/session_time.cpp | 28 +++++++-------------- src/source/pine_adapter.cpp | 19 ++++++-------- src/timeframe.cpp | 10 +++----- 6 files changed, 71 insertions(+), 82 deletions(-) diff --git a/include/pineforge/native_calendar.hpp b/include/pineforge/native_calendar.hpp index 8cca5470..672e8e6b 100644 --- a/include/pineforge/native_calendar.hpp +++ b/include/pineforge/native_calendar.hpp @@ -331,6 +331,42 @@ bool in_session(const SessionCalendar& calendar, int64_t ms); // --------------------------------------------------------------------------- // Civil conversion // --------------------------------------------------------------------------- +// A proleptic-Gregorian civil date: month 1-12, day 1-31. +struct NativeCivilDate { + int64_t year = 1970; + int month = 1; + int day = 1; +}; + +// The civil date `days` days after 1970-01-01 (before it when negative): +// Howard Hinnant's civil_from_days, integer arithmetic exact for every day +// count whose year fits int64_t. The one copy of this arithmetic: the +// calendar, the time-of-day helpers, the report's month keys and a host's +// own day keys all read it. +constexpr NativeCivilDate native_civil_date(int64_t days) noexcept { + const int64_t z = days + 719468; + const int64_t era = (z >= 0 ? z : z - 146096) / 146097; + const int64_t doe = z - era * 146097; // [0, 146096] + const int64_t yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399] + const int64_t doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // from March 1 + const int64_t mp = (5 * doy + 2) / 153; // 0 = March + const int64_t month = mp < 10 ? mp + 3 : mp - 9; + return NativeCivilDate{yoe + era * 400 + (month <= 2 ? 1 : 0), + static_cast(month), + static_cast(doy - (153 * mp + 2) / 5 + 1)}; +} + +// Its inverse: the days from 1970-01-01 to (year, month 1-12, day) +// (Hinnant's days_from_civil). Out-of-range months and days are not refused; +// the arithmetic continues through them. +constexpr int64_t native_civil_days(int64_t year, int month, int day) noexcept { + year -= month <= 2; + const int64_t era = (year >= 0 ? year : year - 399) / 400; + const int64_t yoe = year - era * 400; // [0, 399] + const int64_t doy = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1; // [0, 365] + const int64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096] + return era * 146097 + doe - 719468; +} enum class CivilKind { Unique, Gap, Fold }; diff --git a/src/engine_metrics.cpp b/src/engine_metrics.cpp index 42a1343d..258d6b2a 100644 --- a/src/engine_metrics.cpp +++ b/src/engine_metrics.cpp @@ -5,6 +5,7 @@ * ; docs/pages/report-schema.md summarizes them. */ #include +#include #include #include @@ -78,15 +79,8 @@ int month_key_utc(int64_t ts_ms) { constexpr int64_t kCivilSpan = int64_t{1} << 40; if (secs > -kCivilSpan && secs < kCivilSpan) { const int64_t days = secs / 86400 - (secs % 86400 < 0 ? 1 : 0); - const int64_t z = days + 719468; - const int64_t era = (z >= 0 ? z : z - 146096) / 146097; - const int64_t doe = z - era * 146097; - const int64_t yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - const int64_t doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - const int64_t mp = (5 * doy + 2) / 153; - const int64_t month = mp < 10 ? mp + 3 : mp - 9; - const int64_t year = yoe + era * 400 + (month <= 2 ? 1 : 0); - return static_cast(year * 12 + month - 1); + const native_calendar::NativeCivilDate date = native_calendar::native_civil_date(days); + return static_cast(date.year * 12 + date.month - 1); } struct tm tb {}; gmtime_r(&secs, &tb); @@ -106,23 +100,13 @@ int keep_utc_month(UtcMonthMemo& memo, int64_t ts_ms) { constexpr int64_t kCivilSpan = int64_t{1} << 40; if (secs > -kCivilSpan && secs < kCivilSpan) { const int64_t days = secs / 86400 - (secs % 86400 < 0 ? 1 : 0); - const int64_t z = days + 719468; - const int64_t era = (z >= 0 ? z : z - 146096) / 146097; - const int64_t doe = z - era * 146097; - const int64_t yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - const int64_t doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - const int64_t mp = (5 * doy + 2) / 153; - // The March-based year starts `doy` days back; its month mp starts at - // day (153 * mp + 2) / 5 of it, and its last month (February) runs to - // the year's end, 366 days on when the civil year holding that - // February -- yoe + 1 modulo 400 -- is a leap year. - const int64_t march_first = days - doy; - const int64_t civil = yoe + 1; - const bool leap = civil % 4 == 0 && (civil % 100 != 0 || civil % 400 == 0); - const int64_t next = mp < 11 ? march_first + (153 * (mp + 1) + 2) / 5 - : march_first + (leap ? 366 : 365); - const int64_t lo = std::max((march_first + (153 * mp + 2) / 5) * 86400, - -kCivilSpan + 1); + // The civil month of the floor day, and the first day of the next. + const native_calendar::NativeCivilDate date = native_calendar::native_civil_date(days); + const int64_t first = native_calendar::native_civil_days(date.year, date.month, 1); + const int64_t next = date.month < 12 + ? native_calendar::native_civil_days(date.year, date.month + 1, 1) + : native_calendar::native_civil_days(date.year + 1, 1, 1); + const int64_t lo = std::max(first * 86400, -kCivilSpan + 1); const int64_t hi = std::min(next * 86400, kCivilSpan); if (month_key_utc(lo * 1000) == key && month_key_utc((hi - 1) * 1000) == key) { memo.lo = lo; diff --git a/src/native_calendar.cpp b/src/native_calendar.cpp index 7d479050..b1ca8c91 100644 --- a/src/native_calendar.cpp +++ b/src/native_calendar.cpp @@ -96,28 +96,16 @@ int64_t floor_div(int64_t a, int64_t b) { return q; } -// Howard Hinnant days_from_civil / civil_from_days. +// Howard Hinnant's days_from_civil / civil_from_days, in this file's types: +// native_civil_days / native_civil_date (native_calendar.hpp) are the one copy +// of the arithmetic. int64_t days_from_civil(int y, unsigned m, unsigned d) { - y -= m <= 2; - const int64_t era = (y >= 0 ? y : y - 399) / 400; - const unsigned yoe = static_cast(y - era * 400); - const unsigned doy = (153u * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; - const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; - return era * 146097 + static_cast(doe) - 719468; + return native_civil_days(y, static_cast(m), static_cast(d)); } CivilDate civil_from_days(int64_t z) { - z += 719468; - const int64_t era = (z >= 0 ? z : z - 146096) / 146097; - const unsigned doe = static_cast(z - era * 146097); - const unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - int y = static_cast(yoe) + static_cast(era * 400); - const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - const unsigned mp = (5 * doy + 2) / 153; - unsigned d = doy - (153 * mp + 2) / 5 + 1; - unsigned m = mp < 10 ? mp + 3 : mp - 9; - y += (m <= 2); - return CivilDate{y, static_cast(m), static_cast(d)}; + const NativeCivilDate date = native_civil_date(z); + return CivilDate{static_cast(date.year), date.month, date.day}; } bool valid_civil_date(int y, int m, int d) { diff --git a/src/session_time.cpp b/src/session_time.cpp index 3d23c4cb..71648cd2 100644 --- a/src/session_time.cpp +++ b/src/session_time.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include "timezone.hpp" @@ -39,26 +40,20 @@ bool in_civil_span(int64_t secs) { return secs > -kCivilSpan && secs < kCivilSpan; } -// gmtime_r's nine ISO fields of `secs` (in_civil_span only). +// gmtime_r's nine ISO fields of `secs` (in_civil_span only): the date is +// native_calendar::native_civil_date of the floor day. void civil_fields(int64_t secs, struct tm& out) { const int64_t days = secs / 86400 - (secs % 86400 < 0 ? 1 : 0); const int64_t second_of_day = secs - days * 86400; - const int64_t z = days + 719468; - const int64_t era = (z >= 0 ? z : z - 146096) / 146097; - const int64_t doe = z - era * 146097; - const int64_t yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - const int64_t doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // from March 1 - const int64_t mp = (5 * doy + 2) / 153; // 0 = March - const int64_t year = yoe + era * 400 + (mp >= 10 ? 1 : 0); - const bool leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + const native_calendar::NativeCivilDate date = native_calendar::native_civil_date(days); out.tm_sec = static_cast(second_of_day % 60); out.tm_min = static_cast(second_of_day / 60 % 60); out.tm_hour = static_cast(second_of_day / 3600); - out.tm_mday = static_cast(doy - (153 * mp + 2) / 5 + 1); - out.tm_mon = static_cast(mp < 10 ? mp + 2 : mp - 10); - out.tm_year = static_cast(year - 1900); + out.tm_mday = date.day; + out.tm_mon = date.month - 1; + out.tm_year = static_cast(date.year - 1900); out.tm_wday = static_cast(((days + 4) % 7 + 7) % 7); // 1970-01-01: Thursday - out.tm_yday = static_cast(mp < 10 ? doy + 59 + (leap ? 1 : 0) : doy - 306); + out.tm_yday = static_cast(days - native_calendar::native_civil_days(date.year, 1, 1)); out.tm_isdst = 0; } @@ -70,12 +65,7 @@ bool same_civil_fields(const struct tm& a, const struct tm& b) { // Days from 1970-01-01 to (year, month 1-12, day): civil_fields' inverse. int64_t days_from_civil(int64_t year, int64_t month, int64_t day) { - year -= month <= 2; - const int64_t era = (year >= 0 ? year : year - 399) / 400; - const int64_t yoe = year - era * 400; - const int64_t doy = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1; - const int64_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; - return era * 146097 + doe - 719468; + return native_calendar::native_civil_days(year, static_cast(month), static_cast(day)); } // The zone name gmtime_r reports ("UTC" on macOS, "GMT" on glibc): libc's own diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index e87d5f9c..ad96b88d 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -4,6 +4,7 @@ #include +#include #include #include "../engine_internal.hpp" @@ -13839,21 +13840,15 @@ namespace { constexpr std::int64_t kChartDayCivilSpan = std::int64_t{1} << 40; // chart_day_key on a UTC chart for a second `secs` inside the arithmetic's -// span: gmtime_r's day and month by Howard Hinnant's civil_from_days on the -// floor day. Everything below reads the floor day alone, so the day's seconds -// all answer its key, and `index` (when there is one) keeps them with it. +// span: gmtime_r's day and month, the kernel's civil date of the floor day +// (native_calendar::native_civil_date). Everything below reads the floor day +// alone, so the day's seconds all answer its key, and `index` (when there is +// one) keeps them with it. [[gnu::noinline]] std::int64_t civil_chart_day_key(AdapterLookupIndex* index, std::int64_t secs) noexcept { const std::int64_t days = secs / 86400 - (secs % 86400 < 0 ? 1 : 0); - const std::int64_t z = days + 719468; - const std::int64_t era = (z >= 0 ? z : z - 146096) / 146097; - const std::int64_t doe = z - era * 146097; - const std::int64_t yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - const std::int64_t doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - const std::int64_t mp = (5 * doy + 2) / 153; - const std::int64_t day = doy - (153 * mp + 2) / 5 + 1; - const std::int64_t month = mp < 10 ? mp + 3 : mp - 9; - const std::int64_t key = day * 100 + month; + const native_calendar::NativeCivilDate date = native_calendar::native_civil_date(days); + const std::int64_t key = std::int64_t{date.day} * 100 + date.month; if (index != nullptr) { index->day_lo = std::max(days * 86400, -kChartDayCivilSpan + 1); index->day_hi = std::min(days * 86400 + 86400, kChartDayCivilSpan); diff --git a/src/timeframe.cpp b/src/timeframe.cpp index c1f55d7c..504f4e6c 100644 --- a/src/timeframe.cpp +++ b/src/timeframe.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "runtime_ambient.hpp" #include #include @@ -437,14 +438,9 @@ static const std::string& empty_session() { } /// Epoch day (days since 1970-01-01) of a proleptic-Gregorian civil date -/// (Howard Hinnant's days_from_civil; m is 1-based). +/// (native_calendar::native_civil_days; m is 1-based). static long days_from_civil(int y, int m, int d) { - y -= (m <= 2); - long era = (y >= 0 ? y : y - 399) / 400; - unsigned yoe = (unsigned)(y - era * 400); - unsigned doy = (153u * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; - unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; - return era * 146097L + (long)doe - 719468L; + return static_cast(native_calendar::native_civil_days(y, m, d)); } /// Monday that starts the ISO week of a wall-clock decomposition From 7ed4aea60cfd24d90dd37ce9acdb35e1236ae820 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 07:29:19 +0800 Subject: [PATCH 68/83] v19 hash step (R5 lane H-THIN): the Pine host leaves lot excursions to the kernel (E19), and a FIFO exit reserves its entry's own quantity (P10) The lane's one hash-moving commit, inside the unreleased v19 epoch (the source extension stays pineforge-source-adapter/v4). Every witness it moves is re-pinned here, once. E19 (AUDIT4 section 6, G2-22; measured by R5 lane H-MEASURE). PineStrategyHost overrode owns_lot_excursions() / closed_lot_excursion() with ab9714be's per-lot MFE/MAE model (RULING A48). Where that model and the kernel's sampler differed, the kernel's number was TradingView's (H-MEASURE's tapes, tests/test_e19_excursion_tape.cpp). The host now keeps the kernel's default: the owner model, its entry-bar mask declarations, its excursion caches and level fills (pine_strategy_host, pine_adapter, pine_scheduler_native) and their folds in pine_state_hash.cpp are deleted, and the broker-hash waivers follow. The corpus moves on 19 probes and only in their excursion cells: of the 335 trades whose cells moved, 324 now equal TradingView's, 11 moved closer and none farther (315 of the 320 trades a TradingView export identifies toward, 5 closer; the 15 TradingView lists one bar earlier, graded by their exit identity: 9 toward, 6 closer). The new tape tests/fixtures/e19_allin_trim (lab tv hthin-e19-allin-trim, c229f8fd) pins the all-in residual's own entry-bar call: TradingView books it with the fill alone, as the sampler does. The unit rows that pinned the host model's numbers carry "expectation corrected (R5 lane H-THIN, E19 ...)" notes. P10 (H-MEASURE's pyramiding tape). A default strategy.exit(from_entry) under FIFO reserved the whole book (200 in the adapter, 100 on TradingView's tape); it now reserves min(book, the entry's own cohort exposure), and the kernel closes the oldest lots first. The corpus does not move. The lane's tape hthin-x15b-fifo-fee-basis (f170f906) books the same reservation in its scenario B. Re-pinned, each value marked "expectation corrected (H-THIN, ...)" with its cause, harvested with each TU's own switch on this tree and on the same tree without P10 (so every value names E19 or P10); the same harvest on the base 0d76a099 reproduces every old pin, and X15 d/f move none: test_adapter_report_relower, test_adapter_recording_hash_witness, test_adapter_continuation_view, test_adapter_receipts_in_place, test_adapter_command_allocations, test_adapter_host_view_memo (E19 only); test_adapter_lookup_index_witness (E19; P10 also moves kFlip and one kFlip_trades row); test_adapter_quiet_bar (E19 moves 100 trade digests, which fold max_runup / max_drawdown; P10 moves trades and counts); test_publication_witness (156 digests; P10 moves 12 runs' closed trades and two runs' folds); test_adapter_live_state_equivalence (96 of 108 transcript digests: 40 with E19, 90 with P10). test_native_calendar_hash_ witness does not move. scripts/corpus_parity_baseline.txt: the 19 E19 probes, their engine_trades.csv byte-identical to the E19-only run. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit ab297364217954d6128e51a767a5920cc50400e9) INT26 pick note (late pick, accepted 2026-09-26 11:30): every source, header and test-code hunk applied unchanged. Its hash values fold into the single INT26 v19 hash re-pin (the late-picks list), so the eleven pinned witness files keep this branch's hash values here; the behaviour this commit moves is pinned here instead, harvested on this tree with each TU's own switch: - quiet_bar: 140 of 272 runs' trade digests (E19's excursions fold into the digest) and, where P10 moves them, their counts. Random10/0/* books 23 trades where the lane's tree keeps 24 and the tree before this pick 22: the FIFO exit Y now reserves its entry's own unit and the reversal B closes the rest (A->Y 2 becomes A->Y 1 + A->B 1 at the same price), on the book PAR-ORDERS' and PAR-ORDERS-2's rules leave at that bar. - publication: P10's trades in 12 runs and the folds of Storm17/Storm17M, every value equal to the lane's. - live_state: 96 of 108 digests (the transcript holds no hash value). 55 take the lane's own value; 39 had been moved by an earlier pick too, and 29 of those move their trade count by the lane's own amount; chains seeds 104731 and 523647 take a third value, bisected by pick: H-THIN's source on K-OCA-KEEP's tip gives the lane's value, on PAR-ORDERS' E14 commit (a6e6a005) the merged one -- P10's path meets E14's pending-entry trail. - lookup_index kFlip_trades: one trade, equal to the lane's. Every other pinned behaviour harvests as pinned. The corpus baseline takes the lane's line for order-deferred-flip-guaranteed-gap-stops-01; order-deferred-flip-pooc-cross-bar-01, which PAR-ORDERS-2 moves too, keeps its line until the one re-derivation after every pick (late-picks list, CORPUS4). --- include/pineforge/source/pine_adapter.hpp | 27 -- .../pineforge/source/pine_strategy_host.hpp | 36 -- scripts/broker_state_hash_waivers.txt | 8 +- scripts/corpus_parity_baseline.txt | 36 +- src/source/pine_adapter.cpp | 413 +---------------- src/source/pine_scheduler_native.cpp | 17 - src/source/pine_state_hash.cpp | 14 - src/source/pine_strategy_host.cpp | 335 -------------- tests/fixtures/e19_allin_trim/README.md | 27 ++ .../hthin-e19-allin-trim/meta.json | 16 + .../hthin-e19-allin-trim/metrics.json | 40 ++ .../hthin-e19-allin-trim/strategy.pine | 37 ++ .../hthin-e19-allin-trim/tv_trades.csv | 13 + tests/fixtures/pyramiding_count/README.md | 16 +- .../pyramiding_count/expectations.inc | 8 +- tests/l11a_host_excursion_fixture.hpp | 2 +- ...st_adapter_live_state_equivalence_data.hpp | 293 ++++++++---- ...test_adapter_lookup_index_witness_data.hpp | 7 +- tests/test_adapter_quiet_bar_pinned.inc | 426 ++++++++++++------ tests/test_aggregated_path_regressions.cpp | 29 +- tests/test_e19_excursion_path_order.cpp | 71 +-- tests/test_e19_excursion_tape.cpp | 45 +- tests/test_l10b_excursion_parity.cpp | 17 +- tests/test_l10d_entry_bar_excursion_masks.cpp | 18 +- tests/test_l10j_corpus_parity.cpp | 6 +- tests/test_l10k_corpus_parity.cpp | 23 +- tests/test_l10l_corpus_parity.cpp | 6 +- tests/test_l10y_full_equity_entry_split.cpp | 40 +- tests/test_l11a_host_excursion_twin.cpp | 58 +-- .../test_l11b_same_bar_stopout_excursion.cpp | 50 +- tests/test_publication_witness_pinned.inc | 40 +- tests/test_pyramiding_count_differential.cpp | 5 +- 32 files changed, 886 insertions(+), 1293 deletions(-) create mode 100644 tests/fixtures/e19_allin_trim/README.md create mode 100644 tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/meta.json create mode 100644 tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/metrics.json create mode 100644 tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/strategy.pine create mode 100644 tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/tv_trades.csv diff --git a/include/pineforge/source/pine_adapter.hpp b/include/pineforge/source/pine_adapter.hpp index 551bd7cd..571725ec 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -235,11 +235,6 @@ struct PlacementSnapshot { // later submit of the same instance (a bracket leg materialized when its // parent entry applies) must not re-stamp it to the submitting bar. bool projection_created_bar_pinned = false; - // Set when a carried bracket leg is force-executed at its own level on the - // bar its parent entry opened (ab9714be src/source/pine_fills.cpp:7695-7728). - // The close of such a leg must not inherit the exit bar's path sample: the - // owner books that fill at step 1, before update_per_trade_extremes(). - bool post_parent_calc_level_fill = false; std::int32_t projection_position_side = static_cast(PositionSide::FLAT); bool projection_after_close = false; bool projection_over_pyramiding = false; @@ -1226,15 +1221,6 @@ struct PineRiskState { bool intraday_cancel_pending = false; }; -// ab9714be pine_risk.cpp:256-292 (update_per_trade_extremes): the source -// host's per-lot excursion sampler. Folds one completed source bar's H/L/C -// into every open lot, honoring the entry-bar masks. Shared by the bar-close -// walk, the stream tick walk and the margin-call submit preload. -void sample_open_trade_extremes(std::vector& lots, - PositionSide side, int bar_index, const Bar& bar); -Bar margin_call_sample_bar(const Bar& bar, double fire_price, bool prefix_sample, - bool high_first, double mintick = 0.0, int slippage = 0); - class PineExecutionAdapter; // Allocation-free view facade for Appendix C's later C projection. L2 does @@ -1387,19 +1373,6 @@ class PineExecutionAdapter { std::optional resolve_margin_requirement( const NativeMarginRequirementView&) const; std::optional resolve_margin_call_units(const NativeMarginCallView&) const; - // True when the request is a source exit leg carrying priced stop, limit - // or trailing terms (L10j): its trade row folds the pre-fill path extremes. - bool source_priced_exit(std::uint64_t incarnation) const noexcept; - bool source_post_parent_calc_level_fill(std::uint64_t incarnation) const noexcept; - std::optional source_trail_offset_ticks(std::uint64_t incarnation) const noexcept; - bool source_margin_exit(std::uint64_t incarnation) const noexcept; - static bool source_kernel_liquidation(const native_order::DefinitionRef&) noexcept; - bool has_pending_market_exit(int current_interval_index = -1) const noexcept; - // The carried 1x long's opening money call precedes the bar's excursion - // sample only ahead of one resting full-position priced exit that the - // open does not reach (ab9714be pine_fills.cpp:164-218). - bool carried_long_money_precedes_priced_exit(const NativePrecommitView&, - double held_units) const; void on_bar_open(const Bar&, const NativeDecisionContext&); void on_tick(const Bar&, const NativeTickContext&); // Called from the generic calculation callback after the source script diff --git a/include/pineforge/source/pine_strategy_host.hpp b/include/pineforge/source/pine_strategy_host.hpp index 8627980b..72b67c65 100644 --- a/include/pineforge/source/pine_strategy_host.hpp +++ b/include/pineforge/source/pine_strategy_host.hpp @@ -277,13 +277,6 @@ class PineStrategyHost : public NativeStrategyHost, public BrokerStateHashProvid // parent's fill; TradingView's projection of that level is the adapter's. std::optional resolve_anchored_level( const NativeAnchoredLevelView&) const final; - // RULING A48: the source host owns per-lot excursion accounting (MFE/MAE) - // on the switched route. It samples every completed source bar's H/L/C - // with the owner's entry-bar masks, and supplies the closing row's two - // magnitudes through the kernel's single generic capability. - bool owns_lot_excursions() const noexcept final { return true; } - ClosedLotExcursion closed_lot_excursion( - const ClosedLotExcursionFacts&) const final; // The R5 R2 sizing classification of the adapter, for tests and for hosts // that need to know whether the core or the source owns a default // quantity at this point. @@ -948,39 +941,10 @@ class PineStrategyHost : public NativeStrategyHost, public BrokerStateHashProvid // Read-only test projection cache; no future execution can observe it. mutable std::vector source_pending_view_cache_; - // The owner's sampled excursion of one closing lot, in price points x - // quantity; closed_lot_excursion reports it on TradingView's basis. - ClosedLotExcursion owner_lot_excursion(const ClosedLotExcursionFacts&) const; - - // Transient excursion-sampler caches (RULING A48). Pure caches: each is - // re-derived from the immutable placement snapshots and the delivered - // decision context, and cleared at the applied-notification boundary. - mutable bool excursion_priced_fill_ = false; - mutable bool excursion_level_fill_ = false; - mutable bool excursion_margin_call_ = false; - // Which of the owner's two margin-slice chronologies the pending slice was - // born in: true when it samples only the traversed waypoint prefix (the - // POOC pre-script pass, or the 1x-long opening slice taken inside - // process_pending_orders before a priced exit's fill), false when the - // non-POOC end-of-bar opening trim inherits the complete script bar. - mutable bool excursion_margin_prefix_ = false; - // A slice the owner books with no sample of the current bar at all: the - // carried position's open slice and the general pre-exit slice own only - // their carried extremes and the fill itself. - mutable bool excursion_margin_fill_only_ = false; - mutable bool excursion_range_end_projection_ = false; - mutable double excursion_trail_offset_ticks_ = - std::numeric_limits::quiet_NaN(); // Held units the applied request's precommit saw (the owner's pre-fill // position_qty_); cleared after the applied notification. mutable double precommit_held_units_ = std::numeric_limits::quiet_NaN(); - // The matcher basis behind a TRAIL fill. ab9714be pine_fills.cpp:5766-5770 - // reads the arming peak off the PRE-slip fill price (apply_fill_slippage - // runs later, at :7136/:7184), while the booked - // ClosedLotExcursionFacts::fill_price the sampler sees is already slipped. - mutable double excursion_trail_raw_price_ = - std::numeric_limits::quiet_NaN(); }; using PineNativeHost = PineStrategyHost; diff --git a/scripts/broker_state_hash_waivers.txt b/scripts/broker_state_hash_waivers.txt index c8be3efc..22ffc8ac 100644 --- a/scripts/broker_state_hash_waivers.txt +++ b/scripts/broker_state_hash_waivers.txt @@ -53,10 +53,6 @@ bar_magnifier_ # install-time fact mirrored from the scheduler (retained_.bar_ma 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. 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. -excursion_margin_call_ # Transient excursion-sampler cache; re-derived from the immutable placement snapshots at every precommit and cleared at the applied boundary. -excursion_priced_fill_ # Transient excursion-sampler cache; re-derived from the immutable placement snapshots at every precommit and cleared at the applied boundary. -excursion_range_end_projection_ # Report-only projection scope flag around range-end row building; it is false for every real settlement. -excursion_trail_offset_ticks_ # Transient excursion-sampler cache; re-derived from the immutable native request trigger at every precommit and cleared at the applied boundary. -precommit_held_units_ # Transient precommit cache; re-derived from the physical position at every precommit and cleared at the applied-fill boundary (same class as the excursion_* caches). -kernel_margin_path_point_ # Transient within-point arming value for the kernel's margin check gate: driver point ordinals are strictly monotone and margin_check_allowed only compares this for equality with the current point's, so from the first point after the one it names it can never match again; it is re-decided from already-hashed adapter state at every point (same class as the excursion_* and precommit_held_units_ caches). +precommit_held_units_ # Transient precommit cache; re-derived from the physical position at every precommit and cleared at the applied-fill boundary (a within-request cache, never a decision input past the applied boundary). +kernel_margin_path_point_ # Transient within-point arming value for the kernel's margin check gate: driver point ordinals are strictly monotone and margin_check_allowed only compares this for equality with the current point's, so from the first point after the one it names it can never match again; it is re-decided from already-hashed adapter state at every point (same class as the precommit_held_units_ cache). kernel_margin_resize_point_ # Transient within-point arming value for the kernel's post-exit margin re-size gate: same class and same argument as kernel_margin_path_point_ above -- a strictly monotone driver point ordinal that margin_check_allowed only compares for equality with the current point's, re-decided from already-hashed adapter state at every applied point. diff --git a/scripts/corpus_parity_baseline.txt b/scripts/corpus_parity_baseline.txt index 6d45c519..f4a80b7d 100644 --- a/scripts/corpus_parity_baseline.txt +++ b/scripts/corpus_parity_baseline.txt @@ -25,15 +25,15 @@ aadeb426e49358a2c6d043d8579e048b562994d76e87533dd46d48d06f1f93fc validation/ban 10b93f4e62b1647eb6f1680670de4cc89bf61032240431aee8ae3c88d7e9730a validation/bracket-aurora-market-atr-01/engine_trades.csv 8d2b4718f7006e3f10e42ec381a1359bebb673c1ccc2afbf8cca4a062307d6ac validation/bracket-compass-partial-ladder-01/engine_trades.csv ca05f64f8447d23cb0cce0b93f7066a4b6dd9e1dbfa7495fc0b878017ffaa4c1 validation/bracket-entry-exit-same-pass-attach-01/engine_trades.csv -9fa1cd1b502e1001ce150d70274b89cd666cd497580429fb15cc51ce2e215f76 validation/bracket-exit-stop-limit-trail-same-bar-01/engine_trades.csv +d050b168ec710e42fc68ae7e2c82016d3d84ca3da1b42e6b7829b449ff827a0d validation/bracket-exit-stop-limit-trail-same-bar-01/engine_trades.csv 9f8be3897563d4d8e2cbe277d2ef994b039e232f6d6bd2c7552caaf3eb5fef03 validation/bracket-exit-three-way-set-once-entry-01/engine_trades.csv fb2105f45c7ae20c54b3dde6d6f834ce1fc1a66cf54b7c5a29de3c3284031354 validation/bracket-exit-tp-sl-fixed-01/engine_trades.csv 9705ac82db676135bdd7df24d1df78a431af7cc9732e1d4a29224c3cd494e91e validation/bracket-narrow-stop-limit-with-trail8-01/engine_trades.csv 17a169cd9aecf6f49e2b4e4ab3e95166c35646a0b0e52cb98768689e4f985c63 validation/bracket-partial-exit-qty-percent-01/engine_trades.csv d57cb94b79c382b91cc21e127eb517a61f63c7902b65cf0ea1feeb42dcf1ed12 validation/bracket-revision-exit-replace-01/engine_trades.csv -7074b44bf33362b5df184aad964a2cc5424d69781c8b00f6d77115ef5b7e1f35 validation/bracket-rivet-calc-on-fill-01/engine_trades.csv -932f528212d82eafc7c018f33367ab4b0165210904b085edb85848c0ca7180e9 validation/bracket-same-id-exit-replace-01/engine_trades.csv -7b86aba7c4f730a96b8f927eecc9913843212e98f3ee0834a1ecaf0d7be30327 validation/bracket-tp-sl-oca-reduce-isolate-01/engine_trades.csv +405279b817c4ef78a7a6f1e84000206d47891b4b4a8137e76f56c0b59b430cae validation/bracket-rivet-calc-on-fill-01/engine_trades.csv +a007198ae414a2fb024dde64547c3849673d6d9cdaba0532dcdd76d040694d72 validation/bracket-same-id-exit-replace-01/engine_trades.csv +dc90b6f2ee1b1b8ccf4b8a12fb9da4aa77e01f308a8a5c4b3527d591f3686bbf validation/bracket-tp-sl-oca-reduce-isolate-01/engine_trades.csv 822935d47354c083cd9fe594cbfbcb02a9a5ffeedd829af8d0ca0252ec4e7ae0 validation/bracket-trail-points-no-offset-explicit-01/engine_trades.csv f7a1f6996c9f7285d8f81d5f25a33dd6a640aa745f8e8ea3f84c76c106ef1978 validation/bracket-trail-points-with-offset-only-01/engine_trades.csv 2786565a6b90a709bc6440576236ebd4bf1723c05ced30631eae1f96812323b4 validation/bracket-trailing-activation-offset-path-01/engine_trades.csv @@ -48,7 +48,7 @@ f2e058f53a052e1d1e6f67134a139d1dd4950eae2b8e295696db6079fd77a899 validation/com 6c0023b7f637f31b3b635d7be707e35d685ece98369ea3c80e5d1f8db7976b48 validation/composite-boscurv-integration-01/engine_trades.csv a31eacce9285ae40e9198a51f3a2b5b80e4ec0e794b34e5b0c36e9eb3643daaf validation/composite-boscurv-linreg-slope-channel-01/engine_trades.csv 55d4fb1fdf9a2ba5df4eab9f79f3e8d8e080b51bb7ce25a569d80b791fda7d3f validation/composite-boscurv-pivot-bos-trigger-01/engine_trades.csv -56acda82be9ad0aec084c66036a94785837252a676c200b6a912d18c68acfae5 validation/composite-bracket-cap-range-pending-stop-01/engine_trades.csv +52a10537998e9a1813aff0f7a764080e51877738541d1c4ecfa6c7888c3bc680 validation/composite-bracket-cap-range-pending-stop-01/engine_trades.csv 30d16a4f753bb8958c6d2c3f9b80f3a7cc3a732d1ef61ce3c443a1d1ad664dbc validation/composite-ies-adx-regime-classify-01/engine_trades.csv 2ae01055be6624baeb5b92f3d48c2fbf8e5a89166f269556b7a86e18bff4fcee validation/composite-ies-bb-kc-squeeze-release-01/engine_trades.csv 538e21627712df34cdafdf2ffc9e679f83cc3acc26bc390fe5e42b563880d3c5 validation/composite-ies-cooldown-daily-cap-01/engine_trades.csv @@ -76,8 +76,8 @@ f6ae96c95b225d946bc3a2004065a466e5600a62f29d7b893133c1aa67c02b07 validation/com f7b73457565b6a4120e20c2603223b4facaa393573e9a041ccfefeadd5fd0bb5 validation/composite-marketshift-rolling-highest-lowest-01/engine_trades.csv 5e27b61e33b5c0c26736525241b1cc85ddf66af6c31f04fef1c8f4f1fd612d6a validation/composite-marketshift-state-edge-detector-01/engine_trades.csv 43445bd654f3cc1ca0897bdaa38cba8e8d0e0b55851abd0766cb070426fdd5c2 validation/composite-scalping-fast-ma-cross-trigger-01/engine_trades.csv -25efa461785a60ff19aa8420cfbf3dba8ddf85ca50953b2fae3c926b1febc459 validation/composite-scalping-integration-01/engine_trades.csv -25efa461785a60ff19aa8420cfbf3dba8ddf85ca50953b2fae3c926b1febc459 validation/composite-scalping-tight-tp-sl-points-01/engine_trades.csv +45a5da914ebf7c3fe0b2a6eb806cfc175a25d52c20c928fd22dc8c7621f45f49 validation/composite-scalping-integration-01/engine_trades.csv +45a5da914ebf7c3fe0b2a6eb806cfc175a25d52c20c928fd22dc8c7621f45f49 validation/composite-scalping-tight-tp-sl-points-01/engine_trades.csv 0249dad411940c3dbf796cdd61dfe7c51cb0d0914148ef3b27b20a2c865959b9 validation/composite-trendmaster-integration-01/engine_trades.csv 9f9c06131cee6292ebd5409050e57151ebf50f5dbd744a2d1ba3bee481f3d3e7 validation/composite-trendmaster-line-new-projection-01/engine_trades.csv 1214d0242b57785c9160f551fd5e6f66c18837d116858f8b66b571ad3e5fa7d0 validation/composite-trendmaster-pivot-anchored-bracket-01/engine_trades.csv @@ -107,7 +107,7 @@ af8d0fa572f8583667492da1474533f862c14216112053eac15bda5c1a634270 validation/dra 73d70576782d5f5afa1a76ab04a42d2634ae4cc22787c514d94af737318168f2 validation/ltf-bool-array-bull-majority-01/engine_trades.csv 511c5043bf4790301dc6b47fae00fa0e380bdcc424eb7101f087ca9e1830ff7f validation/ltf-numeric-float-ratio15-01/engine_trades.csv 3ba0f3ac2dab8535d83156d404c7364ff4a357a5cc374a0ed29a46d91c799626 validation/magnifier-tick-dist-endpoints-01/engine_trades.csv -93aec602d059025086e9ddce9a574121194c20e3c40a972bf1a397e2293b5e36 validation/magnifier-tick-dist-endpoints-rsi-cross-08a/engine_trades.csv +d0d95b4cf303c98d1a4a61110d86b66bb8ae5c0ca4374d305f56fdd4ece01108 validation/magnifier-tick-dist-endpoints-rsi-cross-08a/engine_trades.csv 3ba0f3ac2dab8535d83156d404c7364ff4a357a5cc374a0ed29a46d91c799626 validation/magnifier-tick-dist-volume-weighted-on-01/engine_trades.csv 528728f766285a5463329b8661b3ca529efae7b3e98a7854bc667a8ef84004bc validation/map-archive-presence-01/engine_trades.csv 549bb6d0bb986df7171c1c1dd27f4fd494fa2b4ebcaf20b906084e5e51189255 validation/map-mosaic-regime-weight-01/engine_trades.csv @@ -153,17 +153,17 @@ b1c6e2f74a263c3ed0402b20cca655b329326c68b9e91405bf874d74be0e8fe0 validation/ord 59a142a1c1d14d7fb746e96576785173cf718a9773e8f4db069a8611a0d0d284 validation/order-cross-entry-close-same-pass-01/engine_trades.csv c9d1428ff858ea15fb8634b4634e4f438e0ff38b39e19f27e94d552ed98b821c validation/order-cross-exit-close-same-pass-01/engine_trades.csv 5804e69d5646d827f6c300b54d02c063c54e8071e999db3e4cbe9458b9c5413e validation/order-crosscurrent-opposite-market-pair-01/engine_trades.csv -b71d4a6a1d3c177473f041e56504820f9c06c3ba4588fc8b8827c44f24503ed6 validation/order-deferred-flip-guaranteed-gap-stops-01/engine_trades.csv +df3c35f22d522c679e62550bcd4fb16aec7825e93b9c5a9700235e103983a8c9 validation/order-deferred-flip-guaranteed-gap-stops-01/engine_trades.csv cdf4e5cc5c8d45d12da36bb8f0e34e0aa3164a0c9fb54850f89dfbff335ca61b validation/order-deferred-flip-pooc-cross-bar-01/engine_trades.csv 12c029df1fbe1b2b071e6ee2b9f43039d0359ae02bf2ad0ed3260bb3adadda1e validation/order-dual-four-bar-stop-no-close-01/engine_trades.csv 560e75eadf284e4374e77479fa13edf0629cfad1bfa5d76f1cfb5dbe619eb8c4 validation/order-dual-side-same-id-stop-no-cancel-01/engine_trades.csv -a03be8fcb680efdaaf09d4e38f7c7dc9c2f5a74948feb9db6cc57c7c9188a5db validation/order-dual-stop-both-touch-priority-01/engine_trades.csv +bdecaf2fe781c96031e7475473a0d4e112f6dc0aaa4b881eb7a15dceb39402aa validation/order-dual-stop-both-touch-priority-01/engine_trades.csv 24e75d0af83ee9228f672ec43ae4df2b21c7244eff00470c60ef75b3902a2869 validation/order-dual-stop-cancel-rotation-01/engine_trades.csv 49808fa0161711fe239648f22c2212e556956a924d2461588ec8eb7198592b57 validation/order-dual-stop-far-only-01/engine_trades.csv d5d864117237be6a231581e7a78cb96a148b52459bfade60627813e9d4ef6c60 validation/order-dual-stop-near-only-01/engine_trades.csv dc8ae5a1b50caa8f23e8b19267415a7da7f30c42907952b895db8fad82c2f233 validation/order-dual-stop-open-high-first-path-01/engine_trades.csv 857d5b4f57f5f75c83723190bcf88aa440a7ca1dfc4c84127c4ec605e763f494 validation/order-dual-stop-open-low-first-path-01/engine_trades.csv -29dbbd367fae6980880ad2a96a424e8702cce82b78173fbea7534521f10bc84b validation/order-dual-stop-open-tie-01/engine_trades.csv +349f134599f57de7517c846f963ce6c4cb66f05fdf93bdcd97f365db81124a89 validation/order-dual-stop-open-tie-01/engine_trades.csv d5d864117237be6a231581e7a78cb96a148b52459bfade60627813e9d4ef6c60 validation/order-dual-stop-source-order-long-first-01/engine_trades.csv 2743deb0a97413d09d4c3d780ba3e8311043a993feb7d5725da3841405a961fd validation/order-dual-stop-source-order-short-first-01/engine_trades.csv 670d29c6148c58dfb705dc8275e0493b9db93b9cfaabd44ae0ab3d65f6eef908 validation/order-entry-implicit-reversal-exit-01/engine_trades.csv @@ -178,23 +178,23 @@ c87ea211867e2348597e9bfa8f4d5fa8bbe7042f61f209d8b565c7a2f1f0672e validation/ord 7c8f19ade2b6fb3e505fd4a553456e666ddde57bfbfa20c7dda34985ba03152b validation/order-percent-equity-cash-commission-01/engine_trades.csv a417e420e06d9d901da04455678cd447c267ff952435d5eb37d6e1990727b896 validation/order-process-on-close-false-01/engine_trades.csv 0d4fd87450627c8c857d41b85b4f650feb86daf8e2af2f8d4b7157ef38dcdb30 validation/order-process-on-close-true-01/engine_trades.csv -1eb00e9b8f32d7cc15c3b7b8353c28acbff3f42bf313b9c288f925de8ed5dfcd validation/order-range-expansion-pending-stop-01/engine_trades.csv +24f93efaabd752be076564c8a2f16f233144ae91bbc5b2f852ace6f91a549531 validation/order-range-expansion-pending-stop-01/engine_trades.csv 2129f71f51568928782384086952d0c0dff827e23f8f963f98a24bbe6966bb8a validation/order-same-id-entry-close-same-bar-01/engine_trades.csv e171e29cb4a5e4d9367e63d003bc032ff8a720e19fe3ec19d35f69342e4d5727 validation/order-same-id-market-entry-repeat-01/engine_trades.csv 1b2867707e5bbfeb8e5d5a0531c2b51e015fc7229db664b7814329c79c7dd65b validation/order-same-id-stop-after-flat-01/engine_trades.csv -4f46b79d2d78b7fdaa51cf5d3034410cd143fe9daf46a1a37f1ff6b4a05431af validation/order-same-id-stop-cross-before-modify-01/engine_trades.csv +1d374a06222a06d96e125cc12117df2450e37166d57dfabcbdafc5fb2e20e265 validation/order-same-id-stop-cross-before-modify-01/engine_trades.csv b407bd4350a98a6f25484f28db4a8d2ee97d2642ece95fe13b5b78da829d27c4 validation/order-same-id-stop-minute-zero-01/engine_trades.csv 560e75eadf284e4374e77479fa13edf0629cfad1bfa5d76f1cfb5dbe619eb8c4 validation/order-same-id-stop-modification-01/engine_trades.csv 76b56848bfbe176f210cbc5ae3e27edb4b9f8b491f3038b390cc9286e1194e4f validation/order-same-id-stop-raise-only-01/engine_trades.csv 0249e31b2396a21c25e3ed1e05d5e5a72e772a0e8937d6326ddcc6c0903ac83d validation/order-same-id-stop-window-four-bars-01/engine_trades.csv fedde267ca50bab0f83caf0e18923b08b29662a3d4115a44201968ff9f2b190c validation/order-stale-stop-after-close-no-cancel-01/engine_trades.csv -27687f3a416b1bc8450012e73e83de19471da2ad1d45fe6646c09ff1454a8fdd validation/order-stop-cancel-no-regime-close-01/engine_trades.csv -4227dda3e440668982ba47de0f37cc048400bc5a094fb5625d96adac17ee22dd validation/order-stop-entry-cancel-opposite-01/engine_trades.csv -99c3f7e0ee0703479c4fbf0888a4d5ebc8bc7d9f3e447c6ff28728bf1f5d4860 validation/order-stop-entry-reversal-grouping-01/engine_trades.csv -4d3d609243980d5e162a878dda7a26e3dde93a368748b13f2f82791b677a4bb5 validation/order-stop-entry-touch-boundary-01/engine_trades.csv +90697640361f1caaaea1fbd2045d28754d02bdef6e1eb69cd6ae2204cf04c7c5 validation/order-stop-cancel-no-regime-close-01/engine_trades.csv +981305286e28634dfef4787d1fc43d7fb21f8980c8f0aba40e6602703f3c0d41 validation/order-stop-entry-cancel-opposite-01/engine_trades.csv +9f0174a888a56b3b8b07853b069ea18bdb6ccd72b6b1117cda2e67d587b9cea3 validation/order-stop-entry-reversal-grouping-01/engine_trades.csv +a555930292190b4d2777f347accf418251f408ff83ee6b08ed6ee23aa2f040e0 validation/order-stop-entry-touch-boundary-01/engine_trades.csv b389e4b4eb72d0987bebf129187ea8eda6ff0ada26a77223462e9f05f1fadcee validation/order-switchback-all-in-reversal-01/engine_trades.csv 2152665598370f4571e7eaeb7a9e2aad81a9b65e6a60f21a31c326581dc90adc validation/order-tranche-partial-market-close-01/engine_trades.csv -6c32bc7b00a0c97a2bc69b53c897d17722d65ef82244a34dc2b882360371d539 validation/order-vector-stop-breakout-01/engine_trades.csv +8fc09840ccf9828c164d13b31c14ac3451fef1b3753b518a5ffeb30c44fb4bd9 validation/order-vector-stop-breakout-01/engine_trades.csv 277a164c777e9cf3e4ccaee60a6372fdbedfdd0fbd5fdc86c6becdfe2711d6db validation/pyramid-cash-fractional-commission-01/engine_trades.csv fa932273547984161218c80c1d76bacc6e7352774028c55b6f150274e3cb85b0 validation/pyramid-close-id-grouping-01/engine_trades.csv 7584b7b15bf528cb2606050f31465691e26a573d33e56ea37dc9855070034429 validation/pyramid-deferred-flip-close-all-01/engine_trades.csv diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index ad96b88d..982903f5 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -75,91 +75,6 @@ bool finite_non_negative(double value) noexcept { return std::isfinite(value) && value >= 0.0; } -// ab9714be pine_fills.cpp:384 routes a priced exit's fill through -// margin_call_slice_before_priced_exit, whose 1x-long arm -// (pine_fills.cpp:2328-2456) takes the entry-bar opening slice THERE -- inside -// process_pending_orders, strictly before that bar's update_per_trade_extremes -// (pine_scheduler.cpp:257). In that chronology neither the split-off residual -// nor the surviving main lot inherits the bar's H/L/C sample: the residual -// keeps its entry seed (fav 0, adv = its own commission) and the main lot only -// ever sees its exit fill. The end-of-bar opening branch is the one that -// samples the complete bar. A priced exit leg still resting from an EARLIER -// bar whose level this bar's range crosses is exactly the fill that preempts -// the slice, so it is the discriminator between the two sampling points. -template -bool opening_slice_precedes_priced_exit_fill(const Handles& handles, - const Placement& placement, - const Bar& bar, - int interval_index, - bool is_long, - double mintick, - double avg_price, - std::int64_t position_cycle, - const FromEntryFilled& from_entry_filled) noexcept { - // The discriminator is CYCLE-scoped, exactly like the owner's bracket - // liveness: only a leg whose from_entry filled in THIS position cycle is - // evaluated (ab9714be pine_fills.cpp:7669-7673), its activation and leg - // ownership are bound to position_cycle_seq_ and unbound the moment the - // book goes flat (pine_orders.cpp:597-608, 614-627), and its priced legs - // are tested against the live position's own direction. A bracket left - // over from a finished cycle -- or one prearmed for the opposite side -- - // never preempts the opening slice. A trail leg participates through its - // activation price, resolved from the live entry when the birth operand - // was points (pine_fills.cpp:7208-7230). - const auto expected_side = is_long - ? static_cast(PositionSide::LONG) - : static_cast(PositionSide::SHORT); - for (const auto& handle : handles) { - const auto found = placement.find(handle.incarnation); - if (found == placement.end()) continue; - const auto& row = found->second; - if (row.family != PineOrderFamily::ExitLimit - && row.family != PineOrderFamily::ExitStop - && row.family != PineOrderFamily::ExitTrail) { - continue; - } - if (position_cycle > 0 && row.placement_cycle != 0 - && row.placement_cycle != position_cycle) { - continue; - } - if (row.projection_position_side != expected_side - && row.projection_position_side != static_cast(PositionSide::FLAT)) { - continue; - } - // ab9714be pine_fills.cpp:7671-7674 removes an exit whose from_entry - // has not filled in the live position cycle before it can fill, so - // a bracket the script keeps re-issuing for the other side's id - // (re-priced off the live position's average) cannot preempt. - if (!row.from_entry.empty() && !from_entry_filled(row.from_entry)) continue; - if (row.projection_created_bar < 0 - || row.projection_created_bar > interval_index) { - continue; - } - if (row.family == PineOrderFamily::ExitStop && std::isfinite(row.exit_levels.stop)) { - if (is_long && bar.low <= row.exit_levels.stop) return true; - if (!is_long && bar.high >= row.exit_levels.stop) return true; - } - if (row.family == PineOrderFamily::ExitLimit && std::isfinite(row.exit_levels.limit)) { - if (is_long && bar.high >= row.exit_levels.limit) return true; - if (!is_long && bar.low <= row.exit_levels.limit) return true; - } - if (row.family == PineOrderFamily::ExitTrail) { - double trail_act = row.exit_levels.trail_price; - const double base_px = finite_positive(avg_price) ? avg_price : bar.open; - if (!finite_positive(trail_act) && finite_positive(row.exit_levels.trail_points) - && finite_positive(mintick) && finite_positive(base_px)) { - trail_act = base_px + (is_long ? 1.0 : -1.0) - * row.exit_levels.trail_points * mintick; - } - if (finite_positive(trail_act)) { - if (is_long && bar.high >= trail_act) return true; - if (!is_long && bar.low <= trail_act) return true; - } - } - } - return false; -} - // ab9714be pine_strategy_commands.cpp:533-537: a non-NaN limit/stop is a // present price level, including 0.0. bool price_present(double value) noexcept { return !std::isnan(value); } @@ -729,89 +644,6 @@ void audit_placement_scan(const PlacementTable& table, unsigned line, Predicate& #endif } // namespace -// ab9714be pine_fills.cpp:2009-2023: a margin slice born in a prefix-sampling -// chronology (the POOC pre-script pass, or the 1x-long opening slice taken -// inside process_pending_orders before a priced exit's fill) samples only the -// traversed waypoint prefix (open-trigger must not inherit a later high). -// Every other slice is the non-POOC end-of-bar opening trim, which -// process_margin_call runs after the ordinary full-bar sample, so the closed -// row inherits the complete bar. -Bar margin_call_sample_bar(const Bar& bar, double fire_price, bool prefix_sample, - bool high_first, double mintick, int slippage) { - if (!prefix_sample || !std::isfinite(fire_price)) return bar; - // A slipped open waypoint does not bit-match bar.open, so the prefix walk - // below would run past it to the close; the tick-scaled tolerance keeps an - // open-fired slice at the open. - const double slip_tol = (slippage + 1) * (mintick > 0.0 ? mintick : 0.01) + 1e-7; - if (std::abs(fire_price - bar.open) <= slip_tol) { - Bar prefix = bar; - prefix.high = prefix.low = bar.open; - prefix.close = fire_price; - return prefix; - } - const double path[4] = { - bar.open, - high_first ? bar.high : bar.low, - high_first ? bar.low : bar.high, - bar.close, - }; - // finding-446: a short's slice books at the nearest-tick rounded high, - // which need not bit-match the raw waypoint; without the tick fallback - // the walk would run past it and sample the whole bar. - int fire = -1; - for (int i = 0; i < 4 && fire < 0; ++i) { - if (same_double_bits(path[i], fire_price)) fire = i; - } - for (int i = 0; i < 4 && fire < 0 && mintick > 0.0; ++i) { - if (same_double_bits(nearest_tick(path[i], mintick), fire_price)) fire = i; - } - if (fire < 0) fire = 3; - Bar prefix = bar; - prefix.high = prefix.low = path[0]; - for (int i = 1; i <= fire; ++i) { - prefix.high = std::max(prefix.high, path[i]); - prefix.low = std::min(prefix.low, path[i]); - } - prefix.close = fire_price; - return prefix; -} - -// ab9714be pine_risk.cpp:256-292: sample the full bar H/L/C into every open -// lot. Legacy runs this AFTER pending-order fills and BEFORE -// process_margin_call, so a liquidation at the first extreme still owns the -// rest of the bar. Native apply_excursion stops when the lot is closed, so -// the adapter re-runs this walk at margin-call submit. -void sample_open_trade_extremes(std::vector& lots, - PositionSide side, int bar_index, const Bar& bar) { - if (side == PositionSide::FLAT || lots.empty()) return; - if (!std::isfinite(bar.high) || !std::isfinite(bar.low) - || !std::isfinite(bar.close)) { - return; - } - const bool is_long = (side == PositionSide::LONG); - for (auto& pe : lots) { - double pe_hi = bar.high; - double pe_lo = bar.low; - if (pe.entry_bar_index == bar_index) { - if (pe.skip_entry_bar_high) pe_hi = pe.price; - if (pe.skip_entry_bar_low) pe_lo = pe.price; - } - const double fav_px = is_long ? pe_hi : pe_lo; - const double adv_px = is_long ? pe_lo : pe_hi; - const double favorable = is_long ? (fav_px - pe.price) * pe.qty - : (pe.price - fav_px) * pe.qty; - const double adverse = is_long ? (pe.price - adv_px) * pe.qty - : (adv_px - pe.price) * pe.qty; - if (favorable > pe.max_runup) pe.max_runup = favorable; - if (adverse > pe.max_drawdown) pe.max_drawdown = adverse; - const double closing = is_long ? (bar.close - pe.price) * pe.qty - : (pe.price - bar.close) * pe.qty; - if (closing > pe.max_runup) pe.max_runup = closing; - const double closing_dd = -closing; - if (closing_dd > pe.max_drawdown) pe.max_drawdown = closing_dd; - } -} - PineExecutionAdapter::PineExecutionAdapter(compat::pine::CapAttachment attachment) : cap(attachment) { pending_view_.owner_ = this; @@ -5764,8 +5596,8 @@ bool PineExecutionAdapter::coof_fill_at_path_point(double waypoint) const noexce // The leg order the kernel walks `bar` in -- the run's declared // NativeRunSpec::path_order, the open-proximity rule under Auto -// (NativeExecutionConsumer::path_high_first, which E19's excursion seam -// already reads). The source layer asks rather than keeping a copy of the rule. +// (NativeExecutionConsumer::path_high_first). The source layer asks rather +// than keeping a copy of the rule. bool PineExecutionAdapter::source_path_uses_high_first(const Bar& bar) const noexcept { auto* pine = pine_view_of(host_); return pine != nullptr && detail::run_consumer(*pine).path_high_first(bar); @@ -9005,8 +8837,15 @@ void PineExecutionAdapter::exit(const SourceId& exit_id, const SourceId& from_en if (parent != placement_.end()) observe_pending_parent(parent->second); } for (const auto& parent : pending_entries_) observe_pending_parent(parent.snapshot); + // TradingView's default strategy.exit(from_entry) under FIFO closes the + // entry's own quantity -- the oldest lots first -- not the whole book + // (tests/test_pyramiding_count_differential.cpp, P10 on its lab tv tape). + const bool fifo_named_entry = !config_.close_entries_rule_any && !from_entry.empty(); + const double book_basis = fifo_named_entry + ? std::min(std::abs(physical.signed_units), cohort_exposure_for(from_entry)) + : std::abs(physical.signed_units); const double live_reservation_basis = binds_pending_reversal_entry ? 0.0 - : std::max(0.0, std::abs(physical.signed_units) + : std::max(0.0, book_basis - pending_same_bar_close_qty_ + pending_parent_units); const bool reservation_ok = compute_exit_reservation( exit_id, from_entry, qty, qty_percent, live_reservation_basis, @@ -10400,7 +10239,6 @@ void PineExecutionAdapter::flush_pending_bracket_legs( if (execute_after_calculation) { const double units = std::abs( detail::run_position(require_host()).signed_units); - leg.snapshot.post_parent_calc_level_fill = true; leg.snapshot.forced_execution_price = level; leg.snapshot.immediately = true; leg.snapshot.requested_qty = units; @@ -13016,231 +12854,11 @@ native_order::ExecutionTerms PineExecutionAdapter::resolve_terms( return result; } -bool PineExecutionAdapter::source_post_parent_calc_level_fill( - std::uint64_t incarnation) const noexcept { - // True when the request being committed is the bracket leg this route - // force-executed at its own level after its parent entry's calculation - // (ab9714be src/source/pine_fills.cpp:7695-7728). - const auto snapshot = placement_.find(incarnation); - return snapshot != placement_.end() - && snapshot->second.post_parent_calc_level_fill; -} - -// ab9714be pine_fills.cpp:5736-5744: priced exit fills flag fold_exit_path_extremes_ to fold pre-fill path excursion -bool PineExecutionAdapter::source_priced_exit(std::uint64_t incarnation) const noexcept { - const auto snapshot = placement_.find(incarnation); - if (snapshot == placement_.end()) return false; - const auto& source = snapshot->second; - return (source.family == PineOrderFamily::ExitLimit - || source.family == PineOrderFamily::ExitStop - || source.family == PineOrderFamily::ExitTrail) - && (std::isfinite(source.exit_levels.limit) - || std::isfinite(source.exit_levels.stop) - || std::isfinite(source.exit_levels.trail_points) - || std::isfinite(source.exit_levels.trail_price) - || std::isfinite(source.exit_levels.trail_offset)); -} - -std::optional PineExecutionAdapter::source_trail_offset_ticks(std::uint64_t incarnation) const noexcept { - const auto snapshot = placement_.find(incarnation); - if (snapshot == placement_.end()) return std::nullopt; - if (snapshot->second.family != PineOrderFamily::ExitTrail) return std::nullopt; - if (std::isnan(snapshot->second.exit_levels.trail_offset)) return 0.0; - return compat::pine::trail_offset_to_ticks(snapshot->second.exit_levels.trail_offset); -} - -bool PineExecutionAdapter::source_margin_exit(std::uint64_t incarnation) const noexcept { - const auto snapshot = placement_.find(incarnation); - if (snapshot == placement_.end()) return false; - return snapshot->second.family == PineOrderFamily::Margin; -} - -// A request the kernel's own margin model originated. Before on_applied -// adopts it into the source placement table there is no row to read, so its -// origin is the only thing that identifies it. -bool PineExecutionAdapter::source_kernel_liquidation( - const native_order::DefinitionRef& definition) noexcept { - return static_cast(definition) - && definition->origin == native_order::RequestOrigin::KernelLiquidation; -} - -bool PineExecutionAdapter::has_pending_market_exit(int current_bar) const noexcept { - const auto physical = detail::run_position(require_host()); - if (physical.signed_units == 0.0) return false; - const bool is_long = physical.signed_units > 0.0; - for (const auto& handle : live_handles_) { - const auto it = placement_.find(handle.incarnation); - if (it == placement_.end()) continue; - const auto& sn = it->second; - if (current_bar >= 0 && sn.projection_created_bar != current_bar - 1) { - continue; - } - if (sn.opening && sn.family == PineOrderFamily::Entry - && sn.is_long != is_long - && !std::isfinite(sn.exit_levels.stop) - && !std::isfinite(sn.exit_levels.limit) - && !std::isfinite(sn.exit_levels.trail_points) - && !std::isfinite(sn.exit_levels.trail_price) - && !std::isfinite(sn.exit_levels.trail_offset)) { - return true; - } - if (sn.family == PineOrderFamily::Close || sn.family == PineOrderFamily::CloseAll) { - const auto target_side = is_long ? static_cast(PositionSide::LONG) - : static_cast(PositionSide::SHORT); - if (sn.projection_position_side == target_side - || sn.projection_position_side == static_cast(PositionSide::FLAT)) { - return true; - } - } - } - return false; -} - -bool PineExecutionAdapter::carried_long_money_precedes_priced_exit( - const NativePrecommitView& view, double held_units) const { - // ab9714be pine_fills.cpp:164-218: the scheduler, account and single-lot - // book facts, then the one pending order of that book. - const auto state = detail::run_state(require_host()); - const double tick = staged_.syminfo.mintick; - if (config_.process_orders_on_close || config_.calc_on_order_fills - || stream_mode_ || (state.spec && !state.spec->intrabar.is_none()) - || !(held_units > 1.0) || detail::run_position(require_host()).lot_count != 1 - || config_.commission_value != 0.0 || config_.slippage != 0 - || std::abs(config_.margin_long - 100.0) > 1e-12 - || std::abs(staged_.syminfo.pointvalue - 1.0) > 1e-12 - || !staged_.account_fx_effective_from_ms.empty() - || active_staged_fx(view.cursor.point.effective_time_ms) != 1.0 - || cap.active() || risk_.max_intraday_loss != 0.0 - || risk_.max_drawdown != 0.0 || risk_.max_cons_loss_days > 0 - || !pending_entries_.empty() || !pending_same_bar_commands_.empty() - || !pending_bracket_legs_.empty() || !finite_positive(tick) - || !finite_positive(view.raw_price)) { - return false; - } - const PlacementSnapshot* exit = nullptr; - for (const auto& handle : live_handles_) { - if (handle == view.target) continue; - const auto found = placement_.find(handle.incarnation); - if (found == placement_.end()) return false; - const auto& leg = found->second; - // Broker margin slices are not source pending orders. - if (leg.family == PineOrderFamily::Margin) continue; - if (leg.family != PineOrderFamily::ExitLimit - && leg.family != PineOrderFamily::ExitStop) { - return false; - } - if (exit && (leg.source_id != exit->source_id - || leg.from_entry != exit->from_entry)) { - return false; - } - exit = ⋚ - } - if (!exit || exit->from_entry.empty() || exit->legs.dormant() - || exit->projection_created_bar >= projection_bar_index(view.cursor.point) - || !exit->oca_name.empty() || exit->oca_type != 0 - || !std::isnan(exit->exit_levels.trail_points) - || !std::isnan(exit->exit_levels.trail_offset) - || !std::isnan(exit->exit_levels.trail_price) - || std::isinf(exit->exit_levels.limit) || std::isinf(exit->exit_levels.stop)) { - return false; - } - const auto cohort = cohorts_by_id_.find(exit->from_entry); - if (cohort == cohorts_by_id_.end() || cohort->second.opened.empty()) return false; - // One ordinary own full-position reservation. - if (std::isnan(exit->requested_qty)) { - if (!std::isfinite(exit->qty_percent) || exit->qty_percent < 100.0) return false; - } else if (!std::isfinite(exit->requested_qty) - || exit->requested_qty < held_units - internal::kQtyEpsilon) { - return false; - } - const double limit = exit->exit_levels.limit; - const double stop = exit->exit_levels.stop; - if (!finite_positive(limit) && !finite_positive(stop)) return false; - // Every finite leg participates in opening marketability, on the - // tick-quantized open (broker_trigger_bar). - const double open = nearest_tick(view.raw_price, tick); - return !((std::isfinite(limit) && open >= limit) - || (std::isfinite(stop) && open <= stop)); -} - NativePrecommitVerdict PineExecutionAdapter::validate_precommit(const NativePrecommitView& view) const { const auto snapshot = placement_.find(view.target.incarnation); - // R5: a kernel-originated liquidation IS a margin slice, and it reaches - // this hook before it is booked -- before on_applied adopts it into the - // source placement table. The excursion chronology below is decided here, - // so it has to recognise the slice from its origin. - const bool kernel_liquidation = source_kernel_liquidation(view.definition); - if (snapshot != placement_.end() || kernel_liquidation) { - static const PlacementSnapshot kKernelLiquidation = [] { - PlacementSnapshot row; - row.family = PineOrderFamily::Margin; - return row; - }(); - const auto& source = snapshot != placement_.end() ? snapshot->second - : kKernelLiquidation; + if (snapshot != placement_.end()) { + const auto& source = snapshot->second; const auto physical = detail::run_position(require_host()); - // ab9714be pine_scheduler.cpp:257-278: process_margin_call runs after - // update_per_trade_extremes sampled the script bar into every lot that - // is still open, so the residual it splits off inherits that complete - // bar (POOC samples only the traversed waypoint prefix, - // pine_fills.cpp:2014-2023). RULING A48: the host owns the sample - // itself; this resolves which of the owner's two chronologies born the - // slice (complete-bar trim or traversed prefix). - if (source.family == PineOrderFamily::Margin - && view.inspected_closed_units > 0.0) { - if (auto* pine = pine_view_of(&require_host())) { - const Bar& sample_bar = pine->current_bar_; - const bool is_long_pos = pine->position_side_ == PositionSide::LONG; - const bool crosses = opening_slice_precedes_priced_exit_fill( - live_handles_, placement_, sample_bar, - view.cursor.point.interval_index, - is_long_pos, staged_.syminfo.mintick, - require_host().position_avg_price(), current_position_cycle_, - [this](const SourceId& id) { return from_entry_filled_this_cycle(id); }); - const bool one_x_long_opening = physical.signed_units > 0.0 - && !config_.process_orders_on_close - && std::isfinite(config_.margin_long) - && std::abs(config_.margin_long - 100.0) < 1e-12; - // ab9714be pine_fills.cpp:2224: the general pre-exit arm takes - // the slice only when the adverse extreme strictly precedes the - // priced exit's own fill; a crossing leg moves it ahead of the - // bar's update_per_trade_extremes. The 1x-long opening arm is - // the only current-coordinate route through that pre-exit hook. - const bool pre_exit_chronology = view.current - ? (one_x_long_opening && crosses) - : crosses; - pine->excursion_margin_prefix_ = config_.process_orders_on_close - || pre_exit_chronology; - // ab9714be pine_scheduler.cpp:167-191 and pine_fills.cpp: - // 2525-2678: a carried position's slice at the bar open runs - // while current_bar_ is restricted to the opening point, and - // pine_fills.cpp:2150-2305 books the general pre-exit slice - // before update_per_trade_extremes (pine_scheduler.cpp: - // 257-278) with fold_exit_path_extremes_ cleared - // (pine_fills.cpp:5858): either row owns only its carried - // extremes and the fill. - bool carried_open_slice = - view.cursor.point.path_phase == NativePathPhase::Open - && position_open_bar_index_ >= 0 - && position_open_bar_index_ < view.cursor.point.interval_index; - // ab9714be pine_fills.cpp:2544-2548 exempts a 1x long from the - // open slice: its opening-point rounded-money call is booked - // before sampling only through - // process_carried_long_money_before_priced_orders - // (pine_fills.cpp:164-218), whose book is exactly one resting - // full-position priced exit of the open lot that the open does - // not reach. Otherwise the end-of-bar process_margin_call - // (pine_fills.cpp:1350-1352, after pine_scheduler.cpp:257-270 - // sampled the bar) fires at the same open point and the slice - // inherits the complete bar. - if (carried_open_slice && one_x_long_opening) { - carried_open_slice = carried_long_money_precedes_priced_exit( - view, physical.signed_units); - } - pine->excursion_margin_fill_only_ = !config_.process_orders_on_close - && (carried_open_slice || (!view.current && crosses)); - } - } // ab9714be pine_fills.cpp:7449-7459: a deferred strategy.close belongs // to the position cycle it was issued in. When the live position // opened on a bar later than the close's creation bar — the next open's @@ -17126,8 +16744,8 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& // submit through submit_or_replace to record one. Adopt it into the // source placement table on arrival, with the same family the // adapter's own slice carried, so every Margin-family branch below -- - // the pending-sizing refresh, the bracket revival, the excursion - // sample, source_margin_exit -- sees it exactly as before. + // the pending-sizing refresh, the bracket revival -- sees it exactly + // as before. PlacementSnapshot adopted; adopted.family = PineOrderFamily::Margin; adopted.source_id = "__margin_call__"; @@ -17146,9 +16764,6 @@ void PineExecutionAdapter::on_applied(const native_order::ExecutionAppliedEvent& placement_snapshot->family == PineOrderFamily::ExitLimit || placement_snapshot->family == PineOrderFamily::ExitStop || placement_snapshot->family == PineOrderFamily::ExitTrail; - // RULING A48: the closed row's excursions come from the host's own - // per-lot sampler, so the resting-stop fill-based drawdown - // normalization this call site used to request is gone with it. if (auto* pine_host = pine_view_of(&require_host())) { pine_host->adapter_label_bracket_trades(event, from_bracket); } diff --git a/src/source/pine_scheduler_native.cpp b/src/source/pine_scheduler_native.cpp index 991571ba..bb00ddcb 100644 --- a/src/source/pine_scheduler_native.cpp +++ b/src/source/pine_scheduler_native.cpp @@ -630,23 +630,6 @@ void PineScheduler::recalculate(const native_order::ExecutionAppliedEvent& event else if (open_point && bar_known) coof_context.coordinate.path_phase = first_extreme; host.adapter_.begin_coof_recalc( event, coof_context, first_open, host.broker_fill_event_seq_); - // ab9714be pine_scheduler.cpp:357-366: under COOF every fill recalculation - // invokes update_per_trade_extremes() against the full script bar, except - // the carried positive-slip POOC opening-money chain at O - // (pine_scheduler.cpp:494-511), whose exits see only the open. - Bar extremes_bar = callback_bar; - if (host.config_.process_orders_on_close && host.config_.slippage > 0 && open_point) { - extremes_bar.high = extremes_bar.low = extremes_bar.close = extremes_bar.open; - } - // A plain aggregated chart's lots are booked by the kernel in script-bar - // space. Pine's excursion sampler still uses the chart source index here, - // so the entry-bar mask reads the chart bar without changing the lot. - const int extremes_index = !retained_.bar_magnifier - && detail::run_aggregates_input_bars(detail::run_consumer(host), &host.adapter_, - host.adapter_.run_counter_) - ? source_bar_index_for(context) : host.bar_index_; - sample_open_trade_extremes( - host.pyramid_entries_, host.position_side_, extremes_index, extremes_bar); try { host.scheduler_publish_source_bar( callback_bar, true, callback_advances_source_bar); diff --git a/src/source/pine_state_hash.cpp b/src/source/pine_state_hash.cpp index 4fed1d23..74173b06 100644 --- a/src/source/pine_state_hash.cpp +++ b/src/source/pine_state_hash.cpp @@ -86,7 +86,6 @@ void hash_placement(BrokerStateHashSink& f, const source::PlacementSnapshot& val f.i(value.placement_script_open_ms); f.i(value.placement_sub_open_ms); f.i(value.projection_created_bar); f.b(value.projection_created_bar_pinned); - f.b(value.post_parent_calc_level_fill); f.i(value.projection_position_side); f.b(value.projection_after_close); f.b(value.projection_over_pyramiding); f.b(value.projection_opposite_market_predecessor); @@ -707,11 +706,6 @@ void source::PineStrategyHost::hash_host_extension(BrokerStateHashSink& f) const f.i(override_.pyramiding); f.i(override_.slippage); f.i(override_.commission_type); f.i(override_.default_qty_type); f.i(override_.process_orders_on_close); f.i(override_.calc_on_order_fills); f.i(override_.close_entries_rule); - // Transient excursion-sampler cache (see pine_strategy_host.hpp): it is - // re-derived at every precommit, but it is next-decision visible to the - // excursion sampler inside an in-flight execution, so it is folded here - // rather than waived. - f.b(excursion_level_fill_); f.i(source_bar_index_); f.u(source_callback_count_); f.b(source_configuration_captured_); f.b(source_prepare_failed_); #ifdef PINEFORGE_HAS_AUX_SECURITY_FEED_V1 @@ -781,14 +775,6 @@ void source::PineStrategyHost::hash_host_extension(BrokerStateHashSink& f) const } } } - // The margin slice's sampling chronology is resolved once per pending - // slice in the precommit pass and read back by the host's own excursion - // sampler at settlement, so it is folded rather than waived. - f.b(excursion_margin_prefix_); - f.b(excursion_margin_fill_only_); - // The TRAIL peak basis is the precommit view's pre-slip matcher price, - // which no durable snapshot re-derives at settlement. - f.d(excursion_trail_raw_price_); adapter_.hash_state(f); scheduler_.hash_state(f); } diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index d0b05b32..334121fa 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -3,7 +3,6 @@ #include #include "../engine_internal.hpp" -#include #include "../timezone.hpp" #include "../native_execution_consumer.hpp" #include "pine_host_reads.hpp" @@ -24,42 +23,6 @@ using namespace source; namespace { -bool priced_opening_trigger(const native_order::Trigger& trigger) { - return std::holds_alternative(trigger) - || std::holds_alternative(trigger) - || std::holds_alternative(trigger); -} - -// The kernel still samples the delivered path at every driver point, which -// can book an entry-bar extreme the owner masks out (a gap-through stop is -// filled 1 ulp beyond the open). On the entry bar of a masked lot the host's -// own masked H/L/C walk is authoritative, so it replaces whatever the path -// sampling carried. - -void replace_masked_entry_bar_extremes(std::vector& lots, PositionSide side, - int bar_index, const Bar& bar) { - if (side == PositionSide::FLAT || lots.empty()) return; - if (!std::isfinite(bar.high) || !std::isfinite(bar.low) || !std::isfinite(bar.close)) - return; - const bool is_long = (side == PositionSide::LONG); - for (auto& pe : lots) { - if (pe.entry_bar_index != bar_index) continue; - if (!pe.skip_entry_bar_high && !pe.skip_entry_bar_low) continue; - const double pe_hi = pe.skip_entry_bar_high ? pe.price : bar.high; - const double pe_lo = pe.skip_entry_bar_low ? pe.price : bar.low; - const double fav_px = is_long ? pe_hi : pe_lo; - const double adv_px = is_long ? pe_lo : pe_hi; - const double favorable = is_long ? (fav_px - pe.price) * pe.qty - : (pe.price - fav_px) * pe.qty; - const double adverse = is_long ? (pe.price - adv_px) * pe.qty - : (adv_px - pe.price) * pe.qty; - const double closing = is_long ? (bar.close - pe.price) * pe.qty - : (pe.price - bar.close) * pe.qty; - pe.max_runup = std::max(0.0, std::max(favorable, closing)); - pe.max_drawdown = std::max(0.0, std::max(adverse, -closing)); - } -} - [[noreturn]] void reject_begin_bar(int index, const char* field, const char* detail) { throw std::invalid_argument( "bar[" + std::to_string(index) + "]." + field + (detail ? detail : "")); @@ -374,18 +337,6 @@ void source::PineStrategyHost::on_native_tick( if (source_prepare_failed_) return; if (detail::run_phase(*this) == NativeRunPhase::Realtime) stream_warmup_mode_ = false; - { - // ab9714be pine_stream.cpp:298/:450 samples the excursion at every - // realtime print (a price point: H == L == C == print). The explicit - // sample index keeps Pine's chart-bar sampling cadence separate from - // the kernel's script coordinate. - const int sample_index = scheduler_.bar_magnifier_enabled() - || detail::run_aggregates_input_bars(detail::run_consumer(*this), &adapter_, adapter_.run_counter_) - ? scheduler_.source_bar_index_for(context.decision) - : context.decision.coordinate.interval_index; - sample_open_trade_extremes( - pyramid_entries_, position_side_, sample_index, tick); - } scheduler_.tick(tick, context, *this); adapter_.on_tick(tick, context); } @@ -411,41 +362,8 @@ void source::PineStrategyHost::on_native_bar( diag_magnifier_sample_ticks_processed_ = bar_magnifier_enabled_ ? static_cast(context.driver_statistics.sample_ticks_processed) : 0; adapter_.observe_terminal_receipts(); - // Pine's entry-bar sampling uses the chart index explicitly. The kernel - // books every lot and row in script-bar space, including aggregated runs. - const bool aggregated = detail::run_aggregates_input_bars(detail::run_consumer(*this), &adapter_, adapter_.run_counter_); - const int sample_index = scheduler_.bar_magnifier_enabled() || aggregated - ? scheduler_.source_bar_index_for(context) - : context.coordinate.interval_index; - const int slippage_mask_index = aggregated && !scheduler_.bar_magnifier_enabled() - ? sample_index : context.coordinate.interval_index; - sample_open_trade_extremes( - pyramid_entries_, position_side_, sample_index, bar); - replace_masked_entry_bar_extremes( - pyramid_entries_, position_side_, sample_index, bar); scheduler_.bar(bar, context, *this); adapter_.on_bar_close(bar, context); - if (adapter_.config_.slippage > 0) { - for (auto& lot : pyramid_entries_) { - if (lot.entry_bar_index == slippage_mask_index && lot.qty > 0.0) { - const auto found = adapter_.placement_.find(lot.entry_incarnation); - if (found != adapter_.placement_.end()) { - const auto& snap = found->second; - const bool pure_stop_entry = snap.family == PineOrderFamily::Entry - && std::isfinite(snap.exit_levels.stop) && snap.exit_levels.stop > 0.0 - && !std::isfinite(snap.exit_levels.limit); - if (pure_stop_entry) { - // ab9714be pine_risk.cpp:276-282: update_per_trade_extremes measures adverse excursion against opposite extreme - if (lot.price > bar.high && std::isfinite(bar.low) && bar.low > 0.0) { - lot.max_drawdown = std::max(lot.max_drawdown, (lot.price - bar.low) * lot.qty); - } else if (lot.price < bar.low && std::isfinite(bar.high) && bar.high > 0.0) { - lot.max_drawdown = std::max(lot.max_drawdown, (bar.high - lot.price) * lot.qty); - } - } - } - } - } - } if (context.is_terminal_sub_bar && context.coordinate.interval_index == source_last_bar_index_) { scheduler_record_range_end(bar); @@ -525,24 +443,10 @@ void source::PineStrategyHost::on_native_applied( } } const auto p = adapter_.placement_.find(event.handle().incarnation); - // ab9714be pine_fills.cpp:42: a priced (stop/limit) entry masks the - // assumed-OHLC extreme the path reaches BEFORE the fill. const bool pine_priced = p != adapter_.placement_.end() && ((std::isfinite(p->second.exit_levels.stop) && p->second.exit_levels.stop > 0.0) || (std::isfinite(p->second.exit_levels.limit) && p->second.exit_levels.limit > 0.0)); if (event.opened_units != 0.0) { - // ab9714be pine_fills.cpp:42, said to the kernel instead of written - // into its lots: a priced entry's fill sits somewhere on the entry - // bar's path, and a close-phase fill sits after the whole of it. The - // kernel derives the mask from its own path geometry - // (declare_opened_lot_entry_bar_mask); this host never touches a lot. - if (pine_priced) { - declare_opened_lot_entry_bar_mask(event.handle().incarnation, current_bar_, - OpenedLotFillPoint::OnPath); - } else if (event.cursor.point.path_phase == NativePathPhase::Close) { - declare_opened_lot_entry_bar_mask(event.handle().incarnation, current_bar_, - OpenedLotFillPoint::AfterPath); - } // ab9714be pine_orders.cpp:750-753 and pine_fills.cpp:7189-7193 // (KI-62): a MARKET entry that adds to a live same-side position is // flagged so a same-bar from_entry bracket exit covers it. @@ -567,13 +471,6 @@ void source::PineStrategyHost::on_native_applied( if (broker_fill_event_seq_ == std::numeric_limits::max()) throw std::overflow_error("source broker fill sequence exhausted"); ++broker_fill_event_seq_; - excursion_priced_fill_ = false; - excursion_level_fill_ = false; - excursion_margin_call_ = false; - excursion_margin_prefix_ = false; - excursion_margin_fill_only_ = false; - excursion_trail_offset_ticks_ = std::numeric_limits::quiet_NaN(); - excursion_trail_raw_price_ = std::numeric_limits::quiet_NaN(); if (position_side_ != PositionSide::FLAT) { // ab9714be engine_orders.cpp:531-541: settle_position_after_partial_exit resets to flat when position_qty_ <= kQtyEpsilon or empty bool changed = false; @@ -692,240 +589,10 @@ std::optional source::PineStrategyHost::resolve_anchored_level( NativePrecommitVerdict source::PineStrategyHost::validate_execution_precommit( const NativePrecommitView& view) const { - // ab9714be pine_fills.cpp:5741: the exit-bar path prefix belongs to the - // closing lot's excursion only while a priced fill is being applied. The - // host's own sampler consumes this cache in closed_lot_excursion(). - bool priced = false; - if (view.definition) { - const auto& trigger = view.definition->request.trigger; - priced = priced_opening_trigger(trigger) - || std::holds_alternative(trigger); - } - // L10j: an exit leg carrying priced stop/limit/trailing terms folds its - // pre-fill path extremes too; the magnifier one-price gate below applies - // to it as well (L10h), which the former adapter-side override bypassed. - priced = priced || adapter_.source_priced_exit(view.target.incarnation); - // When an opposite market entry or script close is pending at the bar open, - // the legacy owner models the exit as the market order (closing the trade - // at the open without folding the exit bar's path). - if (view.cursor.point.path_phase == NativePathPhase::Open - && adapter_.has_pending_market_exit(view.cursor.point.interval_index)) { - priced = false; - } - double trail_ticks = std::numeric_limits::quiet_NaN(); - if (const auto off = adapter_.source_trail_offset_ticks(view.target.incarnation)) { - trail_ticks = *off; - } - excursion_priced_fill_ = priced; - excursion_level_fill_ = adapter_.source_post_parent_calc_level_fill( - view.target.incarnation); - excursion_margin_call_ = adapter_.source_margin_exit(view.target.incarnation) - || source::PineExecutionAdapter::source_kernel_liquidation(view.definition); - excursion_trail_offset_ticks_ = trail_ticks; - // ab9714be pine_fills.cpp:5766-5770: the peak a TRAIL fill retraces from is - // taken off the matcher's pre-slip price, which this view still carries; the - // booked facts.fill_price the sampler reads has already been slipped by - // resolve_terms. - excursion_trail_raw_price_ = - (!std::isnan(trail_ticks) && std::isfinite(view.raw_price) - && view.raw_price > 0.0) - ? view.raw_price - : std::numeric_limits::quiet_NaN(); - // A zero-offset trail's matcher touch is the half-tick boundary of the - // rounded price path, not its fill; the owner's peak is the pre-slip fill - // itself, snapped onto the tick grid (ab9714be pine_fills.cpp:5761-5771, - // engine_internal.hpp:187-198). Its open-gap fill is no TRAIL event - // (engine_path_resolve.cpp:620-631) and folds no peak. - if (trail_ticks == 0.0 && view.cursor.point.path_phase == NativePathPhase::Open) { - excursion_trail_offset_ticks_ = std::numeric_limits::quiet_NaN(); - excursion_trail_raw_price_ = std::numeric_limits::quiet_NaN(); - } else if (trail_ticks == 0.0 && std::isfinite(view.resolved_price) - && view.resolved_price > 0.0) { - const double slip = config_.slippage * syminfo_.mintick; - excursion_trail_raw_price_ = compat::pine::snap_trail_level_to_tick_grid( - detail::run_position(*this).signed_units > 0.0 ? view.resolved_price + slip - : view.resolved_price - slip, - syminfo_.mintick); - } - // A fill-through (slipped touch) trail leg books its fill slippage ticks - // past the touch; the owner's peak is still that leg's PRE-slip fill - // (ab9714be pine_fills.cpp:5760-5769 runs before apply_fill_slippage), - // which is the booked price with the slippage step taken back. An - // open-gap fill is not a TRAIL event at all (ab9714be - // engine_path_resolve.cpp:620-631 leaves is_trail false), so it folds - // no peak. - if (!std::isnan(trail_ticks) && view.definition && std::isfinite(view.resolved_price)) { - const auto* touch = std::get_if(&view.definition->request.trigger); - if (touch && touch->fill_through) { - if (view.cursor.point.path_phase == NativePathPhase::Open) { - excursion_trail_offset_ticks_ = std::numeric_limits::quiet_NaN(); - excursion_trail_raw_price_ = std::numeric_limits::quiet_NaN(); - } else { - const double slip = config_.slippage * syminfo_.mintick; - excursion_trail_raw_price_ = detail::run_position(*this).signed_units > 0.0 - ? view.resolved_price + slip : view.resolved_price - slip; - } - } - } - // The margin slice's sampling chronology is a book fact resolved in the - // adapter precommit pass; start clean for every request. - excursion_margin_prefix_ = false; - excursion_margin_fill_only_ = false; precommit_held_units_ = std::abs(detail::run_position(*this).signed_units); return adapter_.validate_precommit(view); } -ClosedLotExcursion source::PineStrategyHost::closed_lot_excursion( - const ClosedLotExcursionFacts& facts) const { - // The kernel records an owner's answer as given (R5 lane F3), so the - // reporting basis is this host's: TradingView's net open-profit basis, - // exactly as the kernel's own excursion row applies it - // (build_close_trade_with_costs) -- the price-point magnitudes converted - // to account currency, the entry fee share taken off the favorable side - // (floored at zero) and added to the adverse side. - // The row's currency is the rate the row converts at (facts.account_fx). - const ClosedLotExcursion owned = owner_lot_excursion(facts); - const double pv = syminfo_.pointvalue; - return ClosedLotExcursion{ - std::max(0.0, owned.favorable * pv * facts.account_fx - - facts.entry_commission), - std::max(0.0, owned.adverse * pv * facts.account_fx - + facts.entry_commission)}; -} - -ClosedLotExcursion source::PineStrategyHost::owner_lot_excursion( - const ClosedLotExcursionFacts& facts) const { - // ab9714be engine_orders.cpp:319 build_close_trade_with_costs, excursion - // half. Everything here is the owner's model: the carried per-lot extremes - // already hold every completed source bar's masked H/L/C walk - // (sample_open_trade_extremes), scaled to the closed slice; the exit fill - // itself always belongs to the trade; a TRAIL fill retrace contributes the - // peak that armed it; and for a priced exit the assumed OHLC path prefix - // the fill sits behind is folded in, honoring the entry-bar masks when the - // lot opened on this same bar. - const double slice = - (facts.lot_qty > 0.0) ? (facts.closed_qty / facts.lot_qty) : 1.0; - double fill_fav = - (facts.is_long ? (facts.fill_price - facts.entry_price) - : (facts.entry_price - facts.fill_price)) - * facts.closed_qty; - // Open-gap scratches book the script open on both legs; a 1-ULP - // entry/exit residual formats as CSV -0.000000 against owner's 0. - if (std::abs(facts.fill_price - facts.entry_price) < 1e-9) { - fill_fav = 0.0; - } - ClosedLotExcursion owned; - const bool same_bar = facts.entry_bar_index == facts.exit_bar_index; - // ab9714be src/source/pine_scheduler.cpp:242,257 samples the bar's H/L/C - // into every OPEN trade (update_per_trade_extremes, step 2) only after the - // resting priced exits of step 1 have closed. A leg this route force-fills - // at its level on its own entry bar therefore keeps the owner's unsampled - // entry seed: no bar of this trade was ever walked by the sampler, so the - // exit fill and the pre-fill path prefix below are the whole excursion. - const bool unsampled_entry_bar = same_bar && excursion_level_fill_; - const double carried_favorable = unsampled_entry_bar ? 0.0 : facts.carried_favorable; - const double carried_adverse = unsampled_entry_bar ? 0.0 : facts.carried_adverse; - owned.favorable = std::max(carried_favorable * slice, fill_fav); - owned.adverse = std::max(carried_adverse * slice, -fill_fav); - // ab9714be pine_fills.cpp:5766-5770: a TRAIL fill retraces exactly the - // trailing offset from the peak that armed it, so that peak is a pre-fill - // favorable excursion no bar-boundary sample ever sees. - if (!std::isnan(excursion_trail_offset_ticks_)) { - const double off = excursion_trail_offset_ticks_ * syminfo_.mintick; - const double basis = std::isnan(excursion_trail_raw_price_) - ? facts.fill_price - : excursion_trail_raw_price_; - const double peak = facts.is_long ? (basis + off) : (basis - off); - const double peak_fav = (facts.is_long ? (peak - facts.entry_price) - : (facts.entry_price - peak)) - * facts.closed_qty; - owned.favorable = std::max(owned.favorable, peak_fav); - } - // A range-end report row is a projection at the terminal close: the owner - // folds no exit-bar path prefix there. A market fill lands on a bar - // boundary the sampler already walked, and under the bar magnifier a - // one-price open bar has no path left to fold. - if (excursion_range_end_projection_) return owned; - if (excursion_margin_call_ && excursion_margin_fill_only_) return owned; - // Both folds below walk this bar's modeled path, so both need the leg - // order the RUN declares (NativeRunSpec::path_order), not the bare - // open-proximity rule (internal::bar_path_uses_high_first), which answers - // AUTO under every declared order. It is asked of the consumer, which is - // the same call - // BacktestEngine::declare_opened_lot_entry_bar_mask derives - // facts.entry_bar_{high,low}_masked on — one resolution for the mask and - // for the arithmetic that completes it, so the two cannot disagree. Under - // AUTO it is the open-proximity rule this read always returned here. - const bool path_high_first = detail::run_consumer(*this).path_high_first(current_bar_); - if (excursion_margin_call_) { - // ab9714be pine_risk.cpp:256-292: a margin-call liquidation at the - // adverse extreme owns the rest of the bar. Which part of the bar it - // owns is the slice's birth chronology: the non-POOC opening trim - // inherits the complete bar, while the POOC/pre-exit prefix routes - // sample only the traversed waypoint prefix. - const Bar sample_bar = margin_call_sample_bar( - current_bar_, facts.fill_price, excursion_margin_prefix_, - path_high_first, syminfo_.mintick, config_.slippage); - const double margin_high = (same_bar && facts.entry_bar_high_masked) - ? facts.entry_price : sample_bar.high; - const double margin_low = (same_bar && facts.entry_bar_low_masked) - ? facts.entry_price : sample_bar.low; - const double fav_px = facts.is_long ? margin_high : margin_low; - const double adv_px = facts.is_long ? margin_low : margin_high; - const double fav = (facts.is_long ? (fav_px - facts.entry_price) - : (facts.entry_price - fav_px)) - * facts.closed_qty; - const double adv = (facts.is_long ? (facts.entry_price - adv_px) - : (adv_px - facts.entry_price)) - * facts.closed_qty; - owned.favorable = std::max(owned.favorable, fav); - owned.adverse = std::max(owned.adverse, adv); - const double closing = (facts.is_long ? (sample_bar.close - facts.entry_price) - : (facts.entry_price - sample_bar.close)) - * facts.closed_qty; - owned.favorable = std::max(owned.favorable, closing); - owned.adverse = std::max(owned.adverse, -closing); - return owned; - } - if (!excursion_priced_fill_) return owned; - // ab9714be pine_scheduler.cpp:99-106 and 548-552: the calc_on_order_fills - // historical dispatch books an O-point fill against a one-price point - // bar, so no path extreme precedes it. - if ((scheduler_.bar_magnifier_enabled() || config_.calc_on_order_fills) - && std::abs(facts.fill_price - current_bar_.open) < 1e-7) { - return owned; - } - const double touch_price = bar_fill_price(facts.fill_price); - double fill_pos = 0.0; - // The fill's position and the extremes it is compared against have to be - // coordinates on ONE walk, so the order resolved above picks the path - // here too (the two-argument form would re-derive it from the override). - if (!internal::first_touch_position(current_bar_, path_high_first, touch_price, - &fill_pos)) - return owned; - const double high_pos = path_high_first ? 1.0 : 2.0; - const double low_pos = path_high_first ? 2.0 : 1.0; - const bool mask_high = same_bar && facts.entry_bar_high_masked; - const bool mask_low = same_bar && facts.entry_bar_low_masked; - if (high_pos < fill_pos && !mask_high) { - const double hi_fav = (facts.is_long - ? (current_bar_.high - facts.entry_price) - : (facts.entry_price - current_bar_.high)) - * facts.closed_qty; - owned.favorable = std::max(owned.favorable, hi_fav); - owned.adverse = std::max(owned.adverse, -hi_fav); - } - if (low_pos < fill_pos && !mask_low) { - const double lo_fav = (facts.is_long - ? (current_bar_.low - facts.entry_price) - : (facts.entry_price - current_bar_.low)) - * facts.closed_qty; - owned.favorable = std::max(owned.favorable, lo_fav); - owned.adverse = std::max(owned.adverse, -lo_fav); - } - return owned; -} - void source::PineStrategyHost::configure_pine_strategy(const PineStrategyConfig& config) { guard_native_mutation("configure_pine_strategy"); config_ = config; @@ -1687,11 +1354,9 @@ void source::PineStrategyHost::scheduler_record_range_end(const Bar& terminal_ba // handed to the producer: the presented clock is never rewritten (R5 // lane B-ADAPTER; E21's rule -- thread the rate, never the clock). const std::int64_t mark_time = equity_curve_.back().time_ms; - excursion_range_end_projection_ = true; const double range_end_pnl = as_native_consumer(execution_consumer()) .append_open_position_report_rows(*this, fill_price, mark_time, bar_index_, account_currency_fx_at(mark_time)); - excursion_range_end_projection_ = false; auto& last = equity_curve_.back(); last.open_profit = 0.0; last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl; diff --git a/tests/fixtures/e19_allin_trim/README.md b/tests/fixtures/e19_allin_trim/README.md new file mode 100644 index 00000000..4c877bad --- /dev/null +++ b/tests/fixtures/e19_allin_trim/README.md @@ -0,0 +1,27 @@ +# All-in opening trim tape (R5 wave H, lane H-THIN, E19) + +Which excursion does TradingView give the small residual an all-in +commissioned opening books and margin-calls again on its own entry bar? +Default 100 % of equity, 0.02 % commission, 2 ticks slippage, margin 100/100; +four scenarios, each flat at its start and closed by `strategy.close_all` +(`hthin-e19-allin-trim/strategy.pine` names them). TradingView books both +entry-bar margin calls (trades #1 and #4) with the fill alone: favorable 0, +adverse = the fill's loss plus the entry commission (#1: +`-(0.04 + 0.0002 * 1812.35) * 1 = -0.40247`). It does not fold the entry +bar's high or low into them. The Pine host's former owner model (ab9714be's +rule, `tests/test_l10y_full_equity_entry_split.cpp` before R5 lane H-THIN) +gave such a residual the complete entry bar; the kernel sampler books the +fill alone, as TradingView does. + +| tape (`lab tv`, ws-report-v1, rangeProof covered) | chart | window | trades | tv_trades.csv sha256 | campaign note | +|---|---|---|---|---|---| +| `hthin-e19-allin-trim` | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2025-05-01 | 6 | `c229f8fd7d1fd6c414db51a032d4c5ac8261bcee35c98989b7d9d168af5eb299` | `tv-tape-hthin-e19-allin-trim-c229f8fd` | + +Replayed on the corpus 15m feed through a `PineStrategyHost` running the same +script ($EV/exec/H-THIN-scratch/probes/probe_allin_trim.cpp), scenario A's +residual (qty 1, 1812.35 -> 1812.31) reads favorable 0 / adverse 0.402470 on +the kernel sampler and 7.467530 / 12.082470 on the retired owner model; the +main lot (#2, 33.727846 / 118.842902) and #3 agree with the tape on both. +Scenario C's short trim is a different trade on both engines (the adapter +slices 0.1696 at 1651.81 where TradingView slices 1 at 1646.05) -- a +pre-existing margin-slice divergence, recorded, not an excursion question. diff --git a/tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/meta.json b/tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/meta.json new file mode 100644 index 00000000..72ed6983 --- /dev/null +++ b/tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-THIN E19 all-in opening trim", + "slug": "hthin-e19-allin-trim", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-25T23:18:34.016Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01" + }, + "pine_sha256": "ec6038f03e89929c26c0e7862f7e24f18575f12d710947e823eafccfe2e60ce9", + "tv_trades_sha256": "c229f8fd7d1fd6c414db51a032d4c5ac8261bcee35c98989b7d9d168af5eb299", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/metrics.json b/tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/metrics.json new file mode 100644 index 00000000..125e09a5 --- /dev/null +++ b/tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hthin-e19-allin-trim", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "ec6038f03e89929c26c0e7862f7e24f18575f12d710947e823eafccfe2e60ce9", + "tvTradesCsvHash": "c229f8fd7d1fd6c414db51a032d4c5ac8261bcee35c98989b7d9d168af5eb299", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75050", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "651a47eeb7759d40cddf51103d9fab9ef1d549e8ac700f01377fdda2243e5a5c", + "decodedReportSha256": "aa05f3fa9e1ac3f78cbc751cc4c5d7ab6457d32a3d9a99b338cbb84984d80aaf", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-25T23:18:34.015Z" +} diff --git a/tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/strategy.pine b/tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/strategy.pine new file mode 100644 index 00000000..10dffd7b --- /dev/null +++ b/tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/strategy.pine @@ -0,0 +1,37 @@ +// R5 lane H-THIN, E19: which excursion does TradingView give the residual an +// all-in commissioned opening books and margin-calls again on its own entry +// bar? Default 100 % of equity, 0.02 % commission, 2 ticks slippage, margin +// 100/100; each scenario is flat at its start and closed by strategy.close_all. +// A 2025-04-04 15:00Z signal: long all-in + a bracket the entry bar never reaches +// B 2025-04-09 13:00Z signal: long all-in, no bracket +// C 2025-04-15 09:00Z signal: short all-in, no bracket +// D 2025-04-22 14:00Z signal: long all-in + a bracket the entry bar never reaches +//@version=6 +strategy("H-THIN E19 all-in opening trim", overlay=true, initial_capital=10000, + default_qty_type=strategy.percent_of_equity, default_qty_value=100, pyramiding=1, + commission_type=strategy.commission.percent, commission_value=0.02, slippage=2, + process_orders_on_close=false, margin_long=100, margin_short=100) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 4, 4, 15, 0) + strategy.entry("LA", strategy.long) + strategy.exit("LAX", "LA", limit=1830.0, stop=1780.0) +if at(2025, 4, 4, 15, 30) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 9, 13, 0) + strategy.entry("LB", strategy.long) +if at(2025, 4, 9, 14, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 15, 9, 0) + strategy.entry("SC", strategy.short) +if at(2025, 4, 15, 10, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 22, 14, 0) + strategy.entry("LD", strategy.long) + strategy.exit("LDX", "LD", limit=close * 1.03, stop=close * 0.97) +if at(2025, 4, 22, 15, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/tv_trades.csv b/tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/tv_trades.csv new file mode 100644 index 00000000..75920ac7 --- /dev/null +++ b/tests/fixtures/e19_allin_trim/hthin-e19-allin-trim/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-04 23:15,Margin call,1812.31,1,1812.35,-0.764932,-0.04,0.724932,0,0.00,-0.40247,-0.02,-2.402064,-0.02,0 +1,Entry long,2025-04-04 23:15,LA,1812.35,1,1812.35,-0.764932,-0.04,0.724932,0,0.00,-0.40247,-0.02,-2.402064,-0.02,0 +2,Exit long,2025-04-04 23:45,Close position order,1791.56,4.5166,8185.66001,-97.1556,-1.19,3.255483981,33.727844,0.41,-118.8429,-1.45,-97.92053,-0.98,2 +2,Entry long,2025-04-04 23:15,LA,1812.35,4.5166,8185.66001,-97.1556,-1.19,3.255483981,33.727844,0.41,-118.8429,-1.45,-97.92053,-0.98,2 +3,Exit long,2025-04-09 22:15,Close position order,1475.97,6.7738,9899.976438,93.96957,0.95,3.979580405,265.72058,2.68,-48.24505,-0.49,-3.9509623,-0.04,4 +3,Entry long,2025-04-09 21:15,LB,1461.51,6.7738,9899.976438,93.96957,0.95,3.979580405,265.72058,2.68,-48.24505,-0.49,-3.9509623,-0.04,4 +4,Exit short,2025-04-15 17:15,Margin call,1646.05,1,1646.01,-0.698412,-0.04,0.658412,0,0.00,-0.369202,-0.02,-6.3189554,-0.06,0 +4,Entry short,2025-04-15 17:15,SC,1646.01,1,1646.01,-0.698412,-0.04,0.658412,0,0.00,-0.369202,-0.02,-6.3189554,-0.06,0 +5,Exit short,2025-04-15 18:15,Close position order,1642.54,5.0716,8347.904316,14.26281,0.17,3.335642036,61.928284,0.74,-30.983429,-0.37,9.613436,0.10,4 +5,Entry short,2025-04-15 17:15,SC,1646.01,5.0716,8347.904316,14.26281,0.17,3.335642036,61.928284,0.74,-30.983429,-0.37,9.613436,0.10,4 +6,Exit long,2025-04-22 23:00,LDX,1699.05,6.0667,10007.506986,296.0566,2.96,4.063026724,298.11813,2.98,-64.36718,-0.64,305.67004,3.06,3 +6,Entry long,2025-04-22 22:15,LD,1649.58,6.0667,10007.506986,296.0566,2.96,4.063026724,298.11813,2.98,-64.36718,-0.64,305.67004,3.06,3 \ No newline at end of file diff --git a/tests/fixtures/pyramiding_count/README.md b/tests/fixtures/pyramiding_count/README.md index 952a4453..da24af2d 100644 --- a/tests/fixtures/pyramiding_count/README.md +++ b/tests/fixtures/pyramiding_count/README.md @@ -28,7 +28,7 @@ NYSE:F 15), byte-identical: `strategy.pine`, `tv_trades.csv` (UTC+8), | P6 | three market entries on one flat bar | 2 fill | 3 fill | = | | P8 | `exit(qty)` from B stops out lot A (FIFO), third id | fills | refuses | = | | P9 | `exit(qty)` from A stops out lot A, third id | fills | = | = | -| P10 | `exit` from B, default quantity, lots A+B | closes 100 (A) | closes 200 | = (Reduce 100) | +| P10 | `exit` from B, default quantity, lots A+B | closes 100 (A) | closes 200 (100 since R5 lane H-THIN) | = (Reduce 100) | | D1 | one lot; same bar: far limit, then market add | add fills | refuses | = | | D2 | flat; same bar: far limit, two markets | both fill | = | = | | D3 | at the cap; same bar: `close("A")` then `entry("C")` | C fills | refuses | = | @@ -43,7 +43,19 @@ that passed that check is never checked again when it fills (P4b). The kernel's `max_open_lots` (surviving + new lots at every match) agrees on 14 of 15, all but P4b; the adapter's command-time count on 8 of 15. P10 is not a pyramiding question (the quantity a default `strategy.exit(from_entry)` closes -under FIFO) and is pinned as a separate recorded divergence. +under FIFO); it was pinned as a separate recorded divergence and R5 lane +H-THIN fixed it (the exit reserves its entry's own quantity; 9 of 15 now). + +R5 lane H-THIN measured the lowering the fifteen suggest -- `project()` +declaring `max_open_lots = max(1, pyramiding)` -- on the corpus: with the +adapter's count kept beside it, 4 probes move, each away from its TradingView +tape (`bracket-tp-sl-oca-reduce-isolate-01` 2240 -> 2150 trades matched, +`composite-bracket-cap-range-pending-stop-01` 1178 -> 1170, +`order-dual-stop-both-touch-priority-01` 755 -> 738, +`order-opposite-entry-close-same-pass-01` 828 -> 824); without the count, 11 +move. TradingView's first-eligible-point rule (P4b) decides those corpus +trades, and the kernel's cap, checked at every match, has no such mode, so +the count stays until the kernel does. `bars.inc`: per scenario the 12 bars from its first signal bar, from the registry feed `lab bars NYSE:F 15`, evidence sha256 diff --git a/tests/fixtures/pyramiding_count/expectations.inc b/tests/fixtures/pyramiding_count/expectations.inc index 80433625..ed3e13da 100644 --- a/tests/fixtures/pyramiding_count/expectations.inc +++ b/tests/fixtures/pyramiding_count/expectations.inc @@ -42,10 +42,14 @@ const Scenario kScenarios[] = { true, true, "A 100 @1:10.77->4:10.75; B 100 @2:10.77->9:10.78; C 100 @7:10.77->9:10.78; ", "A 100 @1:10.77->4:10.75; B 100 @2:10.77->9:10.78; C 100 @7:10.77->9:10.78; "}, + // expectation corrected (R5 lane H-THIN, M13's P10): a default + // strategy.exit(from_entry) under FIFO reserves its entry's own quantity, + // so the stop closes 100 (lot A, the oldest) as TradingView does; it + // closed the whole book (A and B at 11.38) before. {"P10", "hm-orders-m13-f-pyramiding", {{0, Op::Entry, "A"}, {1, Op::Entry, "B"}, {2, Op::StopExit, "XB", kNaN, 11.38, "B"}, {6, Op::CloseAll, "close_all"}}, - false, true, - "A 100 @1:11.34->4:11.38; B 100 @2:11.37->4:11.38; ", + true, true, + "A 100 @1:11.34->4:11.38; B 100 @2:11.37->7:11.34; ", "A 100 @1:11.34->4:11.38; B 100 @2:11.37->7:11.34; "}, {"D1", "hm-orders-m13-f-pyramiding-rule", {{0, Op::Entry, "A"}, {2, Op::LimitEntry, "L2", kNaN, 10.27}, {2, Op::Entry, "M"}, {5, Op::Cancel, "L2"}, {6, Op::CloseAll, "close_all"}}, diff --git a/tests/l11a_host_excursion_fixture.hpp b/tests/l11a_host_excursion_fixture.hpp index 3027d4d1..e8785a1e 100644 --- a/tests/l11a_host_excursion_fixture.hpp +++ b/tests/l11a_host_excursion_fixture.hpp @@ -38,7 +38,7 @@ inline void expect(const char* tag, const Trade& t, bool is_long, double entry_p CHECK(t.is_long == is_long); CHECK(near(t.entry_price, entry_px)); CHECK(near(t.exit_price, exit_px)); - CHECK(near(t.max_runup, fav) /* exact: the host owns excursion */); + CHECK(near(t.max_runup, fav) /* exact */); CHECK(near(t.max_drawdown, adv)); } diff --git a/tests/test_adapter_live_state_equivalence_data.hpp b/tests/test_adapter_live_state_equivalence_data.hpp index 9096d146..9a7c350b 100644 --- a/tests/test_adapter_live_state_equivalence_data.hpp +++ b/tests/test_adapter_live_state_equivalence_data.hpp @@ -63,113 +63,214 @@ // chains seed 314189: 10134000431367652603 -> 5640170146031748940 (107 trades, 526 commands, 444 rows placed -> 107 trades, 526 commands, 454 rows placed) // chains seed 1780395: 632278533107602324 -> 11993708214650334332 (82 trades, 480 commands, 426 rows placed -> 82 trades, 480 commands, 436 rows placed) // chains seed 3246601: 17544117140908072910 -> 17828078233634427072 (98 trades, 485 commands, 468 rows placed -> 98 trades, 485 commands, 466 rows placed) +// R5 lane H-THIN moves 96 of 108 digests (INT26: the behaviour half of its hash +// step ab297364, harvested on the integrated tree with this TU's own switch; +// the transcript holds no hash value): P10 moves the trades of configurations +// with a FIFO exit from one of several entries, E19 the excursion fields every +// closed trade folds: +// reversals seed 104729: 17062111550189209216 -> 14377401231487041547 (an earlier pick had moved it too) (reversals seed 104729, 131 trades, 397 commands, 431 rows placed -> reversals seed 104729, 131 trades, 397 commands, 435 rows placed) +// reversals seed 209458: 3937583110602879389 -> 8277084373591058696 (reversals seed 209458, 126 trades, 382 commands, 409 rows placed -> reversals seed 209458, 123 trades, 379 commands, 419 rows placed) +// reversals seed 314187: 9508064597864072295 -> 2936193272230691901 (an earlier pick had moved it too) (reversals seed 314187, 208 trades, 684 commands, 652 rows placed -> reversals seed 314187, 168 trades, 628 commands, 582 rows placed) +// reversals seed 418916: 14691229822702022375 -> 6549991687676950761 (reversals seed 418916, 105 trades, 378 commands, 361 rows placed -> reversals seed 418916, 107 trades, 381 commands, 383 rows placed) +// reversals seed 523645: 201369272286234124 -> 9953188267307049090 (reversals seed 523645, 104 trades, 417 commands, 357 rows placed -> reversals seed 523645, 104 trades, 417 commands, 379 rows placed) +// reversals seed 628374: 12651613190033426865 -> 16185846816223020088 (an earlier pick had moved it too) (reversals seed 628374, 138 trades, 410 commands, 399 rows placed -> reversals seed 628374, 126 trades, 412 commands, 399 rows placed) +// reversals seed 733103: 10868119957001212051 -> 4216355035770739779 (reversals seed 733103, 139 trades, 386 commands, 404 rows placed -> reversals seed 733103, 138 trades, 387 commands, 413 rows placed) +// reversals seed 837832: 15494092140752158088 -> 6363178433100572906 (reversals seed 837832, 91 trades, 401 commands, 332 rows placed -> reversals seed 837832, 91 trades, 401 commands, 366 rows placed) +// reversals seed 942561: 5702736951683229543 -> 1473820887994227184 (reversals seed 942561, 98 trades, 408 commands, 356 rows placed -> reversals seed 942561, 98 trades, 408 commands, 366 rows placed) +// reversals seed 1047290: 10200415260680638236 -> 9193554944507238062 (reversals seed 1047290, 134 trades, 402 commands, 393 rows placed -> reversals seed 1047290, 133 trades, 395 commands, 392 rows placed) +// reversals seed 1152019: 4811666956682998059 -> 6981925030785360377 (an earlier pick had moved it too) (reversals seed 1152019, 151 trades, 397 commands, 409 rows placed -> reversals seed 1152019, 151 trades, 397 commands, 421 rows placed) +// reversals seed 1256748: 9826242374101611623 -> 5305403161869337627 (reversals seed 1256748, 118 trades, 402 commands, 362 rows placed -> reversals seed 1256748, 118 trades, 402 commands, 374 rows placed) +// reversals seed 1361477: 4883135135199369800 -> 9475766261620659540 (reversals seed 1361477, 105 trades, 413 commands, 349 rows placed -> reversals seed 1361477, 106 trades, 411 commands, 389 rows placed) +// reversals seed 1466206: 6980972530369447172 -> 4927715982682604365 (reversals seed 1466206, 151 trades, 372 commands, 393 rows placed -> reversals seed 1466206, 151 trades, 372 commands, 413 rows placed) +// reversals seed 1570935: 17000758005854428354 -> 12202956723937736421 (an earlier pick had moved it too) (reversals seed 1570935, 92 trades, 362 commands, 329 rows placed -> reversals seed 1570935, 69 trades, 344 commands, 273 rows placed) +// reversals seed 1675664: 15023244533711012002 -> 1308277235911883001 (an earlier pick had moved it too) (reversals seed 1675664, 124 trades, 398 commands, 405 rows placed -> reversals seed 1675664, 118 trades, 389 commands, 409 rows placed) +// reversals seed 1780393: 3425020382181293193 -> 15749505417408196912 (an earlier pick had moved it too) (reversals seed 1780393, 196 trades, 702 commands, 629 rows placed -> reversals seed 1780393, 196 trades, 702 commands, 665 rows placed) +// reversals seed 1885122: 4513324373166417315 -> 16795999522017645296 (reversals seed 1885122, 113 trades, 393 commands, 388 rows placed -> reversals seed 1885122, 127 trades, 396 commands, 391 rows placed) +// reversals seed 1989851: 18393465585117555058 -> 6779215636140632786 (reversals seed 1989851, 180 trades, 430 commands, 448 rows placed -> reversals seed 1989851, 173 trades, 429 commands, 449 rows placed) +// reversals seed 2094580: 6120235323624511436 -> 15115396572137765888 (reversals seed 2094580, 92 trades, 384 commands, 340 rows placed -> reversals seed 2094580, 92 trades, 384 commands, 350 rows placed) +// reversals seed 2199309: 9639971099840596709 -> 1053394249578594394 (an earlier pick had moved it too) (reversals seed 2199309, 127 trades, 398 commands, 375 rows placed -> reversals seed 2199309, 124 trades, 398 commands, 378 rows placed) +// reversals seed 2304038: 7979846979225294549 -> 314047600622510661 (reversals seed 2304038, 149 trades, 412 commands, 418 rows placed -> reversals seed 2304038, 149 trades, 412 commands, 440 rows placed) +// reversals seed 2408767: 16092912242308174488 -> 3946903920860189031 (reversals seed 2408767, 139 trades, 415 commands, 404 rows placed -> reversals seed 2408767, 138 trades, 418 commands, 417 rows placed) +// reversals seed 2513496: 1517737495207721277 -> 8490080330531136220 (reversals seed 2513496, 110 trades, 390 commands, 394 rows placed -> reversals seed 2513496, 103 trades, 392 commands, 393 rows placed) +// reversals seed 2618225: 1610718425964291349 -> 12238138393722192163 (reversals seed 2618225, 105 trades, 411 commands, 371 rows placed -> reversals seed 2618225, 105 trades, 411 commands, 399 rows placed) +// reversals seed 2722954: 4868757197565921069 -> 2863182994793154059 (an earlier pick had moved it too) (reversals seed 2722954, 150 trades, 390 commands, 428 rows placed -> reversals seed 2722954, 152 trades, 394 commands, 448 rows placed) +// reversals seed 2827683: 13294227884245356190 -> 13953002312094633174 (an earlier pick had moved it too) (reversals seed 2827683, 48 trades, 347 commands, 215 rows placed -> reversals seed 2827683, 48 trades, 347 commands, 229 rows placed) +// reversals seed 2932412: 8135938306046315211 -> 12936186547753035500 (reversals seed 2932412, 96 trades, 360 commands, 318 rows placed -> reversals seed 2932412, 96 trades, 360 commands, 330 rows placed) +// reversals seed 3037141: 9738428349677663372 -> 11138119210196651599 (reversals seed 3037141, 114 trades, 405 commands, 377 rows placed -> reversals seed 3037141, 118 trades, 408 commands, 398 rows placed) +// reversals seed 3141870: 2670083654842814989 -> 11186738496014205060 (reversals seed 3141870, 66 trades, 332 commands, 248 rows placed -> reversals seed 3141870, 116 trades, 402 commands, 404 rows placed) +// reversals seed 3246599: 14261337452491704800 -> 7365380738509267810 (an earlier pick had moved it too) (reversals seed 3246599, 156 trades, 469 commands, 484 rows placed -> reversals seed 3246599, 147 trades, 455 commands, 507 rows placed) +// reversals seed 3351328: 14583498249706098574 -> 8856317689282633932 (reversals seed 3351328, 108 trades, 414 commands, 345 rows placed -> reversals seed 3351328, 108 trades, 414 commands, 363 rows placed) +// reversals seed 3456057: 7833467542887485857 -> 1680641833169632427 (an earlier pick had moved it too) (reversals seed 3456057, 87 trades, 373 commands, 334 rows placed -> reversals seed 3456057, 87 trades, 373 commands, 350 rows placed) +// reversals seed 3560786: 14092552094636276530 -> 17785508903938760497 (reversals seed 3560786, 147 trades, 413 commands, 444 rows placed -> reversals seed 3560786, 147 trades, 397 commands, 435 rows placed) +// reversals seed 3665515: 4203447471686931733 -> 17135221856069256209 (reversals seed 3665515, 133 trades, 394 commands, 397 rows placed -> reversals seed 3665515, 132 trades, 392 commands, 402 rows placed) +// reversals seed 3770244: 16581004868667265691 -> 16348052548195448490 (an earlier pick had moved it too) (reversals seed 3770244, 53 trades, 339 commands, 245 rows placed -> reversals seed 3770244, 53 trades, 339 commands, 267 rows placed) +// brackets seed 104730: 5125743962394059998 -> 14413849309182271769 +// brackets seed 209459: 3732927941814356572 -> 17836979205695001440 (brackets seed 209459, 77 trades, 487 commands, 551 rows placed -> brackets seed 209459, 76 trades, 487 commands, 569 rows placed) +// brackets seed 314188: 11664346520909115979 -> 6185985077652624317 (an earlier pick had moved it too) (brackets seed 314188, 145 trades, 850 commands, 981 rows placed -> brackets seed 314188, 144 trades, 818 commands, 919 rows placed) +// brackets seed 523646: 12564349532258145101 -> 10672250305191215856 +// brackets seed 628375: 16592088247586231707 -> 1855891803821441106 (brackets seed 628375, 100 trades, 536 commands, 662 rows placed -> brackets seed 628375, 101 trades, 524 commands, 724 rows placed) +// brackets seed 733104: 9284251268869981574 -> 12279391351944236297 (brackets seed 733104, 91 trades, 534 commands, 668 rows placed -> brackets seed 733104, 91 trades, 520 commands, 628 rows placed) +// brackets seed 942562: 15132040098433212256 -> 7040999793872498538 +// brackets seed 1047291: 9624518409170657431 -> 14314528608775687405 (brackets seed 1047291, 86 trades, 468 commands, 526 rows placed -> brackets seed 1047291, 85 trades, 468 commands, 547 rows placed) +// brackets seed 1152020: 15597660030609647223 -> 13142231282207717561 (brackets seed 1152020, 80 trades, 487 commands, 637 rows placed -> brackets seed 1152020, 76 trades, 478 commands, 687 rows placed) +// brackets seed 1466207: 6438015927310529334 -> 16303049545172805242 (brackets seed 1466207, 72 trades, 568 commands, 577 rows placed -> brackets seed 1466207, 70 trades, 568 commands, 669 rows placed) +// brackets seed 1570936: 17986613720295818227 -> 8808473759868459315 (brackets seed 1570936, 69 trades, 475 commands, 535 rows placed -> brackets seed 1570936, 69 trades, 475 commands, 647 rows placed) +// brackets seed 1780394: 1550055039614354387 -> 5435028051816123314 (an earlier pick had moved it too) +// brackets seed 1885123: 2015779222221407240 -> 355520822114888679 (brackets seed 1885123, 90 trades, 542 commands, 587 rows placed -> brackets seed 1885123, 89 trades, 542 commands, 583 rows placed) +// brackets seed 1989852: 1872777722679096721 -> 5402310364412118994 (brackets seed 1989852, 88 trades, 514 commands, 572 rows placed -> brackets seed 1989852, 89 trades, 505 commands, 639 rows placed) +// brackets seed 2304039: 1455827488198344686 -> 15236878832940726852 (brackets seed 2304039, 92 trades, 524 commands, 594 rows placed -> brackets seed 2304039, 94 trades, 577 commands, 623 rows placed) +// brackets seed 2408768: 11979119011511214626 -> 4629113575017480307 (brackets seed 2408768, 94 trades, 559 commands, 694 rows placed -> brackets seed 2408768, 78 trades, 528 commands, 676 rows placed) +// brackets seed 2618226: 10830766203810780391 -> 14075994758817023751 +// brackets seed 2722955: 6172869291970139916 -> 4051940090401488259 (brackets seed 2722955, 85 trades, 503 commands, 661 rows placed -> brackets seed 2722955, 97 trades, 510 commands, 704 rows placed) +// brackets seed 2827684: 9104251795068008494 -> 11315777065199433571 (brackets seed 2827684, 74 trades, 506 commands, 570 rows placed -> brackets seed 2827684, 93 trades, 530 commands, 631 rows placed) +// brackets seed 3037142: 6708847418396898846 -> 9921951745340606853 +// brackets seed 3141871: 7906131650209684341 -> 5531345307764558073 (brackets seed 3141871, 101 trades, 537 commands, 653 rows placed -> brackets seed 3141871, 101 trades, 537 commands, 665 rows placed) +// brackets seed 3246600: 2136070886124345893 -> 13285723008256739950 (an earlier pick had moved it too) (brackets seed 3246600, 115 trades, 657 commands, 847 rows placed -> brackets seed 3246600, 115 trades, 657 commands, 863 rows placed) +// brackets seed 3560787: 6178143291077411923 -> 5589169095865416088 (brackets seed 3560787, 107 trades, 521 commands, 634 rows placed -> brackets seed 3560787, 101 trades, 500 commands, 583 rows placed) +// brackets seed 3665516: 11302959202215131177 -> 3100862192505875197 (brackets seed 3665516, 86 trades, 536 commands, 566 rows placed -> brackets seed 3665516, 92 trades, 571 commands, 626 rows placed) +// chains seed 104731: 11244999202114714137 -> 11442749230602752319 (bisected by pick: H-THIN's source on K-OCA-KEEP's tip gives the lane's own value, on PAR-ORDERS' E14 commit a6e6a005 this one -- P10's path meets E14's pending-entry trail) (chains seed 104731, 59 trades, 301 commands, 286 rows placed -> chains seed 104731, 67 trades, 318 commands, 355 rows placed) +// chains seed 209460: 11413128619516782387 -> 973054536545442904 (an earlier pick had moved it too) (chains seed 209460, 85 trades, 390 commands, 386 rows placed -> chains seed 209460, 66 trades, 384 commands, 419 rows placed) +// chains seed 314189: 5640170146031748940 -> 3920721464232107171 (an earlier pick had moved it too) (chains seed 314189, 107 trades, 526 commands, 454 rows placed -> chains seed 314189, 62 trades, 465 commands, 490 rows placed) +// chains seed 418918: 3121930092179333450 -> 13576086977686921552 (chains seed 418918, 45 trades, 312 commands, 299 rows placed -> chains seed 418918, 44 trades, 293 commands, 349 rows placed) +// chains seed 523647: 165784926829741990 -> 13312605232919440966 (bisected by pick: H-THIN's source on K-OCA-KEEP's tip gives the lane's own value, on PAR-ORDERS' E14 commit a6e6a005 this one -- P10's path meets E14's pending-entry trail) (chains seed 523647, 61 trades, 322 commands, 329 rows placed -> chains seed 523647, 47 trades, 314 commands, 327 rows placed) +// chains seed 628376: 10122741367483366791 -> 6547333340447752844 (an earlier pick had moved it too) (chains seed 628376, 90 trades, 358 commands, 415 rows placed -> chains seed 628376, 73 trades, 342 commands, 367 rows placed) +// chains seed 733105: 13574785125744972298 -> 3389962382455065848 (an earlier pick had moved it too) (chains seed 733105, 88 trades, 351 commands, 318 rows placed -> chains seed 733105, 65 trades, 369 commands, 421 rows placed) +// chains seed 837834: 10108332716913767868 -> 10071629273929733187 (chains seed 837834, 64 trades, 359 commands, 358 rows placed -> chains seed 837834, 60 trades, 368 commands, 418 rows placed) +// chains seed 942563: 12836024126514275333 -> 15248982654499518174 (an earlier pick had moved it too) (chains seed 942563, 59 trades, 341 commands, 322 rows placed -> chains seed 942563, 56 trades, 343 commands, 361 rows placed) +// chains seed 1047292: 16444976724508807594 -> 6473049927299403237 (an earlier pick had moved it too) (chains seed 1047292, 64 trades, 343 commands, 341 rows placed -> chains seed 1047292, 34 trades, 363 commands, 400 rows placed) +// chains seed 1152021: 6715343122935757718 -> 5657155561465537738 (an earlier pick had moved it too) (chains seed 1152021, 70 trades, 342 commands, 384 rows placed -> chains seed 1152021, 64 trades, 338 commands, 409 rows placed) +// chains seed 1256750: 12725802557554099964 -> 140926706188109974 (chains seed 1256750, 65 trades, 319 commands, 302 rows placed -> chains seed 1256750, 64 trades, 314 commands, 315 rows placed) +// chains seed 1361479: 1798455623603086886 -> 1884320088982623449 (chains seed 1361479, 64 trades, 312 commands, 323 rows placed -> chains seed 1361479, 65 trades, 309 commands, 326 rows placed) +// chains seed 1466208: 9973593188851025006 -> 17488515754754645248 (an earlier pick had moved it too) (chains seed 1466208, 73 trades, 382 commands, 338 rows placed -> chains seed 1466208, 78 trades, 390 commands, 407 rows placed) +// chains seed 1570937: 16038855060654239956 -> 8944738996467660919 (an earlier pick had moved it too) (chains seed 1570937, 90 trades, 366 commands, 370 rows placed -> chains seed 1570937, 68 trades, 353 commands, 354 rows placed) +// chains seed 1675666: 4161494051921551918 -> 3579325126147100396 (an earlier pick had moved it too) (chains seed 1675666, 64 trades, 321 commands, 339 rows placed -> chains seed 1675666, 64 trades, 314 commands, 355 rows placed) +// chains seed 1780395: 11993708214650334332 -> 17646633536556052253 (an earlier pick had moved it too) (chains seed 1780395, 82 trades, 480 commands, 436 rows placed -> chains seed 1780395, 67 trades, 438 commands, 465 rows placed) +// chains seed 1885124: 8033768331525115464 -> 7799970005878023626 (chains seed 1885124, 66 trades, 360 commands, 304 rows placed -> chains seed 1885124, 62 trades, 362 commands, 378 rows placed) +// chains seed 1989853: 8033691744657508662 -> 3042300852731654308 (chains seed 1989853, 73 trades, 353 commands, 373 rows placed -> chains seed 1989853, 75 trades, 350 commands, 372 rows placed) +// chains seed 2094582: 10675979410645833515 -> 9104693962049720988 (an earlier pick had moved it too) (chains seed 2094582, 75 trades, 321 commands, 355 rows placed -> chains seed 2094582, 37 trades, 310 commands, 353 rows placed) +// chains seed 2199311: 13103985213974940416 -> 3043920897357867824 (an earlier pick had moved it too) (chains seed 2199311, 54 trades, 326 commands, 285 rows placed -> chains seed 2199311, 56 trades, 321 commands, 327 rows placed) +// chains seed 2304040: 9463870180619581974 -> 17550770482504522165 (an earlier pick had moved it too) (chains seed 2304040, 81 trades, 342 commands, 353 rows placed -> chains seed 2304040, 61 trades, 330 commands, 349 rows placed) +// chains seed 2408769: 1057624106564667840 -> 10315907993814478663 (chains seed 2408769, 67 trades, 347 commands, 374 rows placed -> chains seed 2408769, 62 trades, 349 commands, 378 rows placed) +// chains seed 2513498: 1646636678506043505 -> 11887874226824834835 (an earlier pick had moved it too) (chains seed 2513498, 66 trades, 336 commands, 332 rows placed -> chains seed 2513498, 50 trades, 344 commands, 365 rows placed) +// chains seed 2618227: 15055331467095475975 -> 8569167112979511745 (an earlier pick had moved it too) (chains seed 2618227, 65 trades, 336 commands, 331 rows placed -> chains seed 2618227, 51 trades, 339 commands, 386 rows placed) +// chains seed 2722956: 18266136563684836716 -> 1052992062651979911 (an earlier pick had moved it too) (chains seed 2722956, 62 trades, 345 commands, 344 rows placed -> chains seed 2722956, 59 trades, 355 commands, 422 rows placed) +// chains seed 2827685: 16845310385882942743 -> 18246893670312018403 (chains seed 2827685, 57 trades, 337 commands, 325 rows placed -> chains seed 2827685, 54 trades, 342 commands, 370 rows placed) +// chains seed 2932414: 6451466112212038981 -> 16270472390219306311 (an earlier pick had moved it too) (chains seed 2932414, 65 trades, 350 commands, 330 rows placed -> chains seed 2932414, 62 trades, 336 commands, 373 rows placed) +// chains seed 3037143: 6296605148496367564 -> 14669408219364885155 (an earlier pick had moved it too) (chains seed 3037143, 81 trades, 324 commands, 328 rows placed -> chains seed 3037143, 61 trades, 344 commands, 412 rows placed) +// chains seed 3141872: 18120582394174365029 -> 4441359501225912121 (chains seed 3141872, 82 trades, 355 commands, 349 rows placed -> chains seed 3141872, 59 trades, 364 commands, 395 rows placed) +// chains seed 3246601: 17828078233634427072 -> 4493812043353694436 (an earlier pick had moved it too) (chains seed 3246601, 98 trades, 485 commands, 466 rows placed -> chains seed 3246601, 73 trades, 444 commands, 515 rows placed) +// chains seed 3351330: 9531902743311875506 -> 10753585802392999956 (an earlier pick had moved it too) (chains seed 3351330, 60 trades, 311 commands, 310 rows placed -> chains seed 3351330, 45 trades, 301 commands, 354 rows placed) +// chains seed 3456059: 1817828500136052373 -> 9038807849693065880 (an earlier pick had moved it too) (chains seed 3456059, 59 trades, 292 commands, 289 rows placed -> chains seed 3456059, 43 trades, 310 commands, 298 rows placed) +// chains seed 3560788: 5397962162622300398 -> 2170094277734349720 (an earlier pick had moved it too) (chains seed 3560788, 73 trades, 359 commands, 337 rows placed -> chains seed 3560788, 60 trades, 363 commands, 422 rows placed) +// chains seed 3665517: 3084783482698433102 -> 11220893490402964604 (chains seed 3665517, 70 trades, 347 commands, 326 rows placed -> chains seed 3665517, 70 trades, 347 commands, 380 rows placed) +// chains seed 3770246: 15996277680102595200 -> 1535576416135278226 (chains seed 3770246, 76 trades, 362 commands, 298 rows placed -> chains seed 3770246, 62 trades, 346 commands, 372 rows placed) constexpr std::uint64_t kTranscriptDigests[] = { - 17062111550189209216ull, // reversals seed 104729, 131 trades, 397 commands, 431 rows placed - 3937583110602879389ull, // reversals seed 209458, 126 trades, 382 commands, 409 rows placed - 9508064597864072295ull, // reversals seed 314187, 208 trades, 684 commands, 652 rows placed - 14691229822702022375ull, // reversals seed 418916, 105 trades, 378 commands, 361 rows placed - 201369272286234124ull, // reversals seed 523645, 104 trades, 417 commands, 357 rows placed - 12651613190033426865ull, // reversals seed 628374, 138 trades, 410 commands, 399 rows placed - 10868119957001212051ull, // reversals seed 733103, 139 trades, 386 commands, 404 rows placed - 15494092140752158088ull, // reversals seed 837832, 91 trades, 401 commands, 332 rows placed - 5702736951683229543ull, // reversals seed 942561, 98 trades, 408 commands, 356 rows placed - 10200415260680638236ull, // reversals seed 1047290, 134 trades, 402 commands, 393 rows placed - 4811666956682998059ull, // reversals seed 1152019, 151 trades, 397 commands, 409 rows placed - 9826242374101611623ull, // reversals seed 1256748, 118 trades, 402 commands, 362 rows placed - 4883135135199369800ull, // reversals seed 1361477, 105 trades, 413 commands, 349 rows placed - 6980972530369447172ull, // reversals seed 1466206, 151 trades, 372 commands, 393 rows placed - 17000758005854428354ull, // reversals seed 1570935, 92 trades, 362 commands, 329 rows placed - 15023244533711012002ull, // reversals seed 1675664, 124 trades, 398 commands, 405 rows placed - 3425020382181293193ull, // reversals seed 1780393, 196 trades, 702 commands, 629 rows placed - 4513324373166417315ull, // reversals seed 1885122, 113 trades, 393 commands, 388 rows placed - 18393465585117555058ull, // reversals seed 1989851, 180 trades, 430 commands, 448 rows placed - 6120235323624511436ull, // reversals seed 2094580, 92 trades, 384 commands, 340 rows placed - 9639971099840596709ull, // reversals seed 2199309, 127 trades, 398 commands, 375 rows placed - 7979846979225294549ull, // reversals seed 2304038, 149 trades, 412 commands, 418 rows placed - 16092912242308174488ull, // reversals seed 2408767, 139 trades, 415 commands, 404 rows placed - 1517737495207721277ull, // reversals seed 2513496, 110 trades, 390 commands, 394 rows placed - 1610718425964291349ull, // reversals seed 2618225, 105 trades, 411 commands, 371 rows placed - 4868757197565921069ull, // reversals seed 2722954, 150 trades, 390 commands, 428 rows placed - 13294227884245356190ull, // reversals seed 2827683, 48 trades, 347 commands, 215 rows placed - 8135938306046315211ull, // reversals seed 2932412, 96 trades, 360 commands, 318 rows placed - 9738428349677663372ull, // reversals seed 3037141, 114 trades, 405 commands, 377 rows placed - 2670083654842814989ull, // reversals seed 3141870, 66 trades, 332 commands, 248 rows placed - 14261337452491704800ull, // reversals seed 3246599, 156 trades, 469 commands, 484 rows placed - 14583498249706098574ull, // reversals seed 3351328, 108 trades, 414 commands, 345 rows placed - 7833467542887485857ull, // reversals seed 3456057, 87 trades, 373 commands, 334 rows placed - 14092552094636276530ull, // reversals seed 3560786, 147 trades, 413 commands, 444 rows placed - 4203447471686931733ull, // reversals seed 3665515, 133 trades, 394 commands, 397 rows placed - 16581004868667265691ull, // reversals seed 3770244, 53 trades, 339 commands, 245 rows placed - 5125743962394059998ull, // brackets seed 104730, 82 trades, 521 commands, 568 rows placed - 3732927941814356572ull, // brackets seed 209459, 77 trades, 487 commands, 551 rows placed - 11664346520909115979ull, // brackets seed 314188, 145 trades, 850 commands, 981 rows placed + 14377401231487041547ull, // reversals seed 104729, 131 trades, 397 commands, 435 rows placed + 8277084373591058696ull, // reversals seed 209458, 123 trades, 379 commands, 419 rows placed + 2936193272230691901ull, // reversals seed 314187, 168 trades, 628 commands, 582 rows placed + 6549991687676950761ull, // reversals seed 418916, 107 trades, 381 commands, 383 rows placed + 9953188267307049090ull, // reversals seed 523645, 104 trades, 417 commands, 379 rows placed + 16185846816223020088ull, // reversals seed 628374, 126 trades, 412 commands, 399 rows placed + 4216355035770739779ull, // reversals seed 733103, 138 trades, 387 commands, 413 rows placed + 6363178433100572906ull, // reversals seed 837832, 91 trades, 401 commands, 366 rows placed + 1473820887994227184ull, // reversals seed 942561, 98 trades, 408 commands, 366 rows placed + 9193554944507238062ull, // reversals seed 1047290, 133 trades, 395 commands, 392 rows placed + 6981925030785360377ull, // reversals seed 1152019, 151 trades, 397 commands, 421 rows placed + 5305403161869337627ull, // reversals seed 1256748, 118 trades, 402 commands, 374 rows placed + 9475766261620659540ull, // reversals seed 1361477, 106 trades, 411 commands, 389 rows placed + 4927715982682604365ull, // reversals seed 1466206, 151 trades, 372 commands, 413 rows placed + 12202956723937736421ull, // reversals seed 1570935, 69 trades, 344 commands, 273 rows placed + 1308277235911883001ull, // reversals seed 1675664, 118 trades, 389 commands, 409 rows placed + 15749505417408196912ull, // reversals seed 1780393, 196 trades, 702 commands, 665 rows placed + 16795999522017645296ull, // reversals seed 1885122, 127 trades, 396 commands, 391 rows placed + 6779215636140632786ull, // reversals seed 1989851, 173 trades, 429 commands, 449 rows placed + 15115396572137765888ull, // reversals seed 2094580, 92 trades, 384 commands, 350 rows placed + 1053394249578594394ull, // reversals seed 2199309, 124 trades, 398 commands, 378 rows placed + 314047600622510661ull, // reversals seed 2304038, 149 trades, 412 commands, 440 rows placed + 3946903920860189031ull, // reversals seed 2408767, 138 trades, 418 commands, 417 rows placed + 8490080330531136220ull, // reversals seed 2513496, 103 trades, 392 commands, 393 rows placed + 12238138393722192163ull, // reversals seed 2618225, 105 trades, 411 commands, 399 rows placed + 2863182994793154059ull, // reversals seed 2722954, 152 trades, 394 commands, 448 rows placed + 13953002312094633174ull, // reversals seed 2827683, 48 trades, 347 commands, 229 rows placed + 12936186547753035500ull, // reversals seed 2932412, 96 trades, 360 commands, 330 rows placed + 11138119210196651599ull, // reversals seed 3037141, 118 trades, 408 commands, 398 rows placed + 11186738496014205060ull, // reversals seed 3141870, 116 trades, 402 commands, 404 rows placed + 7365380738509267810ull, // reversals seed 3246599, 147 trades, 455 commands, 507 rows placed + 8856317689282633932ull, // reversals seed 3351328, 108 trades, 414 commands, 363 rows placed + 1680641833169632427ull, // reversals seed 3456057, 87 trades, 373 commands, 350 rows placed + 17785508903938760497ull, // reversals seed 3560786, 147 trades, 397 commands, 435 rows placed + 17135221856069256209ull, // reversals seed 3665515, 132 trades, 392 commands, 402 rows placed + 16348052548195448490ull, // reversals seed 3770244, 53 trades, 339 commands, 267 rows placed + 14413849309182271769ull, // brackets seed 104730, 82 trades, 521 commands, 568 rows placed + 17836979205695001440ull, // brackets seed 209459, 76 trades, 487 commands, 569 rows placed + 6185985077652624317ull, // brackets seed 314188, 144 trades, 818 commands, 919 rows placed 3737662390130165609ull, // brackets seed 418917, 94 trades, 557 commands, 632 rows placed - 12564349532258145101ull, // brackets seed 523646, 71 trades, 517 commands, 451 rows placed - 16592088247586231707ull, // brackets seed 628375, 100 trades, 536 commands, 662 rows placed - 9284251268869981574ull, // brackets seed 733104, 91 trades, 534 commands, 668 rows placed + 10672250305191215856ull, // brackets seed 523646, 71 trades, 517 commands, 451 rows placed + 1855891803821441106ull, // brackets seed 628375, 101 trades, 524 commands, 724 rows placed + 12279391351944236297ull, // brackets seed 733104, 91 trades, 520 commands, 628 rows placed 5785846610847751202ull, // brackets seed 837833, 88 trades, 482 commands, 547 rows placed - 15132040098433212256ull, // brackets seed 942562, 85 trades, 490 commands, 457 rows placed - 9624518409170657431ull, // brackets seed 1047291, 86 trades, 468 commands, 526 rows placed - 15597660030609647223ull, // brackets seed 1152020, 80 trades, 487 commands, 637 rows placed + 7040999793872498538ull, // brackets seed 942562, 85 trades, 490 commands, 457 rows placed + 14314528608775687405ull, // brackets seed 1047291, 85 trades, 468 commands, 547 rows placed + 13142231282207717561ull, // brackets seed 1152020, 76 trades, 478 commands, 687 rows placed 15849491031983505500ull, // brackets seed 1256749, 88 trades, 511 commands, 566 rows placed 9918882490509451201ull, // brackets seed 1361478, 81 trades, 540 commands, 556 rows placed - 6438015927310529334ull, // brackets seed 1466207, 72 trades, 568 commands, 577 rows placed - 17986613720295818227ull, // brackets seed 1570936, 69 trades, 475 commands, 535 rows placed + 16303049545172805242ull, // brackets seed 1466207, 70 trades, 568 commands, 669 rows placed + 8808473759868459315ull, // brackets seed 1570936, 69 trades, 475 commands, 647 rows placed 15751662755974333447ull, // brackets seed 1675665, 87 trades, 520 commands, 602 rows placed - 1550055039614354387ull, // brackets seed 1780394, 95 trades, 697 commands, 705 rows placed - 2015779222221407240ull, // brackets seed 1885123, 90 trades, 542 commands, 587 rows placed - 1872777722679096721ull, // brackets seed 1989852, 88 trades, 514 commands, 572 rows placed + 5435028051816123314ull, // brackets seed 1780394, 95 trades, 697 commands, 705 rows placed + 355520822114888679ull, // brackets seed 1885123, 89 trades, 542 commands, 583 rows placed + 5402310364412118994ull, // brackets seed 1989852, 89 trades, 505 commands, 639 rows placed 11372035822747614056ull, // brackets seed 2094581, 85 trades, 496 commands, 575 rows placed 14176875954190827756ull, // brackets seed 2199310, 79 trades, 446 commands, 504 rows placed - 1455827488198344686ull, // brackets seed 2304039, 92 trades, 524 commands, 594 rows placed - 11979119011511214626ull, // brackets seed 2408768, 94 trades, 559 commands, 694 rows placed + 15236878832940726852ull, // brackets seed 2304039, 94 trades, 577 commands, 623 rows placed + 4629113575017480307ull, // brackets seed 2408768, 78 trades, 528 commands, 676 rows placed 1774065971302770478ull, // brackets seed 2513497, 86 trades, 524 commands, 508 rows placed - 10830766203810780391ull, // brackets seed 2618226, 77 trades, 512 commands, 581 rows placed - 6172869291970139916ull, // brackets seed 2722955, 85 trades, 503 commands, 661 rows placed - 9104251795068008494ull, // brackets seed 2827684, 74 trades, 506 commands, 570 rows placed + 14075994758817023751ull, // brackets seed 2618226, 77 trades, 512 commands, 581 rows placed + 4051940090401488259ull, // brackets seed 2722955, 97 trades, 510 commands, 704 rows placed + 11315777065199433571ull, // brackets seed 2827684, 93 trades, 530 commands, 631 rows placed 4716911134077760843ull, // brackets seed 2932413, 75 trades, 469 commands, 515 rows placed - 6708847418396898846ull, // brackets seed 3037142, 72 trades, 476 commands, 521 rows placed - 7906131650209684341ull, // brackets seed 3141871, 101 trades, 537 commands, 653 rows placed - 2136070886124345893ull, // brackets seed 3246600, 115 trades, 657 commands, 847 rows placed + 9921951745340606853ull, // brackets seed 3037142, 72 trades, 476 commands, 521 rows placed + 5531345307764558073ull, // brackets seed 3141871, 101 trades, 537 commands, 665 rows placed + 13285723008256739950ull, // brackets seed 3246600, 115 trades, 657 commands, 863 rows placed 14331750725646803575ull, // brackets seed 3351329, 85 trades, 504 commands, 520 rows placed 7444517548780988427ull, // brackets seed 3456058, 80 trades, 530 commands, 529 rows placed - 6178143291077411923ull, // brackets seed 3560787, 107 trades, 521 commands, 634 rows placed - 11302959202215131177ull, // brackets seed 3665516, 86 trades, 536 commands, 566 rows placed + 5589169095865416088ull, // brackets seed 3560787, 101 trades, 500 commands, 583 rows placed + 3100862192505875197ull, // brackets seed 3665516, 92 trades, 571 commands, 626 rows placed 5122510577651000745ull, // brackets seed 3770245, 78 trades, 503 commands, 560 rows placed - 11244999202114714137ull, // chains seed 104731, 59 trades, 301 commands, 286 rows placed - 11413128619516782387ull, // chains seed 209460, 85 trades, 390 commands, 386 rows placed - 5640170146031748940ull, // chains seed 314189, 107 trades, 526 commands, 454 rows placed - 3121930092179333450ull, // chains seed 418918, 45 trades, 312 commands, 299 rows placed - 165784926829741990ull, // chains seed 523647, 61 trades, 322 commands, 329 rows placed - 10122741367483366791ull, // chains seed 628376, 90 trades, 358 commands, 415 rows placed - 13574785125744972298ull, // chains seed 733105, 88 trades, 351 commands, 318 rows placed - 10108332716913767868ull, // chains seed 837834, 64 trades, 359 commands, 358 rows placed - 12836024126514275333ull, // chains seed 942563, 59 trades, 341 commands, 322 rows placed - 16444976724508807594ull, // chains seed 1047292, 64 trades, 343 commands, 341 rows placed - 6715343122935757718ull, // chains seed 1152021, 70 trades, 342 commands, 384 rows placed - 12725802557554099964ull, // chains seed 1256750, 65 trades, 319 commands, 302 rows placed - 1798455623603086886ull, // chains seed 1361479, 64 trades, 312 commands, 323 rows placed - 9973593188851025006ull, // chains seed 1466208, 73 trades, 382 commands, 338 rows placed - 16038855060654239956ull, // chains seed 1570937, 90 trades, 366 commands, 370 rows placed - 4161494051921551918ull, // chains seed 1675666, 64 trades, 321 commands, 339 rows placed - 11993708214650334332ull, // chains seed 1780395, 82 trades, 480 commands, 436 rows placed - 8033768331525115464ull, // chains seed 1885124, 66 trades, 360 commands, 304 rows placed - 8033691744657508662ull, // chains seed 1989853, 73 trades, 353 commands, 373 rows placed - 10675979410645833515ull, // chains seed 2094582, 75 trades, 321 commands, 355 rows placed - 13103985213974940416ull, // chains seed 2199311, 54 trades, 326 commands, 285 rows placed - 9463870180619581974ull, // chains seed 2304040, 81 trades, 342 commands, 353 rows placed - 1057624106564667840ull, // chains seed 2408769, 67 trades, 347 commands, 374 rows placed - 1646636678506043505ull, // chains seed 2513498, 66 trades, 336 commands, 332 rows placed - 15055331467095475975ull, // chains seed 2618227, 65 trades, 336 commands, 331 rows placed - 18266136563684836716ull, // chains seed 2722956, 62 trades, 345 commands, 344 rows placed - 16845310385882942743ull, // chains seed 2827685, 57 trades, 337 commands, 325 rows placed - 6451466112212038981ull, // chains seed 2932414, 65 trades, 350 commands, 330 rows placed - 6296605148496367564ull, // chains seed 3037143, 81 trades, 324 commands, 328 rows placed - 18120582394174365029ull, // chains seed 3141872, 82 trades, 355 commands, 349 rows placed - 17828078233634427072ull, // chains seed 3246601, 98 trades, 485 commands, 466 rows placed - 9531902743311875506ull, // chains seed 3351330, 60 trades, 311 commands, 310 rows placed - 1817828500136052373ull, // chains seed 3456059, 59 trades, 292 commands, 289 rows placed - 5397962162622300398ull, // chains seed 3560788, 73 trades, 359 commands, 337 rows placed - 3084783482698433102ull, // chains seed 3665517, 70 trades, 347 commands, 326 rows placed - 15996277680102595200ull, // chains seed 3770246, 76 trades, 362 commands, 298 rows placed + 11442749230602752319ull, // chains seed 104731, 67 trades, 318 commands, 355 rows placed + 973054536545442904ull, // chains seed 209460, 66 trades, 384 commands, 419 rows placed + 3920721464232107171ull, // chains seed 314189, 62 trades, 465 commands, 490 rows placed + 13576086977686921552ull, // chains seed 418918, 44 trades, 293 commands, 349 rows placed + 13312605232919440966ull, // chains seed 523647, 47 trades, 314 commands, 327 rows placed + 6547333340447752844ull, // chains seed 628376, 73 trades, 342 commands, 367 rows placed + 3389962382455065848ull, // chains seed 733105, 65 trades, 369 commands, 421 rows placed + 10071629273929733187ull, // chains seed 837834, 60 trades, 368 commands, 418 rows placed + 15248982654499518174ull, // chains seed 942563, 56 trades, 343 commands, 361 rows placed + 6473049927299403237ull, // chains seed 1047292, 34 trades, 363 commands, 400 rows placed + 5657155561465537738ull, // chains seed 1152021, 64 trades, 338 commands, 409 rows placed + 140926706188109974ull, // chains seed 1256750, 64 trades, 314 commands, 315 rows placed + 1884320088982623449ull, // chains seed 1361479, 65 trades, 309 commands, 326 rows placed + 17488515754754645248ull, // chains seed 1466208, 78 trades, 390 commands, 407 rows placed + 8944738996467660919ull, // chains seed 1570937, 68 trades, 353 commands, 354 rows placed + 3579325126147100396ull, // chains seed 1675666, 64 trades, 314 commands, 355 rows placed + 17646633536556052253ull, // chains seed 1780395, 67 trades, 438 commands, 465 rows placed + 7799970005878023626ull, // chains seed 1885124, 62 trades, 362 commands, 378 rows placed + 3042300852731654308ull, // chains seed 1989853, 75 trades, 350 commands, 372 rows placed + 9104693962049720988ull, // chains seed 2094582, 37 trades, 310 commands, 353 rows placed + 3043920897357867824ull, // chains seed 2199311, 56 trades, 321 commands, 327 rows placed + 17550770482504522165ull, // chains seed 2304040, 61 trades, 330 commands, 349 rows placed + 10315907993814478663ull, // chains seed 2408769, 62 trades, 349 commands, 378 rows placed + 11887874226824834835ull, // chains seed 2513498, 50 trades, 344 commands, 365 rows placed + 8569167112979511745ull, // chains seed 2618227, 51 trades, 339 commands, 386 rows placed + 1052992062651979911ull, // chains seed 2722956, 59 trades, 355 commands, 422 rows placed + 18246893670312018403ull, // chains seed 2827685, 54 trades, 342 commands, 370 rows placed + 16270472390219306311ull, // chains seed 2932414, 62 trades, 336 commands, 373 rows placed + 14669408219364885155ull, // chains seed 3037143, 61 trades, 344 commands, 412 rows placed + 4441359501225912121ull, // chains seed 3141872, 59 trades, 364 commands, 395 rows placed + 4493812043353694436ull, // chains seed 3246601, 73 trades, 444 commands, 515 rows placed + 10753585802392999956ull, // chains seed 3351330, 45 trades, 301 commands, 354 rows placed + 9038807849693065880ull, // chains seed 3456059, 43 trades, 310 commands, 298 rows placed + 2170094277734349720ull, // chains seed 3560788, 60 trades, 363 commands, 422 rows placed + 11220893490402964604ull, // chains seed 3665517, 70 trades, 347 commands, 380 rows placed + 1535576416135278226ull, // chains seed 3770246, 62 trades, 346 commands, 372 rows placed }; diff --git a/tests/test_adapter_lookup_index_witness_data.hpp b/tests/test_adapter_lookup_index_witness_data.hpp index ce08a937..2941a67a 100644 --- a/tests/test_adapter_lookup_index_witness_data.hpp +++ b/tests/test_adapter_lookup_index_witness_data.hpp @@ -797,9 +797,14 @@ constexpr std::uint64_t kFlip_reads[] = { // expectation corrected (v19-D): kFlip_final 3653829772705383872ull -> 8142912162810668916ull, because v19-D keeps fewer retired rows and folds a bracket family's erased members behind a retained one as runs (pineforge-source-adapter/v4: K1 releases a current-cycle leg the revival's superseded test answers for; BracketRoster parks); the trades did not move; harvested with the TU's own switch on this tree (r5/v19-d). // expectation corrected (V19-FIX): kFlip_final 8142912162810668916ull -> 9440871734801450516ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. constexpr std::uint64_t kFlip_final = 9440871734801450516ull; +// R5 lane H-THIN's P10 moves one kFlip trade (INT26: the behaviour half of its +// hash step ab297364, harvested on the integrated tree, equal to the lane's): +// {1736121780000, 1736121840000, 103, 106} -> {1736121780000, 1736121900000, +// 103, 105.5}; the rows, reads and final hash values move once, in the INT26 +// v19 hash re-pin. constexpr Trade kFlip_trades[] = { {1736121720000LL, 1736121840000LL, 102, 106, 1, 0}, - {1736121780000LL, 1736121840000LL, 103, 106, 1, 0}, + {1736121780000LL, 1736121900000LL, 103, 105.5, 1, 0}, {1736121900000LL, 1736121960000LL, 105.5, 109, 1, 0}, {1736122080000LL, 1736122260000LL, 110, 111, 1, 0}, {1736122200000LL, 1736122260000LL, 111, 111, 1, 0}, diff --git a/tests/test_adapter_quiet_bar_pinned.inc b/tests/test_adapter_quiet_bar_pinned.inc index 45ea5734..3a55b10c 100644 --- a/tests/test_adapter_quiet_bar_pinned.inc +++ b/tests/test_adapter_quiet_bar_pinned.inc @@ -973,6 +973,152 @@ // Random10/0/0: trades 0x274b98362186a62f -> 0xe335d30ff30abed4, boundary_count 70 -> 69, trade_count 23 -> 22 // Random22/0/1: trades 0x603c10590ec68778 -> 0x87be32daa84c6426 // Random22/0/0: trades 0x603c10590ec68778 -> 0x87be32daa84c6426 +// R5 lane H-THIN moves these runs' trade digests and counts (INT26: the +// behaviour half of its hash step ab297364, harvested on the integrated tree +// with this TU's own switch): E19 books every lot's excursions from the +// kernel's sampler, which the trade digest folds, and P10 reserves a default +// FIFO strategy.exit(from_entry)'s own entry quantity. Their hash values move +// once, in the INT26 v19 hash re-pin (140 runs): +// CalcOnFills/0/1: trades 0x0b9be2eee5282e84 -> 0x87698c8f4050ce14, boundary_count 57 -> 55, trade_count 10 -> 8 (a run an earlier pick had moved too) +// CalcOnFills/0/0: trades 0x0b9be2eee5282e84 -> 0x87698c8f4050ce14, boundary_count 57 -> 55, trade_count 10 -> 8 (a run an earlier pick had moved too) +// CalcOnFills/1/1: trades 0x57aa00467e66c075 -> 0xa16d17de4933efda, boundary_count 54 -> 52, trade_count 8 -> 6 +// CalcOnFills/1/0: trades 0x57aa00467e66c075 -> 0xa16d17de4933efda, boundary_count 54 -> 52, trade_count 8 -> 6 +// CloseOrders/0/1: trades 0x36ebbdb119ff2815 -> 0xbc944a52841481b1 +// CloseOrders/0/0: trades 0x36ebbdb119ff2815 -> 0xbc944a52841481b1 +// CloseOrders/1/1: trades 0x8261dcdcaf8f5751 -> 0x4cdcb678c06d1a59 +// CloseOrders/1/0: trades 0x8261dcdcaf8f5751 -> 0x4cdcb678c06d1a59 +// Random01/0/1: trades 0x71cab0c731b4168d -> 0xf2ae7ef9674d4425 (a run an earlier pick had moved too) +// Random01/0/0: trades 0x71cab0c731b4168d -> 0xf2ae7ef9674d4425 (a run an earlier pick had moved too) +// Random01/1/1: trades 0xee55d992ca5fbe97 -> 0x64f480bfff90f76a (a run an earlier pick had moved too) +// Random01/1/0: trades 0xee55d992ca5fbe97 -> 0x64f480bfff90f76a (a run an earlier pick had moved too) +// Random02/0/1: trades 0xec986a4dcdf52208 -> 0x41793244d920be3d +// Random02/0/0: trades 0xec986a4dcdf52208 -> 0x41793244d920be3d +// Random02/1/1: trades 0x95d130f42b9a5776 -> 0x31c2efb8bb736f56 +// Random02/1/0: trades 0x95d130f42b9a5776 -> 0x31c2efb8bb736f56 +// Random03/0/1: trades 0x922d2ca744f5f225 -> 0x8bd01a3594893c29, trade_count 12 -> 11 +// Random03/0/0: trades 0x922d2ca744f5f225 -> 0x8bd01a3594893c29, trade_count 12 -> 11 +// Random03/1/1: trades 0xe213d492a23dfa58 -> 0xde85b8fe81a28e59, trade_count 12 -> 11 +// Random03/1/0: trades 0xe213d492a23dfa58 -> 0xde85b8fe81a28e59, trade_count 12 -> 11 +// Random04/0/1: trades 0xc687874028817a5b -> 0x38df0af477021418 +// Random04/0/0: trades 0xc687874028817a5b -> 0x38df0af477021418 +// Random04/1/1: trades 0x37389353e6ac8bae -> 0x28a43578727c43b5 +// Random04/1/0: trades 0x37389353e6ac8bae -> 0x28a43578727c43b5 +// Random05/0/1: trades 0x5da77f8971db3d7a -> 0x3d4c3aae7bbc3b85 (a run an earlier pick had moved too) +// Random05/0/0: trades 0x5da77f8971db3d7a -> 0x3d4c3aae7bbc3b85 (a run an earlier pick had moved too) +// Random05/1/1: trades 0xdf770d465571b020 -> 0xc83a1288cfd7ee4d (a run an earlier pick had moved too) +// Random05/1/0: trades 0xdf770d465571b020 -> 0xc83a1288cfd7ee4d (a run an earlier pick had moved too) +// Random06/0/1: trades 0xb724c9aadc73f827 -> 0x19f4b2ac9f3f76f7 +// Random06/0/0: trades 0xb724c9aadc73f827 -> 0x19f4b2ac9f3f76f7 +// Random06/1/1: trades 0xb1c8ba35c0dd393d -> 0x3561041c80b6994f +// Random06/1/0: trades 0xb1c8ba35c0dd393d -> 0x3561041c80b6994f +// Random07/0/1: trades 0x93cafb8ccdf82d3f -> 0x67ce7c56c16f12a7 +// Random07/0/0: trades 0x93cafb8ccdf82d3f -> 0x67ce7c56c16f12a7 +// Random07/1/1: trades 0xf516d07702627571 -> 0x9cbc1f07c0648d08 +// Random07/1/0: trades 0xf516d07702627571 -> 0x9cbc1f07c0648d08 +// Random08/0/1: trades 0x1d5e1165b05daf8f -> 0x68ebab74d498abed +// Random08/0/0: trades 0x1d5e1165b05daf8f -> 0x68ebab74d498abed +// Random08/1/1: trades 0x7f7cc33b43e81cae -> 0x84ec2499efb30a9e +// Random08/1/0: trades 0x7f7cc33b43e81cae -> 0x84ec2499efb30a9e +// Random09/0/1: trades 0x8abae7302d7911be -> 0xcda6a598aa82b5f0 +// Random09/0/0: trades 0x8abae7302d7911be -> 0xcda6a598aa82b5f0 +// Random09/1/1: trades 0x99aae055f2d8091d -> 0x86b2bbc1465372d7 +// Random09/1/0: trades 0x99aae055f2d8091d -> 0x86b2bbc1465372d7 +// Random10/0/1: trades 0xe335d30ff30abed4 -> 0xc9ae66b6356d2649, trade_count 22 -> 23 (P10 on the book PAR-ORDERS' and PAR-ORDERS-2's rules leave: the FIFO exit Y reserves its entry's own unit and the reversal closes the rest, one row more) +// Random10/0/0: trades 0xe335d30ff30abed4 -> 0xc9ae66b6356d2649, trade_count 22 -> 23 (P10 on the book PAR-ORDERS' and PAR-ORDERS-2's rules leave: the FIFO exit Y reserves its entry's own unit and the reversal closes the rest, one row more) +// Random10/1/1: trades 0x5dcb9b87576b70ed -> 0x724c585ad13c935d, trade_count 22 -> 23 (a run an earlier pick had moved too) +// Random10/1/0: trades 0x5dcb9b87576b70ed -> 0x724c585ad13c935d, trade_count 22 -> 23 (a run an earlier pick had moved too) +// Random12/0/1: trades 0x9ec5e7b69521e874 -> 0xf60b6b66546787a7 +// Random12/0/0: trades 0x9ec5e7b69521e874 -> 0xf60b6b66546787a7 +// Random12/1/1: trades 0xf2c6cada55accbe3 -> 0xfc32c809c37c8290 +// Random12/1/0: trades 0xf2c6cada55accbe3 -> 0xfc32c809c37c8290 +// Random13/0/1: trades 0x7f720ad1034d2991 -> 0x9b711a1ad7f0af2b (a run an earlier pick had moved too) +// Random13/0/0: trades 0x7f720ad1034d2991 -> 0x9b711a1ad7f0af2b (a run an earlier pick had moved too) +// Random13/1/1: trades 0xfe702e1eb2f469c0 -> 0x78cbd8f6435e4796 (a run an earlier pick had moved too) +// Random13/1/0: trades 0xfe702e1eb2f469c0 -> 0x78cbd8f6435e4796 (a run an earlier pick had moved too) +// Random15/0/1: trades 0xa729d04861d8f487 -> 0xc7b14196e7b743c6, trade_count 11 -> 12 +// Random15/0/0: trades 0xa729d04861d8f487 -> 0xc7b14196e7b743c6, trade_count 11 -> 12 +// Random15/1/1: trades 0xeabf56d064294b39 -> 0x9e6d92b9d40ff818, trade_count 11 -> 12 +// Random15/1/0: trades 0xeabf56d064294b39 -> 0x9e6d92b9d40ff818, trade_count 11 -> 12 +// Random16/0/1: trades 0xf751fbce2a822df8 -> 0x6889115fd9a0fe41, trade_count 17 -> 15 +// Random16/0/0: trades 0xf751fbce2a822df8 -> 0x6889115fd9a0fe41, trade_count 17 -> 15 +// Random16/1/1: trades 0xeb8b68e57975e307 -> 0x552b70400304fd25, trade_count 17 -> 15 +// Random16/1/0: trades 0xeb8b68e57975e307 -> 0x552b70400304fd25, trade_count 17 -> 15 +// Random17/0/1: trades 0xf5006d7b3dcd5c43 -> 0xf3e62779123d7565 +// Random17/0/0: trades 0xf5006d7b3dcd5c43 -> 0xf3e62779123d7565 +// Random17/1/1: trades 0x364d848056b18418 -> 0x5814b347681cd974 +// Random17/1/0: trades 0x364d848056b18418 -> 0x5814b347681cd974 +// Random18/0/1: trades 0x946751d8a6dd7292 -> 0x5f03c99e6854a46a +// Random18/0/0: trades 0x946751d8a6dd7292 -> 0x5f03c99e6854a46a +// Random18/1/1: trades 0x946751d8a6dd7292 -> 0x5f03c99e6854a46a +// Random18/1/0: trades 0x946751d8a6dd7292 -> 0x5f03c99e6854a46a +// Random19/0/1: trades 0xcfd0b979ead5a43c -> 0x95d7b888983ad8e6 +// Random19/0/0: trades 0xcfd0b979ead5a43c -> 0x95d7b888983ad8e6 +// Random20/0/1: trades 0x35a6da11cd35b29c -> 0x3e0974d0ca2bfa0b +// Random20/0/0: trades 0x35a6da11cd35b29c -> 0x3e0974d0ca2bfa0b +// Random20/1/1: trades 0xafad93e39c8617d9 -> 0x1cd39188275caa5d +// Random20/1/0: trades 0xafad93e39c8617d9 -> 0x1cd39188275caa5d +// Random21/0/1: trades 0x0571605cf9e50eb1 -> 0xbc89178a735678f6 (a run an earlier pick had moved too) +// Random21/0/0: trades 0x0571605cf9e50eb1 -> 0xbc89178a735678f6 (a run an earlier pick had moved too) +// Random21/1/1: trades 0x20e23c0060589e5e -> 0x0cb28a314aa970ae (a run an earlier pick had moved too) +// Random21/1/0: trades 0x20e23c0060589e5e -> 0x0cb28a314aa970ae (a run an earlier pick had moved too) +// Random22/0/1: trades 0x87be32daa84c6426 -> 0xedb01aeb6057c25b (a run an earlier pick had moved too) +// Random22/0/0: trades 0x87be32daa84c6426 -> 0xedb01aeb6057c25b (a run an earlier pick had moved too) +// Random22/1/1: trades 0x29259a278cc3600b -> 0x70ac53cf16690a86 +// Random22/1/0: trades 0x29259a278cc3600b -> 0x70ac53cf16690a86 +// Random27/0/1: trades 0x3c0165d3836f406a -> 0xbfe2f70bda5f466d +// Random27/0/0: trades 0x3c0165d3836f406a -> 0xbfe2f70bda5f466d +// Random27/1/1: trades 0x97addcfee41a4dd8 -> 0x0bb88d26b3dc8cc1 +// Random27/1/0: trades 0x97addcfee41a4dd8 -> 0x0bb88d26b3dc8cc1 +// Random28/0/1: trades 0xc447f3f5058cd81b -> 0x7ee366b7feceed4e +// Random28/0/0: trades 0xc447f3f5058cd81b -> 0x7ee366b7feceed4e +// Random31/1/1: trades 0x2569d07bfd939198 -> 0xc42eba994cb3bc7f +// Random31/1/0: trades 0x2569d07bfd939198 -> 0xc42eba994cb3bc7f +// Random32/0/1: trades 0x4994badb687c8750 -> 0xb5c0211fafc19569, trade_count 19 -> 18 +// Random32/0/0: trades 0x4994badb687c8750 -> 0xb5c0211fafc19569, trade_count 19 -> 18 +// Random33/0/1: trades 0xf8d13762bc3ab9c0 -> 0xb69ec34fc873583a +// Random33/0/0: trades 0xf8d13762bc3ab9c0 -> 0xb69ec34fc873583a +// Random33/1/1: trades 0xca5862380ffcef2f -> 0xa51cad1078866f00 +// Random33/1/0: trades 0xca5862380ffcef2f -> 0xa51cad1078866f00 +// Random35/0/1: trades 0xa8b1e0f14c6cc49a -> 0x2e59ea5f57457b34 +// Random35/0/0: trades 0xa8b1e0f14c6cc49a -> 0x2e59ea5f57457b34 +// Random35/1/1: trades 0xb2b344c283516d30 -> 0xf9dd1b1a0c3d9f6a +// Random35/1/0: trades 0xb2b344c283516d30 -> 0xf9dd1b1a0c3d9f6a +// Random36/0/1: trades 0x24bf064fe8416ffd -> 0x71b837070cbf3386 (a run an earlier pick had moved too) +// Random36/0/0: trades 0x24bf064fe8416ffd -> 0x71b837070cbf3386 (a run an earlier pick had moved too) +// Random36/1/1: trades 0x86d1b68c9a0453a2 -> 0xc91ddda9c3e46569 (a run an earlier pick had moved too) +// Random36/1/0: trades 0x86d1b68c9a0453a2 -> 0xc91ddda9c3e46569 (a run an earlier pick had moved too) +// Random37/0/1: trades 0x6b11a4e13499e50d -> 0x52bf39ca2f2765c6 +// Random37/0/0: trades 0x6b11a4e13499e50d -> 0x52bf39ca2f2765c6 +// Random37/1/1: trades 0x15dfd0d0fb5f649a -> 0x41de938f8a060981 +// Random37/1/0: trades 0x15dfd0d0fb5f649a -> 0x41de938f8a060981 +// Random38/0/1: trades 0x3a8c8a474889316e -> 0xe38062999cae5744 +// Random38/0/0: trades 0x3a8c8a474889316e -> 0xe38062999cae5744 +// Random38/1/1: trades 0x084c597474eaa400 -> 0x13e4cce8783da942 +// Random38/1/0: trades 0x084c597474eaa400 -> 0x13e4cce8783da942 +// Random40/0/1: trades 0x76e2d6a7f41b161c -> 0x78c15a2f88a24807 +// Random40/0/0: trades 0x76e2d6a7f41b161c -> 0x78c15a2f88a24807 +// Random40/1/1: trades 0x9a84d463fe03ec91 -> 0x41975079a7cc855b +// Random40/1/0: trades 0x9a84d463fe03ec91 -> 0x41975079a7cc855b +// Random41/0/1: trades 0x4b1ec3cc1bd0909c -> 0xf6e95f1e1a0264bc +// Random41/0/0: trades 0x4b1ec3cc1bd0909c -> 0xf6e95f1e1a0264bc +// Random41/1/1: trades 0x4b1ec3cc1bd0909c -> 0xf6e95f1e1a0264bc +// Random41/1/0: trades 0x4b1ec3cc1bd0909c -> 0xf6e95f1e1a0264bc +// Random42/0/1: trades 0x75b6953551fe9575 -> 0xd4d5dd88f1bc7129 +// Random42/0/0: trades 0x75b6953551fe9575 -> 0xd4d5dd88f1bc7129 +// Random42/1/1: trades 0xf736fbadc411c223 -> 0x292b9553961fa163 +// Random42/1/0: trades 0xf736fbadc411c223 -> 0x292b9553961fa163 +// Random44/0/1: trades 0x513c7ae44c865ec8 -> 0x316199af1a335dbf +// Random44/0/0: trades 0x513c7ae44c865ec8 -> 0x316199af1a335dbf +// Random44/1/1: trades 0x1a79f44215a3981e -> 0x9f0e22f8fb2632f6 +// Random44/1/0: trades 0x1a79f44215a3981e -> 0x9f0e22f8fb2632f6 +// Random45/0/1: trades 0xf543670d7cc61817 -> 0xf7b0f07cda17c110 +// Random45/0/0: trades 0xf543670d7cc61817 -> 0xf7b0f07cda17c110 +// Random45/1/1: trades 0xa2aef1f38f464b4d -> 0x797aff1ec9239dff +// Random45/1/0: trades 0xa2aef1f38f464b4d -> 0x797aff1ec9239dff +// Random48/0/1: trades 0x62a7be3a970dee6a -> 0xda74ceb1a9e7d904, trade_count 9 -> 8 +// Random48/0/0: trades 0x62a7be3a970dee6a -> 0xda74ceb1a9e7d904, trade_count 9 -> 8 +// Random48/1/1: trades 0xee7c0d61bfb7dd62 -> 0x0876fbb01d747e2d, trade_count 9 -> 8 +// Random48/1/0: trades 0xee7c0d61bfb7dd62 -> 0x0876fbb01d747e2d, trade_count 9 -> 8 constexpr Pinned kPinned[] = { {"Quiet", 0, 1, 0x1c2b2e37a44f6877ull, 0x81799b58f0e052b1ull, 0x47fe0d7eaf8e51e3ull, 0xe0aa4c0240939bcfull, 40, 40, 0}, {"Quiet", 0, 0, 0x1c2b2e37a44f6877ull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0xe0aa4c0240939bcfull, 40, 0, 0}, @@ -1018,14 +1164,14 @@ constexpr Pinned kPinned[] = { {"DeclinedReversal", 0, 0, 0xfcb9145b39ec7b5aull, 0x14650fb0739d0383ull, 0xc3448bef59024d51ull, 0x764450acb0596bfcull, 24, 0, 1}, {"DeclinedReversal", 1, 1, 0x4a1378eba012ef40ull, 0x16ea86de895d5b40ull, 0xc3448bef59024d51ull, 0x14734f0e113371e4ull, 24, 24, 1}, {"DeclinedReversal", 1, 0, 0x4a1378eba012ef40ull, 0x14650fb0739d0383ull, 0xc3448bef59024d51ull, 0x14734f0e113371e4ull, 24, 0, 1}, - {"CalcOnFills", 0, 1, 0x7641907936993779ull, 0x3777fe6ece242880ull, 0x0b9be2eee5282e84ull, 0x2583ce2bc6a3c1d9ull, 57, 40, 10}, - {"CalcOnFills", 0, 0, 0x7641907936993779ull, 0x14650fb0739d0383ull, 0x0b9be2eee5282e84ull, 0x2583ce2bc6a3c1d9ull, 57, 0, 10}, - {"CalcOnFills", 1, 1, 0xd120e8fefb0dc236ull, 0xdbb28de374d30bc3ull, 0x57aa00467e66c075ull, 0xad6e26cf69199431ull, 54, 40, 8}, - {"CalcOnFills", 1, 0, 0xd120e8fefb0dc236ull, 0x14650fb0739d0383ull, 0x57aa00467e66c075ull, 0xad6e26cf69199431ull, 54, 0, 8}, - {"CloseOrders", 0, 1, 0x78ff3352f7085436ull, 0x896bcdcf15c8a4c6ull, 0x36ebbdb119ff2815ull, 0x8e67de695bfb2c60ull, 48, 48, 16}, - {"CloseOrders", 0, 0, 0x78ff3352f7085436ull, 0x14650fb0739d0383ull, 0x36ebbdb119ff2815ull, 0x8e67de695bfb2c60ull, 48, 0, 16}, - {"CloseOrders", 1, 1, 0xe00ae8be3caf3315ull, 0x2584350c36f68309ull, 0x8261dcdcaf8f5751ull, 0xa998448999ff9b1eull, 48, 48, 16}, - {"CloseOrders", 1, 0, 0xe00ae8be3caf3315ull, 0x14650fb0739d0383ull, 0x8261dcdcaf8f5751ull, 0xa998448999ff9b1eull, 48, 0, 16}, + {"CalcOnFills", 0, 1, 0x7641907936993779ull, 0x3777fe6ece242880ull, 0x87698c8f4050ce14ull, 0x2583ce2bc6a3c1d9ull, 55, 40, 8}, + {"CalcOnFills", 0, 0, 0x7641907936993779ull, 0x14650fb0739d0383ull, 0x87698c8f4050ce14ull, 0x2583ce2bc6a3c1d9ull, 55, 0, 8}, + {"CalcOnFills", 1, 1, 0xd120e8fefb0dc236ull, 0xdbb28de374d30bc3ull, 0xa16d17de4933efdaull, 0xad6e26cf69199431ull, 52, 40, 6}, + {"CalcOnFills", 1, 0, 0xd120e8fefb0dc236ull, 0x14650fb0739d0383ull, 0xa16d17de4933efdaull, 0xad6e26cf69199431ull, 52, 0, 6}, + {"CloseOrders", 0, 1, 0x78ff3352f7085436ull, 0x896bcdcf15c8a4c6ull, 0xbc944a52841481b1ull, 0x8e67de695bfb2c60ull, 48, 48, 16}, + {"CloseOrders", 0, 0, 0x78ff3352f7085436ull, 0x14650fb0739d0383ull, 0xbc944a52841481b1ull, 0x8e67de695bfb2c60ull, 48, 0, 16}, + {"CloseOrders", 1, 1, 0xe00ae8be3caf3315ull, 0x2584350c36f68309ull, 0x4cdcb678c06d1a59ull, 0xa998448999ff9b1eull, 48, 48, 16}, + {"CloseOrders", 1, 0, 0xe00ae8be3caf3315ull, 0x14650fb0739d0383ull, 0x4cdcb678c06d1a59ull, 0xa998448999ff9b1eull, 48, 0, 16}, {"CloseOrdersSlipped", 0, 1, 0x58e9160429c03bcaull, 0xd3632e32e35ef626ull, 0x0f201395d3fe211full, 0x13f82da0279b5836ull, 36, 36, 2}, {"CloseOrdersSlipped", 0, 0, 0x58e9160429c03bcaull, 0x14650fb0739d0383ull, 0x0f201395d3fe211full, 0x13f82da0279b5836ull, 36, 0, 2}, {"CloseOrdersSlipped", 1, 1, 0x3507072a3e435b83ull, 0xc5d77a9b36fdec85ull, 0x0f201395d3fe211full, 0xc7b132391c547427ull, 36, 36, 2}, @@ -1054,94 +1200,94 @@ constexpr Pinned kPinned[] = { {"AccountFx", 0, 0, 0xaeec7af5859fe6b3ull, 0x14650fb0739d0383ull, 0x3b853d0c008c3078ull, 0x2a28b4e61cd5cbbcull, 48, 0, 1}, {"Stream", 0, 1, 0x49574a22705f52c0ull, 0x3cade1c0a6b65b75ull, 0xce38e7485b22ec04ull, 0x8c9bb935bf0c32daull, 40, 40, 5}, {"Stream", 0, 0, 0x49574a22705f52c0ull, 0x14650fb0739d0383ull, 0xce38e7485b22ec04ull, 0x8c9bb935bf0c32daull, 40, 0, 5}, - {"Random01", 0, 1, 0xfa3864e9f5f3a619ull, 0x22da84303f34b121ull, 0x71cab0c731b4168dull, 0xa142013345fb25ddull, 55, 55, 15}, - {"Random01", 0, 0, 0xfa3864e9f5f3a619ull, 0x14650fb0739d0383ull, 0x71cab0c731b4168dull, 0xa142013345fb25ddull, 55, 0, 15}, - {"Random01", 1, 1, 0xfdafd368c22a3bb4ull, 0x30ace005c5ea2e1bull, 0xee55d992ca5fbe97ull, 0xb44a05ed46f5109dull, 55, 55, 15}, - {"Random01", 1, 0, 0xfdafd368c22a3bb4ull, 0x14650fb0739d0383ull, 0xee55d992ca5fbe97ull, 0xb44a05ed46f5109dull, 55, 0, 15}, - {"Random02", 0, 1, 0xf933c58464603966ull, 0x8549ba7d7e735482ull, 0xec986a4dcdf52208ull, 0xd9870babe5d18626ull, 58, 40, 12}, - {"Random02", 0, 0, 0xf933c58464603966ull, 0x14650fb0739d0383ull, 0xec986a4dcdf52208ull, 0xd9870babe5d18626ull, 58, 0, 12}, - {"Random02", 1, 1, 0x25ba5f1040df3b56ull, 0x1593a8e96a0c90a9ull, 0x95d130f42b9a5776ull, 0x0fb50af38c25c54dull, 58, 40, 12}, - {"Random02", 1, 0, 0x25ba5f1040df3b56ull, 0x14650fb0739d0383ull, 0x95d130f42b9a5776ull, 0x0fb50af38c25c54dull, 58, 0, 12}, - {"Random03", 0, 1, 0xb5a3f885ba0143fdull, 0xd91b7712cc62209bull, 0x922d2ca744f5f225ull, 0xd93ef819aff693f3ull, 44, 44, 12}, - {"Random03", 0, 0, 0xb5a3f885ba0143fdull, 0x14650fb0739d0383ull, 0x922d2ca744f5f225ull, 0xd93ef819aff693f3ull, 44, 0, 12}, - {"Random03", 1, 1, 0x36230659e7bf570eull, 0xdb83075a5690e996ull, 0xe213d492a23dfa58ull, 0x23acbc0041308125ull, 44, 44, 12}, - {"Random03", 1, 0, 0x36230659e7bf570eull, 0x14650fb0739d0383ull, 0xe213d492a23dfa58ull, 0x23acbc0041308125ull, 44, 0, 12}, - {"Random04", 0, 1, 0x0c41be29e4497a7cull, 0x1fdaabb2a1c312cdull, 0xc687874028817a5bull, 0x542f1b01ef32d535ull, 44, 44, 18}, - {"Random04", 0, 0, 0x0c41be29e4497a7cull, 0x14650fb0739d0383ull, 0xc687874028817a5bull, 0x542f1b01ef32d535ull, 44, 0, 18}, - {"Random04", 1, 1, 0x93ade030662e0b77ull, 0x5beecc5dc910feb8ull, 0x37389353e6ac8baeull, 0x254f4f4933be69e0ull, 44, 44, 18}, - {"Random04", 1, 0, 0x93ade030662e0b77ull, 0x14650fb0739d0383ull, 0x37389353e6ac8baeull, 0x254f4f4933be69e0ull, 44, 0, 18}, - {"Random05", 0, 1, 0x3e8e6495fe2e6dc1ull, 0x2703dbc59d3516a6ull, 0x5da77f8971db3d7aull, 0xb72557b7f6894eafull, 58, 53, 3}, - {"Random05", 0, 0, 0x3e8e6495fe2e6dc1ull, 0x14650fb0739d0383ull, 0x5da77f8971db3d7aull, 0xb72557b7f6894eafull, 58, 0, 3}, - {"Random05", 1, 1, 0x4be132773d14c0e0ull, 0xa6803dead66d3343ull, 0xdf770d465571b020ull, 0xb3561169b361d8bfull, 66, 53, 10}, - {"Random05", 1, 0, 0x4be132773d14c0e0ull, 0x14650fb0739d0383ull, 0xdf770d465571b020ull, 0xb3561169b361d8bfull, 66, 0, 10}, - {"Random06", 0, 1, 0xecb44da2cbdb5c42ull, 0x5485171eedfc86cfull, 0xb724c9aadc73f827ull, 0xff2efe1925e45a9dull, 46, 46, 13}, - {"Random06", 0, 0, 0xecb44da2cbdb5c42ull, 0x14650fb0739d0383ull, 0xb724c9aadc73f827ull, 0xff2efe1925e45a9dull, 46, 0, 13}, - {"Random06", 1, 1, 0x65cc03efd59ec6b5ull, 0x2dce2153546130dbull, 0xb1c8ba35c0dd393dull, 0x1130940f86f108d2ull, 46, 46, 13}, - {"Random06", 1, 0, 0x65cc03efd59ec6b5ull, 0x14650fb0739d0383ull, 0xb1c8ba35c0dd393dull, 0x1130940f86f108d2ull, 46, 0, 13}, - {"Random07", 0, 1, 0x7bf47df91573ab9cull, 0x3c1feb47a4d6d81eull, 0x93cafb8ccdf82d3full, 0x438a828933e58c5bull, 46, 46, 12}, - {"Random07", 0, 0, 0x7bf47df91573ab9cull, 0x14650fb0739d0383ull, 0x93cafb8ccdf82d3full, 0x438a828933e58c5bull, 46, 0, 12}, - {"Random07", 1, 1, 0x29fcc776be13bdf5ull, 0x9a5ab1b2b00efff1ull, 0xf516d07702627571ull, 0x353c5f0e5bc1c22bull, 46, 46, 12}, - {"Random07", 1, 0, 0x29fcc776be13bdf5ull, 0x14650fb0739d0383ull, 0xf516d07702627571ull, 0x353c5f0e5bc1c22bull, 46, 0, 12}, - {"Random08", 0, 1, 0xc2f66d9e5f4943c7ull, 0xf4a38750150093e6ull, 0x1d5e1165b05daf8full, 0x9ce7eb6ee2b122baull, 93, 59, 24}, - {"Random08", 0, 0, 0xc2f66d9e5f4943c7ull, 0x14650fb0739d0383ull, 0x1d5e1165b05daf8full, 0x9ce7eb6ee2b122baull, 93, 0, 24}, - {"Random08", 1, 1, 0xbc7e1d017519ee12ull, 0xc18ef29ec0f4fa8eull, 0x7f7cc33b43e81caeull, 0xbd83191789f76873ull, 93, 59, 24}, - {"Random08", 1, 0, 0xbc7e1d017519ee12ull, 0x14650fb0739d0383ull, 0x7f7cc33b43e81caeull, 0xbd83191789f76873ull, 93, 0, 24}, - {"Random09", 0, 1, 0x400c46d70e6e84bcull, 0xc58cc1d4b5bd3580ull, 0x8abae7302d7911beull, 0x1f6cdd2e111e016aull, 58, 58, 2}, - {"Random09", 0, 0, 0x400c46d70e6e84bcull, 0x14650fb0739d0383ull, 0x8abae7302d7911beull, 0x1f6cdd2e111e016aull, 58, 0, 2}, - {"Random09", 1, 1, 0x32d4b35f029bad56ull, 0x447742874dde46e0ull, 0x99aae055f2d8091dull, 0x3d99e7abdd92a773ull, 58, 58, 2}, - {"Random09", 1, 0, 0x32d4b35f029bad56ull, 0x14650fb0739d0383ull, 0x99aae055f2d8091dull, 0x3d99e7abdd92a773ull, 58, 0, 2}, - {"Random10", 0, 1, 0x998ee74ea73334c6ull, 0x5a3104007433abc0ull, 0xe335d30ff30abed4ull, 0x0a7888fb9b890fb4ull, 69, 51, 22}, - {"Random10", 0, 0, 0x998ee74ea73334c6ull, 0x14650fb0739d0383ull, 0xe335d30ff30abed4ull, 0x0a7888fb9b890fb4ull, 69, 0, 22}, - {"Random10", 1, 1, 0xde7dbd52ac33a7ffull, 0xaf8f0ca763b06949ull, 0x5dcb9b87576b70edull, 0xfbe70c357ac296cfull, 67, 51, 22}, - {"Random10", 1, 0, 0xde7dbd52ac33a7ffull, 0x14650fb0739d0383ull, 0x5dcb9b87576b70edull, 0xfbe70c357ac296cfull, 67, 0, 22}, + {"Random01", 0, 1, 0xfa3864e9f5f3a619ull, 0x22da84303f34b121ull, 0xf2ae7ef9674d4425ull, 0xa142013345fb25ddull, 55, 55, 15}, + {"Random01", 0, 0, 0xfa3864e9f5f3a619ull, 0x14650fb0739d0383ull, 0xf2ae7ef9674d4425ull, 0xa142013345fb25ddull, 55, 0, 15}, + {"Random01", 1, 1, 0xfdafd368c22a3bb4ull, 0x30ace005c5ea2e1bull, 0x64f480bfff90f76aull, 0xb44a05ed46f5109dull, 55, 55, 15}, + {"Random01", 1, 0, 0xfdafd368c22a3bb4ull, 0x14650fb0739d0383ull, 0x64f480bfff90f76aull, 0xb44a05ed46f5109dull, 55, 0, 15}, + {"Random02", 0, 1, 0xf933c58464603966ull, 0x8549ba7d7e735482ull, 0x41793244d920be3dull, 0xd9870babe5d18626ull, 58, 40, 12}, + {"Random02", 0, 0, 0xf933c58464603966ull, 0x14650fb0739d0383ull, 0x41793244d920be3dull, 0xd9870babe5d18626ull, 58, 0, 12}, + {"Random02", 1, 1, 0x25ba5f1040df3b56ull, 0x1593a8e96a0c90a9ull, 0x31c2efb8bb736f56ull, 0x0fb50af38c25c54dull, 58, 40, 12}, + {"Random02", 1, 0, 0x25ba5f1040df3b56ull, 0x14650fb0739d0383ull, 0x31c2efb8bb736f56ull, 0x0fb50af38c25c54dull, 58, 0, 12}, + {"Random03", 0, 1, 0xb5a3f885ba0143fdull, 0xd91b7712cc62209bull, 0x8bd01a3594893c29ull, 0xd93ef819aff693f3ull, 44, 44, 11}, + {"Random03", 0, 0, 0xb5a3f885ba0143fdull, 0x14650fb0739d0383ull, 0x8bd01a3594893c29ull, 0xd93ef819aff693f3ull, 44, 0, 11}, + {"Random03", 1, 1, 0x36230659e7bf570eull, 0xdb83075a5690e996ull, 0xde85b8fe81a28e59ull, 0x23acbc0041308125ull, 44, 44, 11}, + {"Random03", 1, 0, 0x36230659e7bf570eull, 0x14650fb0739d0383ull, 0xde85b8fe81a28e59ull, 0x23acbc0041308125ull, 44, 0, 11}, + {"Random04", 0, 1, 0x0c41be29e4497a7cull, 0x1fdaabb2a1c312cdull, 0x38df0af477021418ull, 0x542f1b01ef32d535ull, 44, 44, 18}, + {"Random04", 0, 0, 0x0c41be29e4497a7cull, 0x14650fb0739d0383ull, 0x38df0af477021418ull, 0x542f1b01ef32d535ull, 44, 0, 18}, + {"Random04", 1, 1, 0x93ade030662e0b77ull, 0x5beecc5dc910feb8ull, 0x28a43578727c43b5ull, 0x254f4f4933be69e0ull, 44, 44, 18}, + {"Random04", 1, 0, 0x93ade030662e0b77ull, 0x14650fb0739d0383ull, 0x28a43578727c43b5ull, 0x254f4f4933be69e0ull, 44, 0, 18}, + {"Random05", 0, 1, 0x3e8e6495fe2e6dc1ull, 0x2703dbc59d3516a6ull, 0x3d4c3aae7bbc3b85ull, 0xb72557b7f6894eafull, 58, 53, 3}, + {"Random05", 0, 0, 0x3e8e6495fe2e6dc1ull, 0x14650fb0739d0383ull, 0x3d4c3aae7bbc3b85ull, 0xb72557b7f6894eafull, 58, 0, 3}, + {"Random05", 1, 1, 0x4be132773d14c0e0ull, 0xa6803dead66d3343ull, 0xc83a1288cfd7ee4dull, 0xb3561169b361d8bfull, 66, 53, 10}, + {"Random05", 1, 0, 0x4be132773d14c0e0ull, 0x14650fb0739d0383ull, 0xc83a1288cfd7ee4dull, 0xb3561169b361d8bfull, 66, 0, 10}, + {"Random06", 0, 1, 0xecb44da2cbdb5c42ull, 0x5485171eedfc86cfull, 0x19f4b2ac9f3f76f7ull, 0xff2efe1925e45a9dull, 46, 46, 13}, + {"Random06", 0, 0, 0xecb44da2cbdb5c42ull, 0x14650fb0739d0383ull, 0x19f4b2ac9f3f76f7ull, 0xff2efe1925e45a9dull, 46, 0, 13}, + {"Random06", 1, 1, 0x65cc03efd59ec6b5ull, 0x2dce2153546130dbull, 0x3561041c80b6994full, 0x1130940f86f108d2ull, 46, 46, 13}, + {"Random06", 1, 0, 0x65cc03efd59ec6b5ull, 0x14650fb0739d0383ull, 0x3561041c80b6994full, 0x1130940f86f108d2ull, 46, 0, 13}, + {"Random07", 0, 1, 0x7bf47df91573ab9cull, 0x3c1feb47a4d6d81eull, 0x67ce7c56c16f12a7ull, 0x438a828933e58c5bull, 46, 46, 12}, + {"Random07", 0, 0, 0x7bf47df91573ab9cull, 0x14650fb0739d0383ull, 0x67ce7c56c16f12a7ull, 0x438a828933e58c5bull, 46, 0, 12}, + {"Random07", 1, 1, 0x29fcc776be13bdf5ull, 0x9a5ab1b2b00efff1ull, 0x9cbc1f07c0648d08ull, 0x353c5f0e5bc1c22bull, 46, 46, 12}, + {"Random07", 1, 0, 0x29fcc776be13bdf5ull, 0x14650fb0739d0383ull, 0x9cbc1f07c0648d08ull, 0x353c5f0e5bc1c22bull, 46, 0, 12}, + {"Random08", 0, 1, 0xc2f66d9e5f4943c7ull, 0xf4a38750150093e6ull, 0x68ebab74d498abedull, 0x9ce7eb6ee2b122baull, 93, 59, 24}, + {"Random08", 0, 0, 0xc2f66d9e5f4943c7ull, 0x14650fb0739d0383ull, 0x68ebab74d498abedull, 0x9ce7eb6ee2b122baull, 93, 0, 24}, + {"Random08", 1, 1, 0xbc7e1d017519ee12ull, 0xc18ef29ec0f4fa8eull, 0x84ec2499efb30a9eull, 0xbd83191789f76873ull, 93, 59, 24}, + {"Random08", 1, 0, 0xbc7e1d017519ee12ull, 0x14650fb0739d0383ull, 0x84ec2499efb30a9eull, 0xbd83191789f76873ull, 93, 0, 24}, + {"Random09", 0, 1, 0x400c46d70e6e84bcull, 0xc58cc1d4b5bd3580ull, 0xcda6a598aa82b5f0ull, 0x1f6cdd2e111e016aull, 58, 58, 2}, + {"Random09", 0, 0, 0x400c46d70e6e84bcull, 0x14650fb0739d0383ull, 0xcda6a598aa82b5f0ull, 0x1f6cdd2e111e016aull, 58, 0, 2}, + {"Random09", 1, 1, 0x32d4b35f029bad56ull, 0x447742874dde46e0ull, 0x86b2bbc1465372d7ull, 0x3d99e7abdd92a773ull, 58, 58, 2}, + {"Random09", 1, 0, 0x32d4b35f029bad56ull, 0x14650fb0739d0383ull, 0x86b2bbc1465372d7ull, 0x3d99e7abdd92a773ull, 58, 0, 2}, + {"Random10", 0, 1, 0x998ee74ea73334c6ull, 0x5a3104007433abc0ull, 0xc9ae66b6356d2649ull, 0x0a7888fb9b890fb4ull, 69, 51, 23}, + {"Random10", 0, 0, 0x998ee74ea73334c6ull, 0x14650fb0739d0383ull, 0xc9ae66b6356d2649ull, 0x0a7888fb9b890fb4ull, 69, 0, 23}, + {"Random10", 1, 1, 0xde7dbd52ac33a7ffull, 0xaf8f0ca763b06949ull, 0x724c585ad13c935dull, 0xfbe70c357ac296cfull, 67, 51, 23}, + {"Random10", 1, 0, 0xde7dbd52ac33a7ffull, 0x14650fb0739d0383ull, 0x724c585ad13c935dull, 0xfbe70c357ac296cfull, 67, 0, 23}, {"Random11", 0, 1, 0x805038d1cb0a3848ull, 0xebc3fde6db4a37f3ull, 0x2f92b1af8d2cfafcull, 0x27bc15c5c142c728ull, 60, 60, 3}, {"Random11", 0, 0, 0x805038d1cb0a3848ull, 0x14650fb0739d0383ull, 0x2f92b1af8d2cfafcull, 0x27bc15c5c142c728ull, 60, 0, 3}, {"Random11", 1, 1, 0x4a739854a58a3db9ull, 0xc91f1e4519586a06ull, 0xad6d6d2577cca5c9ull, 0x8af717fd3fa04097ull, 60, 60, 8}, {"Random11", 1, 0, 0x4a739854a58a3db9ull, 0x14650fb0739d0383ull, 0xad6d6d2577cca5c9ull, 0x8af717fd3fa04097ull, 60, 0, 8}, - {"Random12", 0, 1, 0xb9a9bb3fafab7349ull, 0xe93fb7b821f6a7feull, 0x9ec5e7b69521e874ull, 0x29c94b5df165789aull, 92, 62, 22}, - {"Random12", 0, 0, 0xb9a9bb3fafab7349ull, 0x14650fb0739d0383ull, 0x9ec5e7b69521e874ull, 0x29c94b5df165789aull, 92, 0, 22}, - {"Random12", 1, 1, 0x298ab491e3caa61full, 0xf8e78400f1c148acull, 0xf2c6cada55accbe3ull, 0x927c4394206b42b2ull, 93, 62, 22}, - {"Random12", 1, 0, 0x298ab491e3caa61full, 0x14650fb0739d0383ull, 0xf2c6cada55accbe3ull, 0x927c4394206b42b2ull, 93, 0, 22}, - {"Random13", 0, 1, 0x39caae56088c1db8ull, 0x4028b545fb2c3ad9ull, 0x7f720ad1034d2991ull, 0xc19e5f5bbefe0e7full, 60, 60, 16}, - {"Random13", 0, 0, 0x39caae56088c1db8ull, 0x14650fb0739d0383ull, 0x7f720ad1034d2991ull, 0xc19e5f5bbefe0e7full, 60, 0, 16}, - {"Random13", 1, 1, 0x07b714c492b5ae85ull, 0xa03b3f8bed95186dull, 0xfe702e1eb2f469c0ull, 0xa9f1d1a463925b52ull, 60, 60, 16}, - {"Random13", 1, 0, 0x07b714c492b5ae85ull, 0x14650fb0739d0383ull, 0xfe702e1eb2f469c0ull, 0xa9f1d1a463925b52ull, 60, 0, 16}, + {"Random12", 0, 1, 0xb9a9bb3fafab7349ull, 0xe93fb7b821f6a7feull, 0xf60b6b66546787a7ull, 0x29c94b5df165789aull, 92, 62, 22}, + {"Random12", 0, 0, 0xb9a9bb3fafab7349ull, 0x14650fb0739d0383ull, 0xf60b6b66546787a7ull, 0x29c94b5df165789aull, 92, 0, 22}, + {"Random12", 1, 1, 0x298ab491e3caa61full, 0xf8e78400f1c148acull, 0xfc32c809c37c8290ull, 0x927c4394206b42b2ull, 93, 62, 22}, + {"Random12", 1, 0, 0x298ab491e3caa61full, 0x14650fb0739d0383ull, 0xfc32c809c37c8290ull, 0x927c4394206b42b2ull, 93, 0, 22}, + {"Random13", 0, 1, 0x39caae56088c1db8ull, 0x4028b545fb2c3ad9ull, 0x9b711a1ad7f0af2bull, 0xc19e5f5bbefe0e7full, 60, 60, 16}, + {"Random13", 0, 0, 0x39caae56088c1db8ull, 0x14650fb0739d0383ull, 0x9b711a1ad7f0af2bull, 0xc19e5f5bbefe0e7full, 60, 0, 16}, + {"Random13", 1, 1, 0x07b714c492b5ae85ull, 0xa03b3f8bed95186dull, 0x78cbd8f6435e4796ull, 0xa9f1d1a463925b52ull, 60, 60, 16}, + {"Random13", 1, 0, 0x07b714c492b5ae85ull, 0x14650fb0739d0383ull, 0x78cbd8f6435e4796ull, 0xa9f1d1a463925b52ull, 60, 0, 16}, {"Random14", 0, 1, 0x21085d70db4c336bull, 0x394ebfbc2c15522aull, 0x4e8d14373274d558ull, 0xad1f71b1afdd37f8ull, 49, 49, 16}, {"Random14", 0, 0, 0x21085d70db4c336bull, 0x14650fb0739d0383ull, 0x4e8d14373274d558ull, 0xad1f71b1afdd37f8ull, 49, 0, 16}, {"Random14", 1, 1, 0x5a8dbff598244e8cull, 0x4c48857b5cd25816ull, 0xbec287230f684e1dull, 0x7f45221fb1111aa3ull, 49, 49, 16}, {"Random14", 1, 0, 0x5a8dbff598244e8cull, 0x14650fb0739d0383ull, 0xbec287230f684e1dull, 0x7f45221fb1111aa3ull, 49, 0, 16}, - {"Random15", 0, 1, 0x1fe125a3fb2912dfull, 0x9ef69d4200bfb809ull, 0xa729d04861d8f487ull, 0xf8da94854e79d89eull, 42, 42, 11}, - {"Random15", 0, 0, 0x1fe125a3fb2912dfull, 0x14650fb0739d0383ull, 0xa729d04861d8f487ull, 0xf8da94854e79d89eull, 42, 0, 11}, - {"Random15", 1, 1, 0x7b17bf5a43044023ull, 0xdeec1e0290d1370dull, 0xeabf56d064294b39ull, 0x01758143c50ac69dull, 42, 42, 11}, - {"Random15", 1, 0, 0x7b17bf5a43044023ull, 0x14650fb0739d0383ull, 0xeabf56d064294b39ull, 0x01758143c50ac69dull, 42, 0, 11}, - {"Random16", 0, 1, 0xde7f3e61caa50135ull, 0x9593cb63a9a06f14ull, 0xf751fbce2a822df8ull, 0xc99a0f083d0b2507ull, 54, 54, 17}, - {"Random16", 0, 0, 0xde7f3e61caa50135ull, 0x14650fb0739d0383ull, 0xf751fbce2a822df8ull, 0xc99a0f083d0b2507ull, 54, 0, 17}, - {"Random16", 1, 1, 0xffbc34c7947c611bull, 0xa2b9ee52a1338391ull, 0xeb8b68e57975e307ull, 0x1f80513beefa0b1aull, 54, 54, 17}, - {"Random16", 1, 0, 0xffbc34c7947c611bull, 0x14650fb0739d0383ull, 0xeb8b68e57975e307ull, 0x1f80513beefa0b1aull, 54, 0, 17}, - {"Random17", 0, 1, 0x15bab2d913fb1d81ull, 0x0a3d587bbe912dcfull, 0xf5006d7b3dcd5c43ull, 0x7185bfaebb406df5ull, 49, 47, 1}, - {"Random17", 0, 0, 0x15bab2d913fb1d81ull, 0x14650fb0739d0383ull, 0xf5006d7b3dcd5c43ull, 0x7185bfaebb406df5ull, 49, 0, 1}, - {"Random17", 1, 1, 0xa978788efbcc1631ull, 0xbcbe24594a8acfcaull, 0x364d848056b18418ull, 0x285eaf39ec559587ull, 50, 47, 2}, - {"Random17", 1, 0, 0xa978788efbcc1631ull, 0x14650fb0739d0383ull, 0x364d848056b18418ull, 0x285eaf39ec559587ull, 50, 0, 2}, - {"Random18", 0, 1, 0x90ed08a2949b1fb6ull, 0x665cda1c725bc1b2ull, 0x946751d8a6dd7292ull, 0xe16bbfbfa58dbeb1ull, 66, 62, 3}, - {"Random18", 0, 0, 0x90ed08a2949b1fb6ull, 0x14650fb0739d0383ull, 0x946751d8a6dd7292ull, 0xe16bbfbfa58dbeb1ull, 66, 0, 3}, - {"Random18", 1, 1, 0xbf864c04890cf915ull, 0x807a82ee057fbb5dull, 0x946751d8a6dd7292ull, 0x5c359872c6618dd3ull, 66, 62, 3}, - {"Random18", 1, 0, 0xbf864c04890cf915ull, 0x14650fb0739d0383ull, 0x946751d8a6dd7292ull, 0x5c359872c6618dd3ull, 66, 0, 3}, - {"Random19", 0, 1, 0x3c5cc975f928b775ull, 0xea691a951590ece8ull, 0xcfd0b979ead5a43cull, 0x17d53da80c3a895full, 40, 40, 10}, - {"Random19", 0, 0, 0x3c5cc975f928b775ull, 0x14650fb0739d0383ull, 0xcfd0b979ead5a43cull, 0x17d53da80c3a895full, 40, 0, 10}, + {"Random15", 0, 1, 0x1fe125a3fb2912dfull, 0x9ef69d4200bfb809ull, 0xc7b14196e7b743c6ull, 0xf8da94854e79d89eull, 42, 42, 12}, + {"Random15", 0, 0, 0x1fe125a3fb2912dfull, 0x14650fb0739d0383ull, 0xc7b14196e7b743c6ull, 0xf8da94854e79d89eull, 42, 0, 12}, + {"Random15", 1, 1, 0x7b17bf5a43044023ull, 0xdeec1e0290d1370dull, 0x9e6d92b9d40ff818ull, 0x01758143c50ac69dull, 42, 42, 12}, + {"Random15", 1, 0, 0x7b17bf5a43044023ull, 0x14650fb0739d0383ull, 0x9e6d92b9d40ff818ull, 0x01758143c50ac69dull, 42, 0, 12}, + {"Random16", 0, 1, 0xde7f3e61caa50135ull, 0x9593cb63a9a06f14ull, 0x6889115fd9a0fe41ull, 0xc99a0f083d0b2507ull, 54, 54, 15}, + {"Random16", 0, 0, 0xde7f3e61caa50135ull, 0x14650fb0739d0383ull, 0x6889115fd9a0fe41ull, 0xc99a0f083d0b2507ull, 54, 0, 15}, + {"Random16", 1, 1, 0xffbc34c7947c611bull, 0xa2b9ee52a1338391ull, 0x552b70400304fd25ull, 0x1f80513beefa0b1aull, 54, 54, 15}, + {"Random16", 1, 0, 0xffbc34c7947c611bull, 0x14650fb0739d0383ull, 0x552b70400304fd25ull, 0x1f80513beefa0b1aull, 54, 0, 15}, + {"Random17", 0, 1, 0x15bab2d913fb1d81ull, 0x0a3d587bbe912dcfull, 0xf3e62779123d7565ull, 0x7185bfaebb406df5ull, 49, 47, 1}, + {"Random17", 0, 0, 0x15bab2d913fb1d81ull, 0x14650fb0739d0383ull, 0xf3e62779123d7565ull, 0x7185bfaebb406df5ull, 49, 0, 1}, + {"Random17", 1, 1, 0xa978788efbcc1631ull, 0xbcbe24594a8acfcaull, 0x5814b347681cd974ull, 0x285eaf39ec559587ull, 50, 47, 2}, + {"Random17", 1, 0, 0xa978788efbcc1631ull, 0x14650fb0739d0383ull, 0x5814b347681cd974ull, 0x285eaf39ec559587ull, 50, 0, 2}, + {"Random18", 0, 1, 0x90ed08a2949b1fb6ull, 0x665cda1c725bc1b2ull, 0x5f03c99e6854a46aull, 0xe16bbfbfa58dbeb1ull, 66, 62, 3}, + {"Random18", 0, 0, 0x90ed08a2949b1fb6ull, 0x14650fb0739d0383ull, 0x5f03c99e6854a46aull, 0xe16bbfbfa58dbeb1ull, 66, 0, 3}, + {"Random18", 1, 1, 0xbf864c04890cf915ull, 0x807a82ee057fbb5dull, 0x5f03c99e6854a46aull, 0x5c359872c6618dd3ull, 66, 62, 3}, + {"Random18", 1, 0, 0xbf864c04890cf915ull, 0x14650fb0739d0383ull, 0x5f03c99e6854a46aull, 0x5c359872c6618dd3ull, 66, 0, 3}, + {"Random19", 0, 1, 0x3c5cc975f928b775ull, 0xea691a951590ece8ull, 0x95d7b888983ad8e6ull, 0x17d53da80c3a895full, 40, 40, 10}, + {"Random19", 0, 0, 0x3c5cc975f928b775ull, 0x14650fb0739d0383ull, 0x95d7b888983ad8e6ull, 0x17d53da80c3a895full, 40, 0, 10}, {"Random19", 1, 1, 0x92536ad17b9fbf26ull, 0xabc10759f5e1ffa1ull, 0x011bec7774b82ec9ull, 0x168791086f51c368ull, 40, 40, 11}, {"Random19", 1, 0, 0x92536ad17b9fbf26ull, 0x14650fb0739d0383ull, 0x011bec7774b82ec9ull, 0x168791086f51c368ull, 40, 0, 11}, - {"Random20", 0, 1, 0x90b20d166e1357e0ull, 0x808745030887aa13ull, 0x35a6da11cd35b29cull, 0x3968c24c22199036ull, 70, 57, 21}, - {"Random20", 0, 0, 0x90b20d166e1357e0ull, 0x14650fb0739d0383ull, 0x35a6da11cd35b29cull, 0x3968c24c22199036ull, 70, 0, 21}, - {"Random20", 1, 1, 0xa82d4e53d2192a5full, 0x8e1411c055e37af3ull, 0xafad93e39c8617d9ull, 0x5c29f15f02417fcaull, 72, 57, 21}, - {"Random20", 1, 0, 0xa82d4e53d2192a5full, 0x14650fb0739d0383ull, 0xafad93e39c8617d9ull, 0x5c29f15f02417fcaull, 72, 0, 21}, - {"Random21", 0, 1, 0x18e96beb584671fcull, 0x6c5b4e5a67209f94ull, 0x0571605cf9e50eb1ull, 0xca719be0f46f42acull, 54, 54, 18}, - {"Random21", 0, 0, 0x18e96beb584671fcull, 0x14650fb0739d0383ull, 0x0571605cf9e50eb1ull, 0xca719be0f46f42acull, 54, 0, 18}, - {"Random21", 1, 1, 0xd32a038cc5b6ea6eull, 0x9074957bf890f8e6ull, 0x20e23c0060589e5eull, 0xb6387745e72b0075ull, 54, 54, 18}, - {"Random21", 1, 0, 0xd32a038cc5b6ea6eull, 0x14650fb0739d0383ull, 0x20e23c0060589e5eull, 0xb6387745e72b0075ull, 54, 0, 18}, - {"Random22", 0, 1, 0xef05a2f01ca0c05cull, 0xb151ebfe9a44ebebull, 0x87be32daa84c6426ull, 0x1b1b4c42bccc2191ull, 68, 49, 13}, - {"Random22", 0, 0, 0xef05a2f01ca0c05cull, 0x14650fb0739d0383ull, 0x87be32daa84c6426ull, 0x1b1b4c42bccc2191ull, 68, 0, 13}, - {"Random22", 1, 1, 0x1624d3651edc9089ull, 0x2a2f5457686fdb7full, 0x29259a278cc3600bull, 0xb361721c9b7f57e7ull, 68, 49, 13}, - {"Random22", 1, 0, 0x1624d3651edc9089ull, 0x14650fb0739d0383ull, 0x29259a278cc3600bull, 0xb361721c9b7f57e7ull, 68, 0, 13}, + {"Random20", 0, 1, 0x90b20d166e1357e0ull, 0x808745030887aa13ull, 0x3e0974d0ca2bfa0bull, 0x3968c24c22199036ull, 70, 57, 21}, + {"Random20", 0, 0, 0x90b20d166e1357e0ull, 0x14650fb0739d0383ull, 0x3e0974d0ca2bfa0bull, 0x3968c24c22199036ull, 70, 0, 21}, + {"Random20", 1, 1, 0xa82d4e53d2192a5full, 0x8e1411c055e37af3ull, 0x1cd39188275caa5dull, 0x5c29f15f02417fcaull, 72, 57, 21}, + {"Random20", 1, 0, 0xa82d4e53d2192a5full, 0x14650fb0739d0383ull, 0x1cd39188275caa5dull, 0x5c29f15f02417fcaull, 72, 0, 21}, + {"Random21", 0, 1, 0x18e96beb584671fcull, 0x6c5b4e5a67209f94ull, 0xbc89178a735678f6ull, 0xca719be0f46f42acull, 54, 54, 18}, + {"Random21", 0, 0, 0x18e96beb584671fcull, 0x14650fb0739d0383ull, 0xbc89178a735678f6ull, 0xca719be0f46f42acull, 54, 0, 18}, + {"Random21", 1, 1, 0xd32a038cc5b6ea6eull, 0x9074957bf890f8e6ull, 0x0cb28a314aa970aeull, 0xb6387745e72b0075ull, 54, 54, 18}, + {"Random21", 1, 0, 0xd32a038cc5b6ea6eull, 0x14650fb0739d0383ull, 0x0cb28a314aa970aeull, 0xb6387745e72b0075ull, 54, 0, 18}, + {"Random22", 0, 1, 0xef05a2f01ca0c05cull, 0xb151ebfe9a44ebebull, 0xedb01aeb6057c25bull, 0x1b1b4c42bccc2191ull, 68, 49, 13}, + {"Random22", 0, 0, 0xef05a2f01ca0c05cull, 0x14650fb0739d0383ull, 0xedb01aeb6057c25bull, 0x1b1b4c42bccc2191ull, 68, 0, 13}, + {"Random22", 1, 1, 0x1624d3651edc9089ull, 0x2a2f5457686fdb7full, 0x70ac53cf16690a86ull, 0xb361721c9b7f57e7ull, 68, 49, 13}, + {"Random22", 1, 0, 0x1624d3651edc9089ull, 0x14650fb0739d0383ull, 0x70ac53cf16690a86ull, 0xb361721c9b7f57e7ull, 68, 0, 13}, {"Random23", 0, 1, 0xc50cf90af093d6e9ull, 0x629f1e11fd65d1cdull, 0xf0e62f804df9e022ull, 0x9f14134203a5c6e9ull, 56, 54, 1}, {"Random23", 0, 0, 0xc50cf90af093d6e9ull, 0x14650fb0739d0383ull, 0xf0e62f804df9e022ull, 0x9f14134203a5c6e9ull, 56, 0, 1}, {"Random23", 1, 1, 0x61be7e60a493c3efull, 0x7182d347041d20e2ull, 0xf0e62f804df9e022ull, 0xcac3f08a3f6d7067ull, 56, 54, 1}, @@ -1158,12 +1304,12 @@ constexpr Pinned kPinned[] = { {"Random26", 0, 0, 0x406a1ebe98ed8ee5ull, 0x14650fb0739d0383ull, 0xd9522717c794d53bull, 0x6d9f1512cb3c63efull, 42, 0, 12}, {"Random26", 1, 1, 0x28ce140c5a29687dull, 0x383f34833e879b1bull, 0xf7b8acd382701dd1ull, 0x500ef2279c277ba0ull, 42, 42, 12}, {"Random26", 1, 0, 0x28ce140c5a29687dull, 0x14650fb0739d0383ull, 0xf7b8acd382701dd1ull, 0x500ef2279c277ba0ull, 42, 0, 12}, - {"Random27", 0, 1, 0xeafcaf72c5794d88ull, 0xfbb77c6f11f0944full, 0x3c0165d3836f406aull, 0xf2d844781c8e9819ull, 50, 50, 22}, - {"Random27", 0, 0, 0xeafcaf72c5794d88ull, 0x14650fb0739d0383ull, 0x3c0165d3836f406aull, 0xf2d844781c8e9819ull, 50, 0, 22}, - {"Random27", 1, 1, 0x433fe5d0340138d8ull, 0x80e409be19d6131dull, 0x97addcfee41a4dd8ull, 0x2a459b69d95400b1ull, 50, 50, 21}, - {"Random27", 1, 0, 0x433fe5d0340138d8ull, 0x14650fb0739d0383ull, 0x97addcfee41a4dd8ull, 0x2a459b69d95400b1ull, 50, 0, 21}, - {"Random28", 0, 1, 0xa0bb8c1925814fe3ull, 0x8e8763454e7d2dcaull, 0xc447f3f5058cd81bull, 0xe51c2406011cd87dull, 54, 54, 19}, - {"Random28", 0, 0, 0xa0bb8c1925814fe3ull, 0x14650fb0739d0383ull, 0xc447f3f5058cd81bull, 0xe51c2406011cd87dull, 54, 0, 19}, + {"Random27", 0, 1, 0xeafcaf72c5794d88ull, 0xfbb77c6f11f0944full, 0xbfe2f70bda5f466dull, 0xf2d844781c8e9819ull, 50, 50, 22}, + {"Random27", 0, 0, 0xeafcaf72c5794d88ull, 0x14650fb0739d0383ull, 0xbfe2f70bda5f466dull, 0xf2d844781c8e9819ull, 50, 0, 22}, + {"Random27", 1, 1, 0x433fe5d0340138d8ull, 0x80e409be19d6131dull, 0x0bb88d26b3dc8cc1ull, 0x2a459b69d95400b1ull, 50, 50, 21}, + {"Random27", 1, 0, 0x433fe5d0340138d8ull, 0x14650fb0739d0383ull, 0x0bb88d26b3dc8cc1ull, 0x2a459b69d95400b1ull, 50, 0, 21}, + {"Random28", 0, 1, 0xa0bb8c1925814fe3ull, 0x8e8763454e7d2dcaull, 0x7ee366b7feceed4eull, 0xe51c2406011cd87dull, 54, 54, 19}, + {"Random28", 0, 0, 0xa0bb8c1925814fe3ull, 0x14650fb0739d0383ull, 0x7ee366b7feceed4eull, 0xe51c2406011cd87dull, 54, 0, 19}, {"Random28", 1, 1, 0xdb2802eb6d8819c7ull, 0x910e834a102ac932ull, 0xee17522a5f3c15dfull, 0x7406a539e21e57ceull, 54, 54, 19}, {"Random28", 1, 0, 0xdb2802eb6d8819c7ull, 0x14650fb0739d0383ull, 0xee17522a5f3c15dfull, 0x7406a539e21e57ceull, 54, 0, 19}, {"Random29", 0, 1, 0x2bbdb428b4a78935ull, 0x6c7b12c7d5aa45e4ull, 0xe6aec4ce1ae7ccd1ull, 0xfd3d2fb6be6132d7ull, 57, 57, 1}, @@ -1176,64 +1322,64 @@ constexpr Pinned kPinned[] = { {"Random30", 1, 0, 0x8de0587cbe97fc0aull, 0x14650fb0739d0383ull, 0x260809f2d3657490ull, 0x7ffde30dc93fc9d7ull, 61, 0, 1}, {"Random31", 0, 1, 0x09b545a0067e8c26ull, 0x3c6ae0150bb63629ull, 0xcad16841aad879ddull, 0xfaf6a143c8b0d038ull, 49, 49, 9}, {"Random31", 0, 0, 0x09b545a0067e8c26ull, 0x14650fb0739d0383ull, 0xcad16841aad879ddull, 0xfaf6a143c8b0d038ull, 49, 0, 9}, - {"Random31", 1, 1, 0xb42bd9cfbe060a73ull, 0x928f7cfde5bdb105ull, 0x2569d07bfd939198ull, 0xf602f077ecb2b7cfull, 49, 49, 9}, - {"Random31", 1, 0, 0xb42bd9cfbe060a73ull, 0x14650fb0739d0383ull, 0x2569d07bfd939198ull, 0xf602f077ecb2b7cfull, 49, 0, 9}, - {"Random32", 0, 1, 0x220cd02053dbc40aull, 0x9afe2ada13350695ull, 0x4994badb687c8750ull, 0xe577534dd3033b52ull, 62, 62, 19}, - {"Random32", 0, 0, 0x220cd02053dbc40aull, 0x14650fb0739d0383ull, 0x4994badb687c8750ull, 0xe577534dd3033b52ull, 62, 0, 19}, + {"Random31", 1, 1, 0xb42bd9cfbe060a73ull, 0x928f7cfde5bdb105ull, 0xc42eba994cb3bc7full, 0xf602f077ecb2b7cfull, 49, 49, 9}, + {"Random31", 1, 0, 0xb42bd9cfbe060a73ull, 0x14650fb0739d0383ull, 0xc42eba994cb3bc7full, 0xf602f077ecb2b7cfull, 49, 0, 9}, + {"Random32", 0, 1, 0x220cd02053dbc40aull, 0x9afe2ada13350695ull, 0xb5c0211fafc19569ull, 0xe577534dd3033b52ull, 62, 62, 18}, + {"Random32", 0, 0, 0x220cd02053dbc40aull, 0x14650fb0739d0383ull, 0xb5c0211fafc19569ull, 0xe577534dd3033b52ull, 62, 0, 18}, {"Random32", 1, 1, 0xc0ca43094334f008ull, 0xe85eb4136b8c435dull, 0xb1a7a66b620c4fd5ull, 0xba233c9fc46444a4ull, 62, 62, 19}, {"Random32", 1, 0, 0xc0ca43094334f008ull, 0x14650fb0739d0383ull, 0xb1a7a66b620c4fd5ull, 0xba233c9fc46444a4ull, 62, 0, 19}, - {"Random33", 0, 1, 0xbfcfebe96632bd39ull, 0x116bc2f57887ba4bull, 0xf8d13762bc3ab9c0ull, 0xf8a0e855a842d39cull, 51, 51, 13}, - {"Random33", 0, 0, 0xbfcfebe96632bd39ull, 0x14650fb0739d0383ull, 0xf8d13762bc3ab9c0ull, 0xf8a0e855a842d39cull, 51, 0, 13}, - {"Random33", 1, 1, 0xe17f0353f37c41f1ull, 0x0ba98d74e8b769b7ull, 0xca5862380ffcef2full, 0xb76d3ca699ea4ebdull, 51, 51, 13}, - {"Random33", 1, 0, 0xe17f0353f37c41f1ull, 0x14650fb0739d0383ull, 0xca5862380ffcef2full, 0xb76d3ca699ea4ebdull, 51, 0, 13}, + {"Random33", 0, 1, 0xbfcfebe96632bd39ull, 0x116bc2f57887ba4bull, 0xb69ec34fc873583aull, 0xf8a0e855a842d39cull, 51, 51, 13}, + {"Random33", 0, 0, 0xbfcfebe96632bd39ull, 0x14650fb0739d0383ull, 0xb69ec34fc873583aull, 0xf8a0e855a842d39cull, 51, 0, 13}, + {"Random33", 1, 1, 0xe17f0353f37c41f1ull, 0x0ba98d74e8b769b7ull, 0xa51cad1078866f00ull, 0xb76d3ca699ea4ebdull, 51, 51, 13}, + {"Random33", 1, 0, 0xe17f0353f37c41f1ull, 0x14650fb0739d0383ull, 0xa51cad1078866f00ull, 0xb76d3ca699ea4ebdull, 51, 0, 13}, {"Random34", 0, 1, 0xea1b68237d95805full, 0xb29d9ea005d9c282ull, 0x81535eb933e496e7ull, 0xb03d41aea9c00c63ull, 51, 51, 1}, {"Random34", 0, 0, 0xea1b68237d95805full, 0x14650fb0739d0383ull, 0x81535eb933e496e7ull, 0xb03d41aea9c00c63ull, 51, 0, 1}, {"Random34", 1, 1, 0x4ecacf8ab2d85c47ull, 0x294fe821fef3729dull, 0xa788aa75c6b931e3ull, 0xe4e38b6ada294505ull, 51, 51, 2}, {"Random34", 1, 0, 0x4ecacf8ab2d85c47ull, 0x14650fb0739d0383ull, 0xa788aa75c6b931e3ull, 0xe4e38b6ada294505ull, 51, 0, 2}, - {"Random35", 0, 1, 0xeb16fb1a6bc80c17ull, 0x7265444996669f20ull, 0xa8b1e0f14c6cc49aull, 0xb971c6bc6d7ac182ull, 63, 63, 24}, - {"Random35", 0, 0, 0xeb16fb1a6bc80c17ull, 0x14650fb0739d0383ull, 0xa8b1e0f14c6cc49aull, 0xb971c6bc6d7ac182ull, 63, 0, 24}, - {"Random35", 1, 1, 0x193ec2cb9a163900ull, 0x9233c78214742aa3ull, 0xb2b344c283516d30ull, 0xa9bdcf22e28567b2ull, 63, 63, 24}, - {"Random35", 1, 0, 0x193ec2cb9a163900ull, 0x14650fb0739d0383ull, 0xb2b344c283516d30ull, 0xa9bdcf22e28567b2ull, 63, 0, 24}, - {"Random36", 0, 1, 0xd4b5588948d4b755ull, 0x6af2dc7dd208ef5dull, 0x24bf064fe8416ffdull, 0x1980e25b047dea78ull, 87, 60, 21}, - {"Random36", 0, 0, 0xd4b5588948d4b755ull, 0x14650fb0739d0383ull, 0x24bf064fe8416ffdull, 0x1980e25b047dea78ull, 87, 0, 21}, - {"Random36", 1, 1, 0xd1532dd9d52ae5f4ull, 0x0e1d68d8d6c4c5e2ull, 0x86d1b68c9a0453a2ull, 0xe93d9e6c724efef6ull, 87, 60, 21}, - {"Random36", 1, 0, 0xd1532dd9d52ae5f4ull, 0x14650fb0739d0383ull, 0x86d1b68c9a0453a2ull, 0xe93d9e6c724efef6ull, 87, 0, 21}, - {"Random37", 0, 1, 0x3a89ff92c403a3e7ull, 0xddda8d77310d918full, 0x6b11a4e13499e50dull, 0xff9c74898bf3a72aull, 71, 45, 18}, - {"Random37", 0, 0, 0x3a89ff92c403a3e7ull, 0x14650fb0739d0383ull, 0x6b11a4e13499e50dull, 0xff9c74898bf3a72aull, 71, 0, 18}, - {"Random37", 1, 1, 0x8a36a09ee42999ecull, 0xf12dd4800e96f492ull, 0x15dfd0d0fb5f649aull, 0x9f14bee9808ee8baull, 70, 45, 17}, - {"Random37", 1, 0, 0x8a36a09ee42999ecull, 0x14650fb0739d0383ull, 0x15dfd0d0fb5f649aull, 0x9f14bee9808ee8baull, 70, 0, 17}, - {"Random38", 0, 1, 0x36ed3eee0da888c2ull, 0x2bf58dbe1c5bd9acull, 0x3a8c8a474889316eull, 0x5b82e26291364fc0ull, 54, 54, 25}, - {"Random38", 0, 0, 0x36ed3eee0da888c2ull, 0x14650fb0739d0383ull, 0x3a8c8a474889316eull, 0x5b82e26291364fc0ull, 54, 0, 25}, - {"Random38", 1, 1, 0xa3225feaf19b98feull, 0x59c34402fcf68e2aull, 0x084c597474eaa400ull, 0x135197c1ef34cbbdull, 54, 54, 25}, - {"Random38", 1, 0, 0xa3225feaf19b98feull, 0x14650fb0739d0383ull, 0x084c597474eaa400ull, 0x135197c1ef34cbbdull, 54, 0, 25}, + {"Random35", 0, 1, 0xeb16fb1a6bc80c17ull, 0x7265444996669f20ull, 0x2e59ea5f57457b34ull, 0xb971c6bc6d7ac182ull, 63, 63, 24}, + {"Random35", 0, 0, 0xeb16fb1a6bc80c17ull, 0x14650fb0739d0383ull, 0x2e59ea5f57457b34ull, 0xb971c6bc6d7ac182ull, 63, 0, 24}, + {"Random35", 1, 1, 0x193ec2cb9a163900ull, 0x9233c78214742aa3ull, 0xf9dd1b1a0c3d9f6aull, 0xa9bdcf22e28567b2ull, 63, 63, 24}, + {"Random35", 1, 0, 0x193ec2cb9a163900ull, 0x14650fb0739d0383ull, 0xf9dd1b1a0c3d9f6aull, 0xa9bdcf22e28567b2ull, 63, 0, 24}, + {"Random36", 0, 1, 0xd4b5588948d4b755ull, 0x6af2dc7dd208ef5dull, 0x71b837070cbf3386ull, 0x1980e25b047dea78ull, 87, 60, 21}, + {"Random36", 0, 0, 0xd4b5588948d4b755ull, 0x14650fb0739d0383ull, 0x71b837070cbf3386ull, 0x1980e25b047dea78ull, 87, 0, 21}, + {"Random36", 1, 1, 0xd1532dd9d52ae5f4ull, 0x0e1d68d8d6c4c5e2ull, 0xc91ddda9c3e46569ull, 0xe93d9e6c724efef6ull, 87, 60, 21}, + {"Random36", 1, 0, 0xd1532dd9d52ae5f4ull, 0x14650fb0739d0383ull, 0xc91ddda9c3e46569ull, 0xe93d9e6c724efef6ull, 87, 0, 21}, + {"Random37", 0, 1, 0x3a89ff92c403a3e7ull, 0xddda8d77310d918full, 0x52bf39ca2f2765c6ull, 0xff9c74898bf3a72aull, 71, 45, 18}, + {"Random37", 0, 0, 0x3a89ff92c403a3e7ull, 0x14650fb0739d0383ull, 0x52bf39ca2f2765c6ull, 0xff9c74898bf3a72aull, 71, 0, 18}, + {"Random37", 1, 1, 0x8a36a09ee42999ecull, 0xf12dd4800e96f492ull, 0x41de938f8a060981ull, 0x9f14bee9808ee8baull, 70, 45, 17}, + {"Random37", 1, 0, 0x8a36a09ee42999ecull, 0x14650fb0739d0383ull, 0x41de938f8a060981ull, 0x9f14bee9808ee8baull, 70, 0, 17}, + {"Random38", 0, 1, 0x36ed3eee0da888c2ull, 0x2bf58dbe1c5bd9acull, 0xe38062999cae5744ull, 0x5b82e26291364fc0ull, 54, 54, 25}, + {"Random38", 0, 0, 0x36ed3eee0da888c2ull, 0x14650fb0739d0383ull, 0xe38062999cae5744ull, 0x5b82e26291364fc0ull, 54, 0, 25}, + {"Random38", 1, 1, 0xa3225feaf19b98feull, 0x59c34402fcf68e2aull, 0x13e4cce8783da942ull, 0x135197c1ef34cbbdull, 54, 54, 25}, + {"Random38", 1, 0, 0xa3225feaf19b98feull, 0x14650fb0739d0383ull, 0x13e4cce8783da942ull, 0x135197c1ef34cbbdull, 54, 0, 25}, {"Random39", 0, 1, 0x9bcdaebb98702055ull, 0xf4194fbb3f8e63f4ull, 0xc193386eb35a8e65ull, 0x3026f01eb73dfcdaull, 44, 44, 19}, {"Random39", 0, 0, 0x9bcdaebb98702055ull, 0x14650fb0739d0383ull, 0xc193386eb35a8e65ull, 0x3026f01eb73dfcdaull, 44, 0, 19}, {"Random39", 1, 1, 0x5fa98522adc27aafull, 0x50b22f98ba965993ull, 0xeea051f0749b0428ull, 0x123f0c5b09aeae91ull, 44, 44, 19}, {"Random39", 1, 0, 0x5fa98522adc27aafull, 0x14650fb0739d0383ull, 0xeea051f0749b0428ull, 0x123f0c5b09aeae91ull, 44, 0, 19}, - {"Random40", 0, 1, 0x869959795f89f7afull, 0xe9919376f3542ea7ull, 0x76e2d6a7f41b161cull, 0xd91533e6639e22f9ull, 77, 53, 18}, - {"Random40", 0, 0, 0x869959795f89f7afull, 0x14650fb0739d0383ull, 0x76e2d6a7f41b161cull, 0xd91533e6639e22f9ull, 77, 0, 18}, - {"Random40", 1, 1, 0xf47407130500a9bdull, 0xda9b4928c023ca07ull, 0x9a84d463fe03ec91ull, 0x5a8f1ec5a18f3731ull, 78, 53, 19}, - {"Random40", 1, 0, 0xf47407130500a9bdull, 0x14650fb0739d0383ull, 0x9a84d463fe03ec91ull, 0x5a8f1ec5a18f3731ull, 78, 0, 19}, - {"Random41", 0, 1, 0x6509ace183e7d2cdull, 0x957c5244d07600b9ull, 0x4b1ec3cc1bd0909cull, 0xcc1203a69591b134ull, 51, 40, 6}, - {"Random41", 0, 0, 0x6509ace183e7d2cdull, 0x14650fb0739d0383ull, 0x4b1ec3cc1bd0909cull, 0xcc1203a69591b134ull, 51, 0, 6}, - {"Random41", 1, 1, 0xd2d43ab5fdd2ef0bull, 0xb907bbe1bb2da083ull, 0x4b1ec3cc1bd0909cull, 0xc55d2d003a5bec69ull, 51, 40, 6}, - {"Random41", 1, 0, 0xd2d43ab5fdd2ef0bull, 0x14650fb0739d0383ull, 0x4b1ec3cc1bd0909cull, 0xc55d2d003a5bec69ull, 51, 0, 6}, - {"Random42", 0, 1, 0xa0ee848263988a3dull, 0x1f2ad0a937e24f39ull, 0x75b6953551fe9575ull, 0xb124b9f52e8cf9f2ull, 60, 60, 12}, - {"Random42", 0, 0, 0xa0ee848263988a3dull, 0x14650fb0739d0383ull, 0x75b6953551fe9575ull, 0xb124b9f52e8cf9f2ull, 60, 0, 12}, - {"Random42", 1, 1, 0x7f6282e75cc72bb0ull, 0x23bb6075acaedb2eull, 0xf736fbadc411c223ull, 0xdb2120d5edab4d9full, 60, 60, 12}, - {"Random42", 1, 0, 0x7f6282e75cc72bb0ull, 0x14650fb0739d0383ull, 0xf736fbadc411c223ull, 0xdb2120d5edab4d9full, 60, 0, 12}, + {"Random40", 0, 1, 0x869959795f89f7afull, 0xe9919376f3542ea7ull, 0x78c15a2f88a24807ull, 0xd91533e6639e22f9ull, 77, 53, 18}, + {"Random40", 0, 0, 0x869959795f89f7afull, 0x14650fb0739d0383ull, 0x78c15a2f88a24807ull, 0xd91533e6639e22f9ull, 77, 0, 18}, + {"Random40", 1, 1, 0xf47407130500a9bdull, 0xda9b4928c023ca07ull, 0x41975079a7cc855bull, 0x5a8f1ec5a18f3731ull, 78, 53, 19}, + {"Random40", 1, 0, 0xf47407130500a9bdull, 0x14650fb0739d0383ull, 0x41975079a7cc855bull, 0x5a8f1ec5a18f3731ull, 78, 0, 19}, + {"Random41", 0, 1, 0x6509ace183e7d2cdull, 0x957c5244d07600b9ull, 0xf6e95f1e1a0264bcull, 0xcc1203a69591b134ull, 51, 40, 6}, + {"Random41", 0, 0, 0x6509ace183e7d2cdull, 0x14650fb0739d0383ull, 0xf6e95f1e1a0264bcull, 0xcc1203a69591b134ull, 51, 0, 6}, + {"Random41", 1, 1, 0xd2d43ab5fdd2ef0bull, 0xb907bbe1bb2da083ull, 0xf6e95f1e1a0264bcull, 0xc55d2d003a5bec69ull, 51, 40, 6}, + {"Random41", 1, 0, 0xd2d43ab5fdd2ef0bull, 0x14650fb0739d0383ull, 0xf6e95f1e1a0264bcull, 0xc55d2d003a5bec69ull, 51, 0, 6}, + {"Random42", 0, 1, 0xa0ee848263988a3dull, 0x1f2ad0a937e24f39ull, 0xd4d5dd88f1bc7129ull, 0xb124b9f52e8cf9f2ull, 60, 60, 12}, + {"Random42", 0, 0, 0xa0ee848263988a3dull, 0x14650fb0739d0383ull, 0xd4d5dd88f1bc7129ull, 0xb124b9f52e8cf9f2ull, 60, 0, 12}, + {"Random42", 1, 1, 0x7f6282e75cc72bb0ull, 0x23bb6075acaedb2eull, 0x292b9553961fa163ull, 0xdb2120d5edab4d9full, 60, 60, 12}, + {"Random42", 1, 0, 0x7f6282e75cc72bb0ull, 0x14650fb0739d0383ull, 0x292b9553961fa163ull, 0xdb2120d5edab4d9full, 60, 0, 12}, {"Random43", 0, 1, 0x7fb8967d6d6d24d2ull, 0x98cae92178fe91ebull, 0xbd8ba613082e0da5ull, 0xdc29a310e9c7d06bull, 63, 63, 14}, {"Random43", 0, 0, 0x7fb8967d6d6d24d2ull, 0x14650fb0739d0383ull, 0xbd8ba613082e0da5ull, 0xdc29a310e9c7d06bull, 63, 0, 14}, {"Random43", 1, 1, 0xffd1c55ce888ef72ull, 0x8ac3c2c1053a4819ull, 0x572d3f9f9521e7caull, 0x45393018e93b32beull, 63, 63, 14}, {"Random43", 1, 0, 0xffd1c55ce888ef72ull, 0x14650fb0739d0383ull, 0x572d3f9f9521e7caull, 0x45393018e93b32beull, 63, 0, 14}, - {"Random44", 0, 1, 0xdae57d80c4d2a634ull, 0xf7669a1535370da9ull, 0x513c7ae44c865ec8ull, 0xc555f0e88b5eb378ull, 69, 47, 15}, - {"Random44", 0, 0, 0xdae57d80c4d2a634ull, 0x14650fb0739d0383ull, 0x513c7ae44c865ec8ull, 0xc555f0e88b5eb378ull, 69, 0, 15}, - {"Random44", 1, 1, 0x360b704190f157adull, 0x5b2a75cdabd7235cull, 0x1a79f44215a3981eull, 0xde36f7f290af13a5ull, 69, 47, 15}, - {"Random44", 1, 0, 0x360b704190f157adull, 0x14650fb0739d0383ull, 0x1a79f44215a3981eull, 0xde36f7f290af13a5ull, 69, 0, 15}, - {"Random45", 0, 1, 0xbf351c50f4a44f8full, 0xfee900af4573bbc1ull, 0xf543670d7cc61817ull, 0x7f2867c87c78723eull, 99, 63, 25}, - {"Random45", 0, 0, 0xbf351c50f4a44f8full, 0x14650fb0739d0383ull, 0xf543670d7cc61817ull, 0x7f2867c87c78723eull, 99, 0, 25}, - {"Random45", 1, 1, 0x32a1d85e85b93f0aull, 0xebcfcdf78cd848a5ull, 0xa2aef1f38f464b4dull, 0x56b0e295081820c0ull, 99, 63, 24}, - {"Random45", 1, 0, 0x32a1d85e85b93f0aull, 0x14650fb0739d0383ull, 0xa2aef1f38f464b4dull, 0x56b0e295081820c0ull, 99, 0, 24}, + {"Random44", 0, 1, 0xdae57d80c4d2a634ull, 0xf7669a1535370da9ull, 0x316199af1a335dbfull, 0xc555f0e88b5eb378ull, 69, 47, 15}, + {"Random44", 0, 0, 0xdae57d80c4d2a634ull, 0x14650fb0739d0383ull, 0x316199af1a335dbfull, 0xc555f0e88b5eb378ull, 69, 0, 15}, + {"Random44", 1, 1, 0x360b704190f157adull, 0x5b2a75cdabd7235cull, 0x9f0e22f8fb2632f6ull, 0xde36f7f290af13a5ull, 69, 47, 15}, + {"Random44", 1, 0, 0x360b704190f157adull, 0x14650fb0739d0383ull, 0x9f0e22f8fb2632f6ull, 0xde36f7f290af13a5ull, 69, 0, 15}, + {"Random45", 0, 1, 0xbf351c50f4a44f8full, 0xfee900af4573bbc1ull, 0xf7b0f07cda17c110ull, 0x7f2867c87c78723eull, 99, 63, 25}, + {"Random45", 0, 0, 0xbf351c50f4a44f8full, 0x14650fb0739d0383ull, 0xf7b0f07cda17c110ull, 0x7f2867c87c78723eull, 99, 0, 25}, + {"Random45", 1, 1, 0x32a1d85e85b93f0aull, 0xebcfcdf78cd848a5ull, 0x797aff1ec9239dffull, 0x56b0e295081820c0ull, 99, 63, 24}, + {"Random45", 1, 0, 0x32a1d85e85b93f0aull, 0x14650fb0739d0383ull, 0x797aff1ec9239dffull, 0x56b0e295081820c0ull, 99, 0, 24}, {"Random46", 0, 1, 0x3ad713968f2546d9ull, 0x84dd0930114cd829ull, 0x8fe7667934a81f87ull, 0x6bb382cfb976e148ull, 52, 52, 1}, {"Random46", 0, 0, 0x3ad713968f2546d9ull, 0x14650fb0739d0383ull, 0x8fe7667934a81f87ull, 0x6bb382cfb976e148ull, 52, 0, 1}, {"Random46", 1, 1, 0x997f2e0ee9786d91ull, 0x1ce86cd2c755bea3ull, 0x8fe7667934a81f87ull, 0x556134f8585397c9ull, 52, 52, 1}, @@ -1242,8 +1388,8 @@ constexpr Pinned kPinned[] = { {"Random47", 0, 0, 0x8ce025f147bbdb2dull, 0x14650fb0739d0383ull, 0x072cedccf12d6a31ull, 0x31ec37588708d5f1ull, 57, 0, 13}, {"Random47", 1, 1, 0xebf0544eb982483aull, 0xca410d7365dca790ull, 0x33811b67f07874f3ull, 0xa8aff582382caf48ull, 57, 57, 13}, {"Random47", 1, 0, 0xebf0544eb982483aull, 0x14650fb0739d0383ull, 0x33811b67f07874f3ull, 0xa8aff582382caf48ull, 57, 0, 13}, - {"Random48", 0, 1, 0xaa6b18e4254e2f68ull, 0x2f80186c69547a10ull, 0x62a7be3a970dee6aull, 0xaff43b38e6bc02acull, 48, 48, 9}, - {"Random48", 0, 0, 0xaa6b18e4254e2f68ull, 0x14650fb0739d0383ull, 0x62a7be3a970dee6aull, 0xaff43b38e6bc02acull, 48, 0, 9}, - {"Random48", 1, 1, 0x655a5637ae415d60ull, 0x13866e45be14eaeeull, 0xee7c0d61bfb7dd62ull, 0xa0e205aa768a0bc9ull, 48, 48, 9}, - {"Random48", 1, 0, 0x655a5637ae415d60ull, 0x14650fb0739d0383ull, 0xee7c0d61bfb7dd62ull, 0xa0e205aa768a0bc9ull, 48, 0, 9}, + {"Random48", 0, 1, 0xaa6b18e4254e2f68ull, 0x2f80186c69547a10ull, 0xda74ceb1a9e7d904ull, 0xaff43b38e6bc02acull, 48, 48, 8}, + {"Random48", 0, 0, 0xaa6b18e4254e2f68ull, 0x14650fb0739d0383ull, 0xda74ceb1a9e7d904ull, 0xaff43b38e6bc02acull, 48, 0, 8}, + {"Random48", 1, 1, 0x655a5637ae415d60ull, 0x13866e45be14eaeeull, 0x0876fbb01d747e2dull, 0xa0e205aa768a0bc9ull, 48, 48, 8}, + {"Random48", 1, 0, 0x655a5637ae415d60ull, 0x14650fb0739d0383ull, 0x0876fbb01d747e2dull, 0xa0e205aa768a0bc9ull, 48, 0, 8}, }; diff --git a/tests/test_aggregated_path_regressions.cpp b/tests/test_aggregated_path_regressions.cpp index 962b650f..47dfb591 100644 --- a/tests/test_aggregated_path_regressions.cpp +++ b/tests/test_aggregated_path_regressions.cpp @@ -457,9 +457,34 @@ void test_aggregated_equals_chart() { CHECK(agg.net_profit == chart.net_profit); CHECK(agg.closed.size() == chart.closed.size()); CHECK(agg.open_count == chart.open_count); + // expectation corrected (R5 lane H-THIN, E19: the kernel samples the + // Pine host's lots; the host-owned model is gone): every row agrees in + // every column but two runups. In the pyramiding-1 and pyramiding-3 + // runs without process_orders_on_close or calc_on_order_fills, row 1 + // (the ML market entry at bar 7's 11.575 open, filled 11.58, and its + // MX take-profit at 11.66 on the same bar) books the same fills on both + // routes, but the chart route fills the take-profit at its half-tick + // crossing on the low -> high leg while the aggregated route fills it + // at the start of the high -> close leg, after the 11.665 high: the + // lot lives through that high on the aggregated route, and the kernel + // sampler books it (runup +0.50 = (11.665 - 11.66) * 100). That is the + // aggregated route's own fill chronology, which the host model's + // chart-bar walk hid; E19 does not move it (recorded in the lane + // report). Before E19 the two routes agreed on every column here. + auto same_but_runup = [](Row a, Row b) { + a.runup = b.runup = 0.0; + return a == b; + }; + const bool route_timing_variant = !v.pooc && !v.coof; bool rows_equal = agg.closed.size() == chart.closed.size(); - for (std::size_t i = 0; rows_equal && i < agg.closed.size(); ++i) - rows_equal = agg.closed[i] == chart.closed[i]; + for (std::size_t i = 0; rows_equal && i < agg.closed.size(); ++i) { + if (route_timing_variant && i == 1) { + rows_equal = same_but_runup(agg.closed[i], chart.closed[i]) + && std::fabs(agg.closed[i].runup - (chart.closed[i].runup + 0.5)) < 1e-9; + } else { + rows_equal = agg.closed[i] == chart.closed[i]; + } + } CHECK(rows_equal); if (!rows_equal) { std::printf(" pooc=%d coof=%d slippage=%d pyramiding=%d\n", v.pooc, v.coof, diff --git a/tests/test_e19_excursion_path_order.cpp b/tests/test_e19_excursion_path_order.cpp index af07c9b8..6576cbe3 100644 --- a/tests/test_e19_excursion_path_order.cpp +++ b/tests/test_e19_excursion_path_order.cpp @@ -24,14 +24,17 @@ // 3. the AUTO control: the same shape with no declared order. The seam // answers the open-proximity rule, so this row is the same before and // after the fix — the byte-identity of the default, at test scope. -// 4. the margin-call prefix sample, reached directly with owner facts (the -// l11b pattern) after a forced-order run has configured the spec. The -// prefix walk stops at the liquidation waypoint, so which extremes it -// has already traversed is the declared order's question too. // // Fills are unchanged by the fix in every row: the matcher has always walked // the declared order (`NativeExecutionConsumer`'s own resolution). Only the // excursion columns move. +// +// R5 lane H-THIN (E19): the Pine host no longer owns lot excursions; the +// kernel's sampler (NativeExecutionConsumer::apply_excursion) samples every +// point of the path it walks, in the order the run declares, so rows 1-3 now +// read the kernel's numbers, which are these. The former row 4 reached the +// host's margin-call prefix walk directly with owner facts; that walk left the +// source layer with the model, so the row went with it. #include #include @@ -109,24 +112,6 @@ class SameBarBracket : public source::PineStrategyHost { double exit_stop_; }; -// The l11b DeclaresOwnership pattern: reach the owner's excursion model -// directly with chosen facts. The run below is what puts the declared order -// into the consumer's spec, which is where the seam reads it from. -class MarginPrefixProbe : public source::PineStrategyHost { -public: - explicit MarginPrefixProbe(int path_order_mode) { - configure_pine_strategy(cfg()); - if (path_order_mode != 0) set_path_order(path_order_mode); - } - void on_source_bar(const Bar&) override {} - void arm_margin_prefix(const Bar& bar) { - current_bar_ = bar; - excursion_margin_call_ = true; - excursion_margin_prefix_ = true; - excursion_margin_fill_only_ = false; - } -}; - void expect_row(const char* tag, const Trade& t, bool is_long, double entry_px, double exit_px, double fav, double adv) { std::printf("%s %s @%.2f->%.2f mfe=%.6f mae=%.6f (want @%.2f->%.2f mfe=%.6f mae=%.6f)\n", @@ -176,48 +161,6 @@ int main() { run_bracket("auto-long-take-profit", 0, kLowFirstBar, true, 99.00, 112.00, kNaN, 100.00, 112.00, 12.00, 10.00); - // 4. The margin-call prefix sample. A long lot opened at 100.00 and - // liquidated at the 90.00 low of the same bar: the prefix the slice - // inherits is the traversed waypoints, so under the declared - // HIGH_FIRST the 115.00 high is already behind the fire price and the - // slice's favorable excursion is 15.00; the open-proximity frame would - // reach only the open. Adverse is the low either way. - ClosedLotExcursionFacts facts{}; - facts.entry_bar_index = 1; - facts.exit_bar_index = 1; - facts.is_long = true; - facts.entry_price = 100.00; - facts.fill_price = 90.00; - facts.lot_qty = 1.0; - facts.closed_qty = 1.0; - { - MarginPrefixProbe host(1); - const std::vector bars = {kFlatBefore, kFlatAfter}; - host.run(bars.data(), static_cast(bars.size())); - CHECK(host.last_error().empty()); - host.arm_margin_prefix(kLowFirstBar); - const ClosedLotExcursion owned = host.closed_lot_excursion(facts); - std::printf("forced-high-first-margin-prefix fav=%.6f adv=%.6f (want fav=15.000000 adv=10.000000)\n", - owned.favorable, owned.adverse); - CHECK(near(owned.favorable, 15.00)); - CHECK(near(owned.adverse, 10.00)); - } - { - // The AUTO control of row 4: the same bar, no declared order. The - // prefix stops at the low, which IS the first waypoint under the - // open-proximity rule, so nothing but the open precedes it. - MarginPrefixProbe host(0); - const std::vector bars = {kFlatBefore, kFlatAfter}; - host.run(bars.data(), static_cast(bars.size())); - CHECK(host.last_error().empty()); - host.arm_margin_prefix(kLowFirstBar); - const ClosedLotExcursion owned = host.closed_lot_excursion(facts); - std::printf("auto-margin-prefix fav=%.6f adv=%.6f (want fav=0.000000 adv=10.000000)\n", - owned.favorable, owned.adverse); - CHECK(near(owned.favorable, 0.00)); - CHECK(near(owned.adverse, 10.00)); - } - // 5. R5 lane F7 (audit M25): the source layer ASKS the kernel for the leg // order instead of keeping copies of the rule. Rows 1-4 pin that the // forced orders reach the adapter; this pins where the answer comes diff --git a/tests/test_e19_excursion_tape.cpp b/tests/test_e19_excursion_tape.cpp index fbc05a37..b53cf433 100644 --- a/tests/test_e19_excursion_tape.cpp +++ b/tests/test_e19_excursion_tape.cpp @@ -49,9 +49,18 @@ // entry bar into the lot (ab9714be pine_scheduler.cpp:357-366's // update_per_trade_extremes on every fill recalculation). // -// The adapter columns are pinned as MEASURED DIVERGENCES: a lane that moves -// the Pine host onto the kernel sampler, or corrects the adapter's masks, -// flips them to TradingView's value and must re-pin this row. +// The adapter columns were pinned as MEASURED DIVERGENCES, to flip when a lane +// moved the Pine host onto the kernel sampler. +// +// expectation corrected (R5 lane H-THIN, E19): that lane is this one. The Pine +// host no longer owns lot excursions (owns_lot_excursions is the kernel's +// default, false), so the kernel samples its lots and the adapter columns are +// TradingView's wherever the kernel's are: classes A and B and all four COOF +// scratches flip to the tape (adapter == TradingView 6 -> 10 of 12, 0 -> 4 of +// 4). Class C stays off the tape on both sides, and the Pine run shows the +// kernel sampler's reading under the adapter's per-kind tick rules: the +// stop's half-tick crossing, 13.645 / 7.785 (was 13.64 / 7.78), 0.005 closer +// to TradingView's 13.65 / 7.79. #include #include #include @@ -185,13 +194,13 @@ const Measured kMeasured[] = { {29, 4.98, 10.37, 4.98, 10.37, "control"}, {30, 0.32, 12.67, 0.32, 12.67, "control"}, {31, 3.58, 25.21, 3.58, 25.21, "control"}, - {32, 27.07, 0.00, 27.07, 3.22, "A entry-bar over-mask (adverse)"}, - {169, 0.14, 16.35, 0.14, 13.75, "B exit-bar over-fold (adverse)"}, - {170, 11.55, 3.44, 11.55, 0.43, "B exit-bar over-fold (adverse)"}, - {171, 1.47, 13.62, 6.97, 13.62, "A entry-bar over-mask (favorable)"}, + {32, 27.07, 3.22, 27.07, 3.22, "A entry-bar over-mask (adverse)"}, + {169, 0.14, 13.75, 0.14, 13.75, "B exit-bar over-fold (adverse)"}, + {170, 11.55, 0.43, 11.55, 0.43, "B exit-bar over-fold (adverse)"}, + {171, 6.97, 13.62, 6.97, 13.62, "A entry-bar over-mask (favorable)"}, {172, 4.50, 21.77, 4.50, 21.77, "control"}, - {797, 13.64, 5.77, 13.64, 5.77, "C open print before a same-bar stop fill"}, - {798, 7.78, 6.01, 7.78, 6.01, "C open print before a same-bar stop fill"}, + {797, 13.645, 5.77, 13.64, 5.77, "C open print before a same-bar stop fill"}, + {798, 7.785, 6.01, 7.78, 6.01, "C open print before a same-bar stop fill"}, {799, 5.69, 22.27, 5.69, 22.27, "control"}, {800, 3.08, 24.92, 3.08, 24.92, "control"}, }; @@ -241,8 +250,10 @@ class KernelScratch final : public NativeStrategyHost { void test_coof_scratch() { const std::vector days[] = {day_bars(kCoofDay0), day_bars(kCoofDay1), day_bars(kCoofDay2), day_bars(kCoofDay3)}; - // The adapter's magnitudes, pinned (the entry bar's extremes folded in). - const double adapter[4][2] = {{4.08, 4.21}, {11.75, 0.0}, {0.0, 11.19}, {6.44, 0.22}}; + // The adapter's magnitudes, pinned: the kernel's, 0 / 0 (were the entry + // bar's extremes folded in: {4.08, 4.21}, {11.75, 0.0}, {0.0, 11.19}, + // {6.44, 0.22}). + const double adapter[4][2] = {{0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}}; int adapter_tv = 0, kernel_tv = 0; for (const TapeRow& tv : kCoofTape) { const int d = tv.day; @@ -279,11 +290,11 @@ void test_coof_scratch() { a_is_tv ? " =TV" : " !=TV", x.max_runup, x.max_drawdown, k_is_tv ? " =TV" : " !=TV"); CHECK(near(a.max_runup, adapter[d][0])); CHECK(near(a.max_drawdown, adapter[d][1])); - CHECK(!a_is_tv); + CHECK(a_is_tv); CHECK(k_is_tv); } std::printf("coof scratch rows 4: adapter == TradingView %d, kernel == TradingView %d\n", adapter_tv, kernel_tv); - CHECK(adapter_tv == 0); + CHECK(adapter_tv == 4); CHECK(kernel_tv == 4); } @@ -357,16 +368,16 @@ int main() { CHECK(!a_is_tv); CHECK(!k_is_tv); } else { - // Classes A and B: the kernel sampler is TradingView's, the - // adapter's host-owned model is not. - CHECK(!a_is_tv); + // Classes A and B: the kernel sampler is TradingView's, and so is + // the adapter since it reads the kernel's (was CHECK(!a_is_tv)). + CHECK(a_is_tv); CHECK(k_is_tv); } } CHECK(n == 12); std::printf("tape rows %d: adapter == TradingView %d, kernel == TradingView %d\n", n, agree_adapter, agree_kernel); - CHECK(agree_adapter == 6); + CHECK(agree_adapter == 10); CHECK(agree_kernel == 10); test_coof_scratch(); std::printf("test_e19_excursion_tape: %d passed, %d failed\n", passed, failed); diff --git a/tests/test_l10b_excursion_parity.cpp b/tests/test_l10b_excursion_parity.cpp index e952c754..c7045e49 100644 --- a/tests/test_l10b_excursion_parity.cpp +++ b/tests/test_l10b_excursion_parity.cpp @@ -139,6 +139,12 @@ int main() { // Legacy trade #3: Margin call 1 @1606.17, favorableUsd 27.34 // (exit-bar high 1619.86 - 1592.52), adverseUsd 0. Native without // the full-bar pre-margin sample reports 21.28 (entry-bar high only). + // expectation corrected (R5 lane H-THIN, E19: the kernel samples the Pine + // host's lots; the host-owned model is gone): + // 27.34 -> 21.28, which is TradingView's own number for this row + // (corpus tape anomaly-equity-mirror-strategy-equity-01 trade #3: + // Favorable excursion 21.28, Adverse 0); ab9714be's 27.34 folded an + // extreme after the liquidation. MirrorLong host(992399.54089, 623.163); auto bars = bars_0421(); host.run(bars.data(), static_cast(bars.size())); @@ -148,7 +154,7 @@ int main() { const auto& t = host.get_trade(0); CHECK(t.exit_comment == std::string("Margin call")); expect_trade("0421-mc", t, "E", true, 1.0, 1592.52, 1606.17, - 27.34, 0.0, 13.65); + 21.28, 0.0, 13.65); } if (host.trade_count() >= 2) { const auto& t = host.get_trade(1); @@ -164,6 +170,13 @@ int main() { // Legacy samples the bar low (1488.21) before process_margin_call // and scales that extreme to the closed slice (ab9714be output // of this excerpt: mfe=(1501.56-1488.21)*qty). + // expectation corrected (R5 lane H-THIN, E19: the kernel samples the Pine + // host's lots; the host-owned model is gone): + // the high-first walk reaches the liquidation at the high before the + // 1488.21 low, so no favorable price belongs to the slice (mfe 0, + // was (1501.56-1488.21)*qty); TradingView folds no extreme after a + // closing fill (tests/test_e19_excursion_tape.cpp, class B, and the + // row above). No tape of this excerpt exists. const double capital = 7.7232 * 1501.56; ShortPct host(capital); auto bars = bars_h16_short(); @@ -172,7 +185,7 @@ int main() { CHECK(host.trade_count() >= 1); if (host.trade_count() >= 1) { const auto& t = host.get_trade(0); - const double fav = (1501.56 - 1488.21) * t.qty; + const double fav = 0.0; const double adv = (1503.74 - 1501.56) * t.qty; CHECK(t.exit_comment == std::string("Margin call")); expect_trade("h16-short", t, "S", false, t.qty, 1501.56, 1503.74, diff --git a/tests/test_l10d_entry_bar_excursion_masks.cpp b/tests/test_l10d_entry_bar_excursion_masks.cpp index 305a086c..0a7453dc 100644 --- a/tests/test_l10d_entry_bar_excursion_masks.cpp +++ b/tests/test_l10d_entry_bar_excursion_masks.cpp @@ -2,6 +2,14 @@ // reached BEFORE the fill (ab9714be pine_fills.cpp:42 skip_entry_bar_high/low // + pine_risk.cpp:248-300). Literals are the legacy corpus engine_trades.csv // rows that first diverge on 890da75. +// +// expectation corrected (R5 lane H-THIN, E19: the kernel samples the Pine +// host's lots; the host-owned model is gone): every excursion literal below is +// now TradingView's own, read off each probe's corpus tape (tv_trades.csv, +// Favorable / Adverse excursion). ab9714be's entry-bar masks hid a favorable +// extreme the fill had already passed (#46 0.00 -> 0.86, #29 0.00 -> 0.98, +// #60 0.00 -> 1.33, #11 0.00 -> 6.78) and folded an exit-bar extreme after an +// at-open exit (#22 adverse 5.53 -> 0.90); fills and prices are unchanged. #include #include @@ -219,7 +227,7 @@ int main() { CHECK(host.trade_count() >= 1); if (host.trade_count() >= 1) expect_trade("dual-stop-open-tie#46", host.get_trade(0), false, - 1691.11, 1691.13, 0.0, 0.02); + 1691.11, 1691.13, 0.86, 0.02); } { GapShort host; @@ -233,7 +241,7 @@ int main() { CHECK(host.trade_count() >= 1); if (host.trade_count() >= 1) expect_trade("deferred-flip-gap-stops#29", host.get_trade(0), false, - 1630.11, 1661.93, 0.0, 61.46); + 1630.11, 1661.93, 0.98, 61.46); } } { @@ -248,7 +256,7 @@ int main() { CHECK(host.trade_count() >= 1); if (host.trade_count() >= 1) expect_trade("stop-entry-touch-boundary#60", host.get_trade(0), false, - 2413.99, 2467.86, 0.0, 98.01); + 2413.99, 2467.86, 1.33, 98.01); } } { @@ -266,7 +274,7 @@ int main() { CHECK(host.trade_count() >= 1); if (host.trade_count() >= 1) expect_trade("composite-bracket-cap#11", host.get_trade(0), false, - 1812.34, 1819.23, 0.0, 8.42); + 1812.34, 1819.23, 6.78, 8.42); } { RangeLong host; @@ -280,7 +288,7 @@ int main() { CHECK(host.trade_count() >= 1); if (host.trade_count() >= 1) expect_trade("range-expansion-pending-stop#22", host.get_trade(0), true, - 1811.09, 1812.34, 33.88, 5.53); + 1811.09, 1812.34, 33.88, 0.90); } } std::printf("test_l10d_entry_bar_excursion_masks: %d passed, %d failed\n", diff --git a/tests/test_l10j_corpus_parity.cpp b/tests/test_l10j_corpus_parity.cpp index 035fa04e..f1a9b67c 100644 --- a/tests/test_l10j_corpus_parity.cpp +++ b/tests/test_l10j_corpus_parity.cpp @@ -183,8 +183,12 @@ int main() { CHECK(host.last_error().empty()); CHECK(host.trade_count() == 1); if (host.trade_count() >= 1) { + // expectation corrected (R5 lane H-THIN, E19: the kernel samples + // the Pine host's lots; the host-owned model is gone): favorable + // 11.18 -> 10.66, TradingView's own number (corpus tape + // bracket-exit-stop-limit-trail-same-bar-01 trade #466). expect_trade("bracket-exit-stop-limit-trail#466", host.get_trade(0), - false, 3155.66, 3148.74, 6.92, 11.18, 12.23); + false, 3155.66, 3148.74, 6.92, 10.66, 12.23); } } diff --git a/tests/test_l10k_corpus_parity.cpp b/tests/test_l10k_corpus_parity.cpp index bc2b75f9..75d64da8 100644 --- a/tests/test_l10k_corpus_parity.cpp +++ b/tests/test_l10k_corpus_parity.cpp @@ -1,7 +1,9 @@ // R4-D L10k: dual-stop both-touch fills the older book stop first // (order-dual-stop-both-touch-priority-01 #34/#35), and // order-stop-entry-reversal-grouping-01 replays identical to ab9714be -// (the host owns excursion accounting, so the half-tick residual is gone). +// (the host owned excursion accounting, so the half-tick residual was gone; +// since R5 lane H-THIN the kernel samples the Pine host's lots and it is +// back, see #801 below). // Bars are embedded from corpus/data/derived/ohlcv_ETH-USDT-USDT_15m.csv — // this test must never open corpus files (CI has no corpus checkout). #include "l4a_native_route_guard.hpp" @@ -142,8 +144,16 @@ int main() { expect_trade("dual-stop#34", host.get_trade(0), true, 1600.84, 1583.64, 14.11, 17.20, -17.20); if (host.trade_count() >= 2) + // expectation corrected (R5 lane H-THIN, E19: the kernel samples + // the Pine host's lots; the host-owned model is gone): favorable + // 18.59 -> 16.00. The engine books LE2's 1596.36 fill after LE's + // and after the entry bar's 1614.95 high (its fill order on this + // bar, unchanged by E19), so the lot's best price is the 17:15 + // high 1612.36. TradingView fills LE2 first and closes it with + // another exit (tape trade #34, 18.59 to 16:15), so no tape row + // grades this one. expect_trade("dual-stop#35", host.get_trade(1), true, - 1596.36, 1576.62, 18.59, 58.80, -19.74); + 1596.36, 1576.62, 16.00, 58.80, -19.74); } { StopReversal host; @@ -164,7 +174,14 @@ int main() { const auto& t = host.get_trade(i); if (t.is_long && near(t.entry_price, 3912.15) && near(t.exit_price, 3925.79)) { found = true; - expect_trade("reversal#801", t, true, 3912.15, 3925.79, 13.64, 5.77, 13.64); + // expectation corrected (R5 lane H-THIN, E19: the kernel + // samples the Pine host's lots): favorable 13.64 -> 13.645, + // the kernel's sample at the stop's half-tick crossing + // (3925.795) under this run's per-kind tick rules. TradingView + // books 13.65 (corpus tape trade #801: it counts the exit + // bar's 3925.80 open print, which neither model samples), so + // the row moves 0.005 closer to the tape and is not its number. + expect_trade("reversal#801", t, true, 3912.15, 3925.79, 13.645, 5.77, 13.64); break; } } diff --git a/tests/test_l10l_corpus_parity.cpp b/tests/test_l10l_corpus_parity.cpp index 55efd349..371fe1a9 100644 --- a/tests/test_l10l_corpus_parity.cpp +++ b/tests/test_l10l_corpus_parity.cpp @@ -217,7 +217,11 @@ void test_oca_reduce_open_gap_excursion() { CHECK(near(t.exit_price, 1583.34)); CHECK(near(t.qty, 1.0)); CHECK(near(t.pnl, 1.880000)); - CHECK(near(t.max_runup, 3.370000)); + // expectation corrected (R5 lane H-THIN, E19: the kernel samples the + // Pine host's lots; the host-owned model is gone): favorable 3.37 -> + // 2.33, TradingView's own number (corpus tape + // bracket-tp-sl-oca-reduce-isolate-01 trade #117). + CHECK(near(t.max_runup, 2.330000)); CHECK(near(t.max_drawdown, 2.080000)); } } diff --git a/tests/test_l10y_full_equity_entry_split.cpp b/tests/test_l10y_full_equity_entry_split.cpp index 2a58c0c4..e903ed70 100644 --- a/tests/test_l10y_full_equity_entry_split.cpp +++ b/tests/test_l10y_full_equity_entry_split.cpp @@ -1,11 +1,19 @@ // R4-D L10y: a default 100%-of-equity opening that the legacy owner (ab9714be) // ADMITS and books as a main lot plus a small residual lot closed again on the // entry bar must replay the same way on the switched route instead of being -// refused as unaffordable, and the split-off residual must inherit the entry -// bar's COMPLETE H/L excursion sample (pine_scheduler.cpp:257/:363 runs -// update_per_trade_extremes() over the full script bar before the non-POOC -// end-of-bar opening margin trim; only the POOC pre-script pass samples the -// traversed waypoint prefix, pine_fills.cpp:2014-2023). +// refused as unaffordable. The legacy owner also gave the split-off residual +// the entry bar's COMPLETE H/L excursion sample (pine_scheduler.cpp:257/:363 +// runs update_per_trade_extremes() over the full script bar before the +// non-POOC end-of-bar opening margin trim). +// +// expectation corrected (R5 lane H-THIN, E19: the kernel samples the Pine +// host's lots; the host-owned model is gone): TradingView books that residual +// with its fill alone -- favorable 0, adverse the fill's loss plus the entry +// commission -- as the `lab tv` tape tests/fixtures/e19_allin_trim shows on +// this very bar (hthin-e19-allin-trim trade #1: 1812.35 -> 1812.31 margin +// call, favorable 0, adverse -(0.04 + 0.0002 * 1812.35) * 1 = -0.40247), and +// so does the kernel sampler. The residual's rows below are that rule; the +// main lot's are unchanged. // // Shape pinned from corpus/validation/zz-pop-yukozb-gold-ny-orb-v19-close-20-00 // owner rows #2/#3: Entry long 2025-04-04 15:15 @1812.35 is booked as a @@ -156,20 +164,16 @@ void test_all_in_entry_splits_and_residual_sees_full_bar() { CHECK(main_lot.exit_time == bars[3].timestamp); CHECK(main_lot.qty > residual.qty); - // --- the L10y excursion rule: the FULL entry bar, not the open prefix -- - // owner model: (H - entry) * qty and (entry - L) * qty, sampled before the - // non-POOC end-of-bar opening trim. The reverted submit-time prefix - // sample printed max_runup == 0 here. - // The owner's report folds the per-side commission (0.02% of notional) - // into the excursion, so the pinned products carry the same shift: - // fav = ((H - entry) - 0.0002*entry) * qty, adv = ((entry - L) + 0.0002*entry) * qty - // (owner row #2: ((1820.18-1812.35)-0.0002*1812.35)*0.00012464 = 0.000931). + // --- the residual's excursion: the fill alone (TradingView's rule) ---- + // The report folds the per-side commission (0.02% of notional) into the + // excursion: favorable max(0, 0 - fee) = 0, adverse (fill loss + fee) * qty. + // (was the owner's full entry bar: ((H - entry) - fee) * qty and + // ((entry - L) + fee) * qty, and max_runup > 0.) const double fee_px = 0.0002 * 1812.35; - const double full_bar_runup = (1820.18 - 1812.35 - fee_px) * residual.qty; - const double full_bar_drawdown = (1812.35 - 1800.63 + fee_px) * residual.qty; - CHECK(near(residual.max_runup, full_bar_runup, 1e-9)); - CHECK(near(residual.max_drawdown, full_bar_drawdown, 1e-9)); - CHECK(residual.max_runup > 0.0); + const double fill_only_drawdown = (1812.35 - 1812.31 + fee_px) * residual.qty; + CHECK(near(residual.max_runup, 0.0, 1e-9)); + CHECK(near(residual.max_drawdown, fill_only_drawdown, 1e-9)); + CHECK(residual.max_runup == 0.0); // The main lot is sampled by the ordinary per-bar walk over the same bar. CHECK(near(main_lot.max_runup, (1820.18 - 1812.35 - fee_px) * main_lot.qty, 1e-6)); CHECK(near(main_lot.max_drawdown, (1812.35 - 1786.40 + fee_px) * main_lot.qty, 1e-6)); diff --git a/tests/test_l11a_host_excursion_twin.cpp b/tests/test_l11a_host_excursion_twin.cpp index 4b027590..a8cc151a 100644 --- a/tests/test_l11a_host_excursion_twin.cpp +++ b/tests/test_l11a_host_excursion_twin.cpp @@ -1,11 +1,18 @@ -// R4-D L11a (RULING A48), the source-host TWIN: the source host owns per-lot +// R4-D L11a (RULING A48), the source-host TWIN: the source host owned per-lot // excursion accounting behind ONE generic kernel capability. The kernel no -// longer samples the lot at the matched trigger price and no longer folds -// bar-path extremes into the closing row; both magnitudes come from the -// host's own sampler. Pins the owner rows around the first divergence of +// longer sampled the lot at the matched trigger price and no longer folded +// bar-path extremes into the closing row; both magnitudes came from the +// host's own sampler. It pinned the owner rows around the first divergence of // order-stop-entry-reversal-grouping-01 (favorable 13.64, exact — the -// half-tick kernel overshoot 13.645 is gone) and the entry-bar mask rows of +// half-tick kernel overshoot 13.645 was gone) and the entry-bar mask rows of // the composite-scalping shape (same-bar priced entry + priced exit). +// +// R5 lane H-THIN (E19) retired the host model: measured against TradingView +// (tests/test_e19_excursion_tape.cpp and the corpus, 315 of the 326 trades +// where the two differ go to the kernel, none to the host) it was ab9714be's, +// not TradingView's. The Pine host now declares no ownership and the kernel +// samples its lots, so this twin pins the kernel's numbers on the same rows; +// each row says what moved and why. // Bars are embedded; this test must never open corpus files. This half binds // pineforge/source, so tests/CMakeLists.txt registers it only when // PINEFORGE_BUILD_SOURCE_LAYER is ON; the kernel half (a bare host and an @@ -89,25 +96,11 @@ class SameBarScalp : public source::PineStrategyHost { int main() { { + // expectation corrected (R5 lane H-THIN, E19): the Pine host no longer + // owns lot excursions (was CHECK(host.owns_lot_excursions()) and the + // owner model's four magnitudes on hand-made facts). DeclaresOwnership host; - CHECK(host.owns_lot_excursions()); - ClosedLotExcursionFacts facts; - facts.carried_favorable = 4.0; - facts.carried_adverse = 2.0; - facts.entry_price = 100.0; - facts.fill_price = 103.0; - facts.lot_qty = 1.0; - facts.closed_qty = 1.0; - facts.is_long = true; - // The supplier is the host's own model: carried extremes scaled to the - // closed slice, with the exit fill itself always inside the trade. - const ClosedLotExcursion owned = host.closed_lot_excursion(facts); - CHECK(near(owned.favorable, 4.0)); - CHECK(near(owned.adverse, 2.0)); - facts.closed_qty = 0.5; - const ClosedLotExcursion half = host.closed_lot_excursion(facts); - CHECK(near(half.favorable, 2.0)); - CHECK(near(half.adverse, 1.0)); + CHECK(!host.owns_lot_excursions()); } { StopReversal host; @@ -128,8 +121,12 @@ int main() { const auto& t = host.get_trade(i); if (t.is_long && near(t.entry_price, 3912.15) && near(t.exit_price, 3925.79)) { found = true; - // Owner row: favorable == 13.64 exactly, not 13.645. - expect("reversal#801", t, true, 3912.15, 3925.79, 13.64, 5.77); + // expectation corrected (R5 lane H-THIN, E19): 13.64 -> + // 13.645, the kernel's sample at the stop's half-tick crossing + // (3925.795) under this run's per-kind tick rules; the corpus + // tape books 13.65 (it counts the exit bar's 3925.80 open, + // which neither model samples). + expect("reversal#801", t, true, 3912.15, 3925.79, 13.645, 5.77); break; } } @@ -153,9 +150,14 @@ int main() { const auto& t = host.get_trade(0); // High-first bar: the 1819.40 high precedes the stop-short fill, // so it is masked out of the lot and out of the exit fold; the - // post-fill low 1806.40 gives mfe 3.60 and the buy-back fill - // itself gives mae 8.00 (ab9714be pine_fills.cpp:42 + :5741). - expect("same-bar-scalp#1", t, false, 1810.00, 1818.00, 3.60, 8.00); + // post-fill low 1806.40 gave the owner mfe 3.60 and the buy-back + // fill itself gives mae 8.00 (ab9714be pine_fills.cpp:42 + :5741). + // expectation corrected (R5 lane H-THIN, E19): mfe 3.60 -> 0.00. + // The exit's buy limit at 1818.00 is already marketable when the + // entry fills, so the engine books the buy-back at the entry's own + // crossing and the lot is closed before the low; the kernel + // samples that chronology. No tape of this synthetic shape exists. + expect("same-bar-scalp#1", t, false, 1810.00, 1818.00, 0.00, 8.00); } } std::printf("test_l11a_host_excursion_twin: %d passed, %d failed\n", passed, failed); diff --git a/tests/test_l11b_same_bar_stopout_excursion.cpp b/tests/test_l11b_same_bar_stopout_excursion.cpp index 4584db1a..a7819c61 100644 --- a/tests/test_l11b_same_bar_stopout_excursion.cpp +++ b/tests/test_l11b_same_bar_stopout_excursion.cpp @@ -1,8 +1,7 @@ // R4-D L11b: same-bar stopout excursion parity. // Pins the owner rows of arun-rajan #1-#3 and geckin-joey #1-#2 with embedded // bars, and verifies host-owned excursion accounting for same-bar stopouts: -// (a) a short entered and liquidated on a low-first bar with low masked -// reports favorable excursion 0.000000 (ahtisham / waranyutrkm); +// (a) [retired with the host model, R5 lane H-THIN E19]; // (b) a 100%-of-equity long entry opening slice preceding a priced exit // preserves the exit-fill loss in adverse excursion (yukozb). // Bars are embedded literals from corpus/data/derived/ohlcv_ETH-USDT-USDT_15m.csv. @@ -184,19 +183,6 @@ class YukozbSameBarStopout : public source::PineStrategyHost { } }; -class DeclaresOwnership : public source::PineStrategyHost { -public: - DeclaresOwnership() { - source::PineStrategyConfig c; - c.initial_capital = 10000.0; - configure_pine_strategy(c); - set_syminfo_metadata("ETHUSDT", 0.01); - } - void on_source_bar(const Bar&) override {} - void set_margin_call(bool mc) { excursion_margin_call_ = mc; } - void set_current_bar_public(const Bar& b) { current_bar_ = b; } -}; - } // namespace int main() { @@ -338,35 +324,11 @@ int main() { } } - // ----------------------------------------------------------------------- - // Same-bar short stopout: low-first bar with low masked - // (zz-pop-ahtisham trade #83 / zz-pop-waranyutrkm trade #66) - // ----------------------------------------------------------------------- - { - DeclaresOwnership host; - host.set_margin_call(true); - // Bar 2025-05-30 01:00: O 2572.24, H 2595.90, L 2566.55, C 2594.03. Low-first. - host.set_current_bar_public(mk(1748566800000LL, 2572.24, 2595.90, 2566.55, 2594.03)); - ClosedLotExcursionFacts facts{}; - facts.entry_bar_index = 10; - facts.exit_bar_index = 10; - facts.is_long = false; - facts.entry_price = 2572.24; - facts.fill_price = 2595.90; - facts.lot_qty = 4.049824; - facts.closed_qty = 0.295293; - facts.entry_bar_low_masked = true; - facts.entry_bar_high_masked = false; - facts.carried_favorable = 0.0; - facts.carried_adverse = 0.0; - - const ClosedLotExcursion owned = host.closed_lot_excursion(facts); - // Because entry_bar_low_masked is true, the pre-entry low 2566.55 is masked - // to entry price, so favorable excursion MUST be 0.000000, not 1.680216. - CHECK(near(owned.favorable, 0.0)); - // Adverse excursion reaches the high (2595.90 - 2572.24) * 0.295293 = 6.986632 - CHECK(near(owned.adverse, (2595.90 - 2572.24) * 0.295293)); - } + // The former "same-bar short stopout: low-first bar with low masked" + // section (zz-pop-ahtisham trade #83 / zz-pop-waranyutrkm trade #66) + // reached the host's own excursion model directly with owner facts. R5 + // lane H-THIN (E19) retired that model -- the kernel samples the path it + // walks -- so the section went with it. // ----------------------------------------------------------------------- // Same-bar long margin call slice with fill loss diff --git a/tests/test_publication_witness_pinned.inc b/tests/test_publication_witness_pinned.inc index 7c414967..c5b248ea 100644 --- a/tests/test_publication_witness_pinned.inc +++ b/tests/test_publication_witness_pinned.inc @@ -29,6 +29,22 @@ // Storm19M: digest 0x6e51f913b73007d5 -> 0x83ea12d967fea7e7 // Storm22: digest 0xce56d5badb05638c -> 0xda5de455f464504d // Storm22M: digest 0xa8b971a9116eb495 -> 0xfdaa191a97b50614 +// R5 lane H-THIN's P10 moves the closed trades of 12 runs and the source folds +// of Storm17 and Storm17M (INT26: the behaviour half of its hash step ab297364, +// harvested on the integrated tree, every value equal to the lane's); every +// digest moves once, in the INT26 v19 hash re-pin: +// Storm04: trades 20 -> 22 +// Storm04M: trades 20 -> 22 +// Storm07: trades 15 -> 13 +// Storm07M: trades 15 -> 13 +// Storm17: folds 82 -> 86, trades 21 -> 23 +// Storm17M: folds 77 -> 80, trades 17 -> 18 +// Storm18: trades 25 -> 23 +// Storm18M: trades 25 -> 23 +// Storm19: trades 22 -> 23 +// Storm19M: trades 22 -> 23 +// Storm23: trades 27 -> 26 +// Storm23M: trades 27 -> 26 constexpr Pinned kPinned[] = { {"Config01", 0x13599d44631228e2ull, 144, 44, 144}, {"ConfigFlags101", 0x9d75aa3c41307c6aull, 145, 45, 144}, @@ -110,14 +126,14 @@ constexpr Pinned kPinned[] = { {"Storm02M", 0x1521264d78122ebeull, 63, 12, 42}, {"Storm03", 0x25f90b9eeb5f057aull, 41, 17, 41}, {"Storm03M", 0xe4da66913719f57aull, 41, 17, 41}, - {"Storm04", 0x3157bd59f491a789ull, 54, 20, 54}, - {"Storm04M", 0xa1dd548b8f00457eull, 54, 20, 54}, + {"Storm04", 0x3157bd59f491a789ull, 54, 22, 54}, + {"Storm04M", 0xa1dd548b8f00457eull, 54, 22, 54}, {"Storm05", 0x8eacccdcac89765cull, 81, 17, 53}, {"Storm05M", 0x67d9decf4dd40cb4ull, 82, 17, 53}, {"Storm06", 0x4053ebf00e230b53ull, 40, 11, 40}, {"Storm06M", 0x4d4c9f8c75e1901bull, 40, 11, 40}, - {"Storm07", 0xf7e280a190e60133ull, 55, 15, 55}, - {"Storm07M", 0x3b29ea97a5b2dd0aull, 55, 15, 55}, + {"Storm07", 0xf7e280a190e60133ull, 55, 13, 55}, + {"Storm07M", 0x3b29ea97a5b2dd0aull, 55, 13, 55}, {"Storm08", 0x9302e1c04c2dfedfull, 81, 14, 58}, {"Storm08M", 0x8944facb400c0524ull, 81, 14, 58}, {"Storm09", 0x09195d4f14ec28ddull, 48, 21, 48}, @@ -136,20 +152,20 @@ constexpr Pinned kPinned[] = { {"Storm15M", 0xa648d45a63e9a8afull, 43, 14, 43}, {"Storm16", 0x8918c41ad3b998bdull, 58, 18, 58}, {"Storm16M", 0x6b216cf99a1880dfull, 58, 18, 58}, - {"Storm17", 0x644e0ad8e5fd4833ull, 82, 21, 54}, - {"Storm17M", 0x46e2d77c04f174e3ull, 77, 17, 54}, - {"Storm18", 0xe7e874c21bf9c5fcull, 80, 25, 49}, - {"Storm18M", 0x75b39ccdb9b4542dull, 80, 25, 49}, - {"Storm19", 0x8c49334fb012a382ull, 86, 22, 49}, - {"Storm19M", 0x83ea12d967fea7e7ull, 86, 22, 49}, + {"Storm17", 0x644e0ad8e5fd4833ull, 86, 23, 54}, + {"Storm17M", 0x46e2d77c04f174e3ull, 80, 18, 54}, + {"Storm18", 0xe7e874c21bf9c5fcull, 80, 23, 49}, + {"Storm18M", 0x75b39ccdb9b4542dull, 80, 23, 49}, + {"Storm19", 0x8c49334fb012a382ull, 86, 23, 49}, + {"Storm19M", 0x83ea12d967fea7e7ull, 86, 23, 49}, {"Storm20", 0xe93eebdfc01f57b4ull, 45, 12, 42}, {"Storm20M", 0xfdb5a657600e2375ull, 45, 12, 42}, {"Storm21", 0xc0aad15a28128003ull, 47, 14, 47}, {"Storm21M", 0xbc3fbaacf5272ad1ull, 47, 14, 47}, {"Storm22", 0xda5de455f464504dull, 76, 19, 50}, {"Storm22M", 0xfdaa191a97b50614ull, 76, 19, 50}, - {"Storm23", 0xb91e295d6da6778cull, 89, 27, 54}, - {"Storm23M", 0x78f7d0134456384full, 89, 27, 54}, + {"Storm23", 0xb91e295d6da6778cull, 89, 26, 54}, + {"Storm23M", 0x78f7d0134456384full, 89, 26, 54}, {"Storm24", 0x6f40ccb0266ca278ull, 54, 25, 54}, {"Storm24M", 0xa2eeb9f1d0e10619ull, 54, 25, 54}, {"Legs01", 0xb34653b540b649dfull, 30, 6, 30}, diff --git a/tests/test_pyramiding_count_differential.cpp b/tests/test_pyramiding_count_differential.cpp index 2f2320f8..7d9eeae0 100644 --- a/tests/test_pyramiding_count_differential.cpp +++ b/tests/test_pyramiding_count_differential.cpp @@ -28,11 +28,12 @@ * P6 three market entries on one flat bar adapter != TV = kernel * P8 exit(qty) from B drains lot A (FIFO), third adapter != TV = kernel * P9 exit(qty) from A drains lot A, third id adapter = kernel = TV - * P10 exit from B, default quantity, two lots open adapter != TV = kernel + * P10 exit from B, default quantity, two lots open adapter = kernel = TV * * P10 is not a pyramiding question: it is the quantity a default * strategy.exit(from_entry) closes under FIFO with two lots open, which the - * tape's scenario exposed (TradingView closes B's 100, the adapter the book). + * tape's scenario exposed (TradingView closes B's 100, the adapter closed the + * book until R5 lane H-THIN reserved the entry's own quantity). */ #include From 6ccbfe4eb4abdbd9bea13d852252123937ee9f5d Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 07:45:51 +0800 Subject: [PATCH 69/83] Source: the pre-open margin slice sizes through source_margin_units (R5 lane H-THIN, X15 d) schedule_preopen_margin_slice kept its own copy of TradingView's slice (the restore floored to the lot, taken four times, floored again) without the one-contract band, the +1e-6 floor or the dust gates the shared source_margin_units applies at every other margin checkpoint (AUDIT4 section 6, G2-12). It now hands the same money to source_margin_units. H-MEASURE's M10 differential recorded where the copy parted: its band and fudge cases now book the shared rule's 1 and 1, which are the kernel's and ab9714be's, and only the frozen-units case still parts (4 lots on the frozen 19 against the kernel's 1 on the book of 18), with the row's expectation corrected. The corpus, the identity battery, the public slots and every hash witness print the same bytes. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit acec6d9647075f7ec0bf64f146d5aaf33ec4151b) INT26 pick note: X15 d meets PAR-MARGIN's M10 freeze in tests/test_adapter_margin_schedule_differential.cpp; both intents are kept (the late-picks list). PAR-MARGIN froze a default-percent stop above 100 % at its snapped level (18 lots), where the pre-open slice's own x4 sized nothing -- its own comment calls that case "the band row above"; X15 d sizes the slice through source_margin_units, whose one-contract band books that contract. On the merged tree the M10 frozen case therefore books the kernel's 1 @90 on the 18 lots (17 left), and its checks say so; band and fudge take the lane's 1 and 1; the header and the M10 comments keep both lanes' sentences, rewritten to be true of the merged tree. PAR-MARGIN's eight pm-m10 TradingView tapes re-run on it: every fill equals the tape (824 checks, 0 failures). No pinned witness behaviour moves (quiet-bar, publication, live-state and lookup-index trades harvested with their own switches). --- src/source/pine_adapter.cpp | 17 ++++++-- ...t_adapter_margin_schedule_differential.cpp | 43 ++++++++++++------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 982903f5..16cb4f86 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -4817,10 +4817,19 @@ void PineExecutionAdapter::schedule_preopen_margin_slice( || !finite_positive(unit_margin) || !(required > marked_equity)) { continue; } - double restore = floor_quantity_grid((required - marked_equity) / unit_margin, - staged_.quantity_grid); - double slice = floor_quantity_grid(4.0 * restore, staged_.quantity_grid); - slice = std::min(units, slice); + // TradingView's slice from this money is the shared rule + // (source_margin_units: the lot-floored restore taken four times and + // floored again, the one-contract band, the dust gates), sized here + // on the frozen signal-time units the opening will book. + SourceMarginMoney money; + money.valid = true; + money.mark = adverse; + money.held = units; + money.unit_margin = unit_margin; + money.exact_required = required; + money.required = required; + money.equity = marked_equity; + const double slice = source_margin_units(money, /*opening_checkpoint=*/false); if (!finite_positive(slice)) continue; native_order::Request request; diff --git a/tests/test_adapter_margin_schedule_differential.cpp b/tests/test_adapter_margin_schedule_differential.cpp index 8c23256e..87fc0e41 100644 --- a/tests/test_adapter_margin_schedule_differential.cpp +++ b/tests/test_adapter_margin_schedule_differential.cpp @@ -29,13 +29,15 @@ * adapter executes at the open, sized at the open; the kernel has no * check kind that does -- its mark check rests at the adverse extreme, * its CalculationOnly executes at the calculation. - * M10 (G2-12) the pre-open slice. Same money through the pre-open x4 + * M10 (G2-12) the pre-open slice. Same money through the pre-open slice * (schedule_preopen_margin_slice), through the shared rule * (source_margin_units via the kernel's point) and through the kernel's - * AfterApplied point: they part on the one-contract band and on the - * +1e-6 restore fudge. They parted on the frozen units too until R5 - * lane PAR-MARGIN froze a default-percent stop above 100 % at its - * snapped level, as TradingView does (the M10 tapes below). + * AfterApplied point: they parted on the one-contract band and on the + * +1e-6 restore fudge until R5 lane H-THIN (X15) sized the pre-open + * slice through the shared rule, and on the frozen (signal-time) units + * until R5 lane PAR-MARGIN froze a default-percent stop above 100 % at + * its snapped level, as TradingView does (the M10 tapes below). With + * both, the three book the same money. * M11 (G2-13) opening admission. AdmitWithHostMargin admits an opening the * kernel's initial-margin gate declines (a percent entry fee), and * refuses an add the kernel admits (signal-time equity vs marked). @@ -588,8 +590,10 @@ void m8_gap_open() { // bar reaches `low`, equity E at the bar's open. // a-stop : a default-percent (190 %) buy stop at 101 placed at a 102 close, // gapped through at the next 102 open -> schedule_preopen_margin_ -// slice, sized by its OWN x4 (floor_quantity_grid twice, no band, -// no dust gate, no money rounding) on the frozen tuple; +// slice, sized on the frozen tuple -- by its OWN x4 +// (floor_quantity_grid twice, no band, no dust gate, no money +// rounding) until R5 lane H-THIN (X15), by the shared +// source_margin_units since; // a-carry: the same 18 lots carried in from an earlier 102 open -> the // kernel's BarOpen point, sized by resolve_margin_call_units, i.e. // the shared source_margin_units (floor, 4x, +1e-6 floor, the @@ -597,9 +601,11 @@ void m8_gap_open() { // kernel : the stop entry on a bare kernel host -> its AfterApplied point // after the open fill, with the twin's slice. // ab9714be books the kernel's numbers on the stop entry in every case below -// (1 @90 band, 1 @88 fudge, 1 @90 frozen); only the pre-open slice parts. -// The stop entry's own entry bar stays the pre-open slice's where it slices -// nothing (the band): R5 lane PAR-MARGIN admits the kernel's AfterApplied +// (1 @90 band, 1 @88 fudge, 1 @90 frozen); only the pre-open slice parted. +// expectation corrected (R5 lane H-THIN, X15: the pre-open slice sizes +// through source_margin_units): band 0 -> 1 and fudge 3 -> 1 lots, the shared +// rule's, the kernel's and ab9714be's. The stop entry's own entry bar stays +// the pre-open slice's: R5 lane PAR-MARGIN admits the kernel's AfterApplied // point for a leveraged opening's entry bar (M7) everywhere but on the // opening this slice answers for (preopen_slice_class). struct M10Case { @@ -659,8 +665,11 @@ void m10_same_money(const M10Case& m) { // slice sized 4 lots on a 19-lot book the run did not hold. TradingView // freezes a stop not yet marketable at its snapped level, above 100 % too // (the M10 tapes below): both are now the 101 level's 18 lots, and on them -// the pre-open slice's own x4 sizes nothing -- the band row above, whose -// one contract the kernel's point books (1 @90). +// the pre-open slice's own x4 sized nothing -- the band row above, whose +// one contract the kernel's point books (1 @90) -- until R5 lane H-THIN (X15) +// sized the slice through the shared rule: it books that contract too, so +// the stop entry and the kernel book the same 1 @90 on the same 18 lots +// (INT26, where the two lanes meet). void m10_frozen_units() { std::printf("-- M10 frozen: signal 100, fill 102, low 90\n"); Config c; @@ -679,8 +688,10 @@ void m10_frozen_units() { kernel.run_bars(pine_margin_spec("m10-frozen", c, 0.0), bars); const auto native = margin_fills(kernel); print_side("kernel", native, kernel.position(), kernel.balance()); - CHECK(preopen.empty()); - CHECK(stop.position() == 18.0); // the 101 level's 18 lots + REQUIRE(preopen.size() == 1); + check_fill(preopen[0], 1, NativePathPhase::Low, 1.0, 90.0, false); + CHECK(preopen[0].label == "__margin_preopen__L"); + CHECK(stop.position() == 17.0); // the 101 level's 18 lots, less the slice REQUIRE(native.size() == 1); check_fill(native[0], 1, NativePathPhase::Low, 1.0, 90.0, true); } @@ -1888,8 +1899,8 @@ int main() { test("M7-C carried POOC short fee", m7_carried_pooc_short_fee); test("M8 gap open", m8_gap_open); test("M10 plain", [] { m10_same_money({"plain", 1000.0, 85.0, 4, 4}); }); - test("M10 band", [] { m10_same_money({"band", 1000.0, 90.0, 0, 1}); }); - test("M10 fudge", [] { m10_same_money({"fudge", 1000.000022, 88.0, 3, 1}); }); + test("M10 band", [] { m10_same_money({"band", 1000.0, 90.0, 1, 1}); }); + test("M10 fudge", [] { m10_same_money({"fudge", 1000.000022, 88.0, 1, 1}); }); test("M10 frozen", m10_frozen_units); test("M11-A fee opening", m11_fee_opening); test("M11-B add 110", [] { m11_add(110.0, false); }); From 900f8edbbefc521f7cbed36c85f93379f393c5d7 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 08:01:58 +0800 Subject: [PATCH 70/83] Kernel: a public aggregation query -- native_calendar::pairing_aggregates and NativeStrategyHost::native_aggregates_input_bars() (R5 lane H-THIN, X15 f) The Pine host decided whether its chart aggregates the input feed from the spec's two timeframe literals (source::detail::aggregates_input_bars: equal literals without a parse, tf_ratio for every other pair; AUDIT4 section 6, G2-39), beside the pairing the kernel itself resolves and aggregates on. The kernel now answers it: native_calendar::pairing_aggregates(pairing) is true for the pairings whose script bars are buckets gathered from the input (SameUnitMultiple, FixedDivisible, FixedToCalendar, CalendarToCalendar), NativeExecutionConsumer::aggregates_input() reads the run's resolved pairing, and NativeStrategyHost::native_aggregates_input_bars() is the host query (false with no configured spec). The host's one reader, the aggregated re-dating in on_native_applied, and the lookup index's cached fact read it; the literal copy is deleted. The C COVERAGE block lists the query as C++-only: its one reader is the Pine host. ADR-0001 rule 2 ruling: (a) mechanism -- a query of the pairing the run already resolved, with no platform word; (b) knob -- none, it reads the spec and decides nothing. tests/test_aggregates_input_bars_literals.cpp judges the kernel's answer against the deleted copy over every admitted pair of a literal population: they agree but on two counted classes (byte-identical monthly literals, and one-bucket pairings such as 1D on D), where the kernel's answer is the one a chart-timeframe run and TradingView date rows by. The corpus and every witness print the same bytes. The kernel residual gate reads 0 findings. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 3baaf78e283a6c6078aeaa85481a4f45dbba5f7a) --- include/pineforge/native_c_api.h | 4 + include/pineforge/native_calendar.hpp | 7 + include/pineforge/native_host.hpp | 6 + src/native_calendar.cpp | 17 +++ src/native_execution_consumer.cpp | 4 + src/native_execution_consumer.hpp | 5 + src/source/pine_adapter.cpp | 2 +- src/source/pine_host_reads.hpp | 16 +- src/source/pine_strategy_host.cpp | 36 ----- tests/test_adapter_in_place_reads.cpp | 15 +- tests/test_aggregates_input_bars_literals.cpp | 144 ++++++++++++++---- 11 files changed, 175 insertions(+), 81 deletions(-) diff --git a/include/pineforge/native_c_api.h b/include/pineforge/native_c_api.h index 763fcebf..a7b87b1f 100644 --- a/include/pineforge/native_c_api.h +++ b/include/pineforge/native_c_api.h @@ -155,6 +155,10 @@ * [C] physical_position strategy_native_position_v1 * [C] native_marked_equity strategy_native_marked_equity_v1 * [C] native_liquidation_price strategy_native_liquidation_price_v1 + * [--] native_aggregates_input_bars its one reader is the Pine host, which re-dates a closed row + * onto the aggregated script bar TradingView reports; a C host + * reads the rows the kernel books as they are, and names both + * timeframes in its own spec * [C] native_risk_state strategy_native_risk_state_v1 * [C] native_events strategy_native_events_v1 * [C] native_acknowledge_events strategy_native_acknowledge_events_v1 diff --git a/include/pineforge/native_calendar.hpp b/include/pineforge/native_calendar.hpp index 672e8e6b..1b5e9c60 100644 --- a/include/pineforge/native_calendar.hpp +++ b/include/pineforge/native_calendar.hpp @@ -115,6 +115,13 @@ struct TimeframeCompatibility { TimeframeCompatibility compatibility(const Timeframe& input, const Timeframe& script); +// Whether the pairing's script bars are buckets the kernel gathers from the +// input (a same-unit or fixed multiple, a fixed or calendar input under a +// calendar script bar -- a multiple of one included), rather than the input +// bars themselves (byte-identical literals, Passthrough). False for a finer or +// indivisible script and an invalid pair, which a run cannot hold. +bool pairing_aggregates(const TimeframeCompatibility& pairing) noexcept; + // Stream input M/nM is the existing refusal (engine_stream requires a fixed // positive duration). Batch monthly remains accepted via compatibility(). TimeframeCompatibility stream_compatibility(const Timeframe& input, const Timeframe& script); diff --git a/include/pineforge/native_host.hpp b/include/pineforge/native_host.hpp index d827af54..84f7faa9 100644 --- a/include/pineforge/native_host.hpp +++ b/include/pineforge/native_host.hpp @@ -1318,6 +1318,12 @@ class NativeStrategyHost : public BacktestEngine { /// no margin model, the side has no maintenance fraction, the book is flat, /// or no finite price solves the breach (a long at full maintenance). std::optional native_liquidation_price() const; + /// Whether the configured run's script bars are buckets the kernel gathers + /// from its input bars -- native_calendar::pairing_aggregates of the + /// pairing it resolved for the spec's input and script timeframes -- rather + /// than the input bars themselves. False for a chart-timeframe run + /// (byte-identical literals), an undetected timeframe, or no configured spec. + bool native_aggregates_input_bars() const; /// The run's generic risk ledger. Every field is its zero for a run that /// declares no NativeRunSpec::risk; observation only, it moves nothing. NativeRiskState native_risk_state() const; diff --git a/src/native_calendar.cpp b/src/native_calendar.cpp index b1ca8c91..c47cb983 100644 --- a/src/native_calendar.cpp +++ b/src/native_calendar.cpp @@ -1367,6 +1367,23 @@ TimeframeCompatibility compatibility(const Timeframe& input, const Timeframe& sc return out; } +bool pairing_aggregates(const TimeframeCompatibility& pairing) noexcept { + switch (pairing.pairing) { + case TimeframePairing::SameUnitMultiple: + case TimeframePairing::FixedDivisible: + case TimeframePairing::FixedToCalendar: + case TimeframePairing::CalendarToCalendar: + return true; + case TimeframePairing::Passthrough: + case TimeframePairing::ScriptFiner: + case TimeframePairing::IndivisibleFixed: + case TimeframePairing::StreamMonthlyInputRefused: + case TimeframePairing::Invalid: + return false; + } + return false; +} + TimeframeCompatibility stream_compatibility(const Timeframe& input, const Timeframe& script) { if (!input.valid() || !script.valid()) { TimeframeCompatibility out; diff --git a/src/native_execution_consumer.cpp b/src/native_execution_consumer.cpp index 3cb20e63..8f1919f2 100644 --- a/src/native_execution_consumer.cpp +++ b/src/native_execution_consumer.cpp @@ -10732,6 +10732,10 @@ std::optional NativeStrategyHost::native_liquidation_price() const { .host_liquidation_price(*this); } +bool NativeStrategyHost::native_aggregates_input_bars() const { + return NativeExecutionConsumer::bound(*this).aggregates_input(); +} + NativeRiskState NativeStrategyHost::native_risk_state() const { return NativeExecutionConsumer::bound(*this) .risk_state(); diff --git a/src/native_execution_consumer.hpp b/src/native_execution_consumer.hpp index 3a4809c7..0a3d8f0e 100644 --- a/src/native_execution_consumer.hpp +++ b/src/native_execution_consumer.hpp @@ -127,6 +127,11 @@ class NativeExecutionConsumer final : public IExecutionConsumer { // (NativeStrategyHost::native_state, out of line and by value). running() // is the branch view() takes. bool running() const noexcept { return running_spec_ != nullptr; } + // Whether the configured spec's script bars are buckets gathered from its + // input under the resolved pairing (native_aggregates_input_bars). + bool aggregates_input() const noexcept { + return native_calendar::pairing_aggregates(pairing_); + } NativeLifecycleKind state_kind() const { return running_spec_ ? NativeLifecycleKind::Running : view().kind; } diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 16cb4f86..8ab87b19 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -1353,7 +1353,7 @@ void detail::take_run_facts(NativeExecutionConsumer& consumer, const PineExecutionAdapter& adapter, std::uint64_t run) { if (!consumer.running()) return; if (auto* index = adapter_lookup_index(&consumer, &adapter, run)) - index->aggregates_input_bars = aggregates_input_bars(consumer.state_spec()); + index->aggregates_input_bars = consumer.aggregates_input(); } IExecutionConsumer* PineExecutionAdapter::bound_consumer() const noexcept { diff --git a/src/source/pine_host_reads.hpp b/src/source/pine_host_reads.hpp index dab284ae..2c4dfbff 100644 --- a/src/source/pine_host_reads.hpp +++ b/src/source/pine_host_reads.hpp @@ -71,10 +71,6 @@ inline const NativeCurrentPointView* callback_point(const NativeStrategyHost& ho return run_consumer(host).current_point(); } -// A chart whose script bar aggregates several input bars -// (pine_strategy_host.cpp): the spec overload of aggregates_input_bars. -bool aggregates_input_bars(const NativeRunSpec* spec); - // The source layer's state parked with the consumer for one run (R5 lane // PERF-P7's NativeHostCache: the adapter's lookup index derives from this, // pine_adapter.cpp). The consumer drops it at every run begin and never reads @@ -101,10 +97,12 @@ inline PineRunCache* run_cache(const NativeExecutionConsumer& consumer, const vo return facts->owner == owner && facts->run == run ? facts : nullptr; } -// aggregates_input_bars(native_state()) for the adapter at `owner` in run -// `run`: while the run runs, the answer taken at its begin; otherwise, or -// when the consumer keeps no host cache (set_host_cache(false), the reference -// tests/test_adapter_in_place_reads.cpp compares against), computed from the spec. +// Whether a script bar of the chart gathers several input bars -- the +// kernel's answer (NativeStrategyHost::native_aggregates_input_bars) -- for the +// adapter at `owner` in run `run`: while the run runs, the answer taken at its +// begin; otherwise, or when the consumer keeps no host cache +// (set_host_cache(false), the reference tests/test_adapter_in_place_reads.cpp +// compares against), asked of the consumer. inline bool run_aggregates_input_bars(const NativeExecutionConsumer& consumer, const void* owner, std::uint64_t run) { if (consumer.running()) { @@ -114,7 +112,7 @@ inline bool run_aggregates_input_bars(const NativeExecutionConsumer& consumer, return *facts->aggregates_input_bars; } } - return aggregates_input_bars(consumer.state_spec()); + return consumer.aggregates_input(); } // Takes the facts above for the run that has just begun (on_native_run_begin). diff --git a/src/source/pine_strategy_host.cpp b/src/source/pine_strategy_host.cpp index 334121fa..5d720775 100644 --- a/src/source/pine_strategy_host.cpp +++ b/src/source/pine_strategy_host.cpp @@ -78,42 +78,6 @@ void validate_source_begin_bars(const NativeBeginArgs& args) { } // namespace -namespace source::detail { - -// A chart whose script bar aggregates several input bars: the input timeframe -// is finer than the script's (PineScheduler::run_begin's needs_aggregation). -// There the kernel's interval index names the SCRIPT bar (26 for the 26th -// 15m bar of a 1m feed), while NativeCoordinate::input_interval_index keeps -// the input slot for hosts that need it. Pine therefore reads the same public -// script-bar index without a projection-time restamp. -// -// The host and the scheduler ask this in their per-bar callbacks, of a spec -// that cannot change inside the run, so it must not cost a parse of both -// literals each time. Equal literals -- every chart that does not aggregate -- -// need none: tf_ratio(x, x) is -1 exactly when tf_to_seconds(x) is negative, -// and a configured spec's literal passed native_calendar::parse_timeframe -// ([1-9][0-9]*[SDWM]? or a bare D, W or M), so one of at most four characters -// cannot overflow that int product and is negative only when it is monthly. -// Every other pair keeps tf_ratio. The host's and the scheduler's per-bar -// callbacks read its answer for the running spec once per run -// (run_aggregates_input_bars, pine_host_reads.hpp, R5 lane D2-C); the view -// overload's linkage lets tests/test_aggregates_input_bars_literals.cpp judge -// it against tf_ratio. Not part of the installed API. -bool aggregates_input_bars(const NativeRunSpec* spec) { - if (!spec || spec->timeframe_undetected) return false; - const std::string& input = spec->input_tf; - const std::string& script = spec->script_tf; - if (input == script && !script.empty() && script.size() <= 4) return script.back() == 'M'; - const int ratio = tf_ratio(input, script); - return ratio > 1 || ratio == -1; -} - -bool aggregates_input_bars(const NativeStateView& state) { - return aggregates_input_bars(state.spec); -} - -} // namespace source::detail - source::PineStrategyHost::PineStrategyHost(compat::pine::CapAttachment cap) : NativeStrategyHost(), adapter_(*this, cap), diff --git a/tests/test_adapter_in_place_reads.cpp b/tests/test_adapter_in_place_reads.cpp index e899c332..735217bb 100644 --- a/tests/test_adapter_in_place_reads.cpp +++ b/tests/test_adapter_in_place_reads.cpp @@ -5,9 +5,10 @@ // The source layer reads the run's lifecycle, spec and phase, the callback's // point, the book and the bound consumer through src/source/pine_host_reads.hpp // instead of native_state(), current_execution_point(), physical_position() -// and execution_consumer(), and reads aggregates_input_bars once per run, -// kept in the adapter's host cache (pd::PineRunCache, taken at -// on_native_run_begin). For every D2-C scenario +// and execution_consumer(), and reads the kernel's aggregation answer +// (native_aggregates_input_bars, since R5 lane H-THIN; the adapter's own +// literal predicate before) once per run, kept in the adapter's host cache +// (pd::PineRunCache, taken at on_native_run_begin). For every D2-C scenario // (tests/pine_d2c_scenarios_fixture.hpp: magnifier off/on, aggregated charts, // calc_on_order_fills re-entries, warm-up switches, kernel-routed and // host-driven request.security sites, the auxiliary lower-timeframe array, @@ -16,9 +17,9 @@ // 1. The host compares, at every script bar it publishes, in every // request.security evaluation and at the run's script preparation, each // in-place read against its accessor -- field for field, the point by -// its digest -- and the kept fact against -// pd::aggregates_input_bars(native_state()); and again before the -// run (Unconfigured) and after it (Completed). +// its digest -- and the kept fact against the host's +// native_aggregates_input_bars(); and again before the run +// (Unconfigured) and after it (Completed). // 2. The scenario runs with the consumer's host cache (shipped) and without // it (set_host_cache(false): nothing kept, every read of the fact // computes it, the PERF-P7 lookups walk): the transcripts -- every read @@ -135,7 +136,7 @@ class InPlaceHost final : public source::PineStrategyHost { digest = point_digest(*point); CHECK(digest == point_digest(*point_in_place)); } - const bool aggregates = pd::aggregates_input_bars(view.spec); + const bool aggregates = native_aggregates_input_bars(); if (const pd::PineRunCache* facts = kept(); facts != nullptr && view.kind == NativeLifecycleKind::Running) { CHECK(facts->aggregates_input_bars.has_value()); diff --git a/tests/test_aggregates_input_bars_literals.cpp b/tests/test_aggregates_input_bars_literals.cpp index 57999811..219225be 100644 --- a/tests/test_aggregates_input_bars_literals.cpp +++ b/tests/test_aggregates_input_bars_literals.cpp @@ -1,22 +1,41 @@ -// R5 lane PERF-P5611, item P6. Whether a Pine run's chart aggregates input -// bars is a per-run invariant of its spec, and the host and the scheduler ask -// it in their per-bar callbacks (PERF0-P counted 1.04-1.40 asks a bar and -// 18 ns/bar of timeframe parsing behind them). +// R5 lane PERF-P5611, item P6, then R5 lane H-THIN (audit X15): whether a +// Pine run's chart aggregates input bars. // -// Until this lane both callbacks' copies answered it by tf_ratio of the spec's -// two literals: two tf_to_seconds parses per ask. The one definition left -// (source::detail::aggregates_input_bars, src/source/pine_strategy_host.cpp) -// answers equal literals -- every chart that does not aggregate -- without a -// parse, and keeps tf_ratio for every other pair. The answer may not move for -// any spec a run can hold, so this row judges it against the pre-lane body -// verbatim over every pair of a literal population that +// Until H-THIN the source layer answered this itself from the spec's two +// literals (source::detail::aggregates_input_bars in pine_strategy_host.cpp: +// equal literals without a parse, tf_ratio for every other pair; before +// PERF-P5611 tf_ratio for all). The kernel resolves the same pair when a spec +// is configured (native_calendar::compatibility, the pairing its aggregator +// runs on), so H-THIN deleted the source copy: the host asks +// NativeStrategyHost::native_aggregates_input_bars(), which answers +// native_calendar::pairing_aggregates of that pairing. This row judges the +// kernel's answer against the source layer's former one -- tf_ratio, verbatim +// -- over every pair of a literal population that // native_calendar::parse_timeframe accepts, as configure_native requires of a // run's literals: every suffix at the counts where the unit arithmetic // changes shape, the largest count of each unit whose tf_to_seconds still fits // an int, and seeded random counts; plus the undetected and spec-less views. // -// Source-bound: the definition lives in the source layer (the include below is -// what keeps this TU out of the kernel-only profile). +// Pairs configure_native refuses are counted and skipped: no run holds them, +// and the former copy's answer for them was never read. On every admitted pair +// the two agree but two classes, each counted here exactly: +// - byte-identical monthly literals ("M" on "M", "3M" on "3M"): tf_to_seconds +// is negative, tf_ratio answers -1 and the former copy said "aggregates"; +// the kernel pairs them Passthrough, a chart-timeframe run; +// - one-bucket pairings the kernel gathers but tf_ratio rounds to 0 or 1 +// ("1D" on "D", "1440" on "D", "999" on "D", "60S" on "1", "9999" on "W"): +// a multiple of one, or an input under a script bar less than twice it. +// The one Pine reader left (PineStrategyHost::on_native_applied) re-dates a +// row to its chart bar's open on a chart the kernel buckets, because the +// kernel dates a bucket's close point at the bucket's close. A chart-timeframe +// run already dates every modeled point at that open, so the first class moves +// no row; the second class's rows were dated at the bucket's close -- for "1D" +// on "D" the next day's open -- and are now dated at their own bar's open, as a +// chart-timeframe run and TradingView date them (the lane report measures both +// classes on a POOC run). +// +// Source-bound: the host query binds the adapter's consumer (the include below +// is what keeps this TU out of the kernel-only profile). #include #include #include @@ -24,28 +43,52 @@ #include #include +#include #include #include -namespace pineforge::source::detail { -// src/source/pine_strategy_host.cpp. External linkage only so this row and -// PineScheduler reach the one definition; not part of the installed API. -bool aggregates_input_bars(const NativeStateView& state); -} // namespace pineforge::source::detail - namespace { using namespace pineforge; int failures = 0; long long checks = 0; -// Both copies' body before this lane. +// The source layer's body before PERF-P5611 (P5611 kept its answers). bool tf_ratio_answer(const NativeStateView& state) { if (!state.spec || state.spec->timeframe_undetected) return false; const int ratio = tf_ratio(state.spec->input_tf, state.spec->script_tf); return ratio > 1 || ratio == -1; } +long long monthly_passthrough = 0; +long long one_bucket = 0; +long long refused_pairs = 0; + +// The pairing configure_native resolves for these literals, and whether it +// admits a run on it (native_run_spec.cpp: Passthrough, a same-unit or fixed +// multiple, a fixed or calendar feed under a calendar script bar). +bool admitted(const native_calendar::TimeframeCompatibility& pairing) { + using P = native_calendar::TimeframePairing; + switch (pairing.pairing) { + case P::Passthrough: case P::SameUnitMultiple: case P::FixedDivisible: + case P::FixedToCalendar: case P::CalendarToCalendar: return true; + default: return false; + } +} + +// The kernel's answer for a configured spec with these literals (none for an +// undetected timeframe); nullopt when configure_native refuses the pair, so no +// run can hold it. +std::optional kernel_answer(const NativeStateView& state) { + if (!state.spec || state.spec->timeframe_undetected) return false; + const auto input = native_calendar::parse_timeframe(state.spec->input_tf); + const auto script = native_calendar::parse_timeframe(state.spec->script_tf); + if (!input || !script) return std::nullopt; + const auto pairing = native_calendar::compatibility(*input, *script); + if (!admitted(pairing)) return std::nullopt; + return native_calendar::pairing_aggregates(pairing); +} + void check_pair(const std::string& input, const std::string& script, bool undetected) { static NativeRunSpec spec; spec.input_tf = input; @@ -54,11 +97,37 @@ void check_pair(const std::string& input, const std::string& script, bool undete NativeStateView state; state.spec = &spec; ++checks; - const bool expected = tf_ratio_answer(state); - const bool got = source::detail::aggregates_input_bars(state); - if (expected != got && ++failures <= 20) { - std::fprintf(stderr, "FAIL input=\"%s\" script=\"%s\" undetected=%d expected %d got %d\n", - input.c_str(), script.c_str(), undetected ? 1 : 0, expected, got); + const bool former = tf_ratio_answer(state); + const std::optional answer = kernel_answer(state); + if (!answer) { + ++refused_pairs; + return; + } + const bool got = *answer; + const bool monthly_equal = !undetected && input == script && !script.empty() + && script.back() == 'M'; + if (monthly_equal) { + // tf_ratio -1, a Passthrough pairing. + ++monthly_passthrough; + if (!(former && !got) && ++failures <= 20) { + std::fprintf(stderr, "FAIL monthly literal \"%s\" expected former 1 kernel 0, got %d %d\n", + input.c_str(), former, got); + } + return; + } + if (got && !former) { + // A bucket tf_ratio rounds to one bar or none. + const int ratio = tf_ratio(input, script); + ++one_bucket; + if (!(ratio == 0 || ratio == 1) && ++failures <= 20) { + std::fprintf(stderr, "FAIL input=\"%s\" script=\"%s\" kernel 1 former 0 at tf_ratio %d\n", + input.c_str(), script.c_str(), ratio); + } + return; + } + if (former != got && ++failures <= 20) { + std::fprintf(stderr, "FAIL input=\"%s\" script=\"%s\" undetected=%d former %d kernel %d\n", + input.c_str(), script.c_str(), undetected ? 1 : 0, former, got); } } @@ -109,13 +178,32 @@ int main() { check_pair("", "", false); NativeStateView bare; ++checks; - if (source::detail::aggregates_input_bars(bare)) { + if (kernel_answer(bare).value_or(true)) { std::fprintf(stderr, "FAIL a view without a spec aggregates\n"); ++failures; } + // The host query itself: a host with no spec answers false. + ++checks; + { + struct Bare final : NativeStrategyHost { + void on_native_bar(const Bar&, const NativeDecisionContext&) override {} + } host; + if (host.native_aggregates_input_bars()) { + std::fprintf(stderr, "FAIL an unconfigured host aggregates\n"); + ++failures; + } + } + if (monthly_passthrough < 3 || one_bucket < 3) { + std::fprintf(stderr, "FAIL a class was not reached: monthly %lld one-bucket %lld\n", + monthly_passthrough, one_bucket); + ++failures; + } if (failures == 0) { - std::printf("test_aggregates_input_bars_literals: ok (%lld pairs against tf_ratio, " - "%zu literals)\n", checks, literals.size()); + std::printf("test_aggregates_input_bars_literals: ok (%lld pairs, %lld of them refused " + "by configure_native, the rest against tf_ratio, %zu literals; %lld " + "byte-identical monthly pairs tf_ratio called aggregated, %lld one-bucket " + "pairs it did not)\n", checks, refused_pairs, literals.size(), + monthly_passthrough, one_bucket); } return failures == 0 ? 0 : 1; } From 67758f467d2060fc9d09ef578f160bd951c6c4a6 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 09:03:19 +0800 Subject: [PATCH 71/83] Source: the exit-reservation and POOC FIFO close epsilons are the shared kQtyEpsilon / kFullPercentEps (R5 lane H-THIN, A4-DUST) compute_exit_reservation and reconcile_deferred_exit_reservations each kept a local copy of internal::kQtyEpsilon (1e-10) and internal::kFullPercentEps (1e-9), and enqueue_pooc_fifo_close a third (epsilon = 1e-10). The values are equal, so the fold is byte-identical by construction; the three seeded A4-DUST witness families (5,500 runs) print the same bytes before and after. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit ee02d451a96477e19d98c20992c35ac1ce111ab2) --- src/source/pine_adapter.cpp | 41 ++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 8ab87b19..956243b1 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -4298,15 +4298,13 @@ bool PineExecutionAdapter::compute_exit_reservation( const SourceId& exit_id, const SourceId& from_entry, double requested_qty, double& qty_percent, double live_basis, double& reserved_qty) const { - constexpr double kQuantityEpsilon = 1e-10; - constexpr double kFullPercentEpsilon = 1e-9; qty_percent = std::isfinite(qty_percent) ? std::clamp(qty_percent, 0.0, 100.0) : 100.0; reserved_qty = kNaN; - if (!(live_basis > kQuantityEpsilon)) { + if (!(live_basis > internal::kQtyEpsilon)) { if (std::isfinite(requested_qty)) { reserved_qty = std::abs(requested_qty); - return reserved_qty > kQuantityEpsilon; + return reserved_qty > internal::kQtyEpsilon; } return true; } @@ -4389,12 +4387,12 @@ bool PineExecutionAdapter::compute_exit_reservation( continue; } if (std::isfinite(reservation.units)) already_reserved += reservation.units; - if (reservation.percent >= 100.0 - kFullPercentEpsilon) other_full_exit = true; + if (reservation.percent >= 100.0 - internal::kFullPercentEps) other_full_exit = true; } const double available = std::max(0.0, live_basis - already_reserved); if (std::isfinite(requested_qty)) { reserved_qty = std::min(std::abs(requested_qty), available); - } else if (qty_percent < 100.0 - kFullPercentEpsilon + } else if (qty_percent < 100.0 - internal::kFullPercentEps && std::isfinite(preserved_reserved)) { reserved_qty = std::min(preserved_reserved, live_basis); } else { @@ -4402,14 +4400,14 @@ bool PineExecutionAdapter::compute_exit_reservation( // that exposure by 100/100 can round down by one binary64 step and // turn the legacy execute_market_exit branch into a dust reduction // (ab9714be:src/source/pine_fills.cpp:6893-6932; A33). - double requested = qty_percent >= 100.0 - kFullPercentEpsilon + double requested = qty_percent >= 100.0 - internal::kFullPercentEps ? live_basis : live_basis * qty_percent / 100.0; - if (qty_percent < 100.0 - kFullPercentEpsilon) { + if (qty_percent < 100.0 - internal::kFullPercentEps) { requested = quantize_percent_exit_units(requested, available); } reserved_qty = std::min(requested, available); } - if (!(reserved_qty > kQuantityEpsilon)) return false; + if (!(reserved_qty > internal::kQtyEpsilon)) return false; qty_percent = reserved_qty / live_basis * 100.0; const bool partial = reserved_qty < live_basis - 1e-9; if (partial && other_full_exit) return false; @@ -4418,9 +4416,7 @@ bool PineExecutionAdapter::compute_exit_reservation( void PineExecutionAdapter::reconcile_deferred_exit_reservations( const SourceId& from_entry, double live_basis) { - constexpr double kQuantityEpsilon = 1e-10; - constexpr double kFullPercentEpsilon = 1e-9; - if (!(live_basis > kQuantityEpsilon)) return; + if (!(live_basis > internal::kQtyEpsilon)) return; struct Family { std::uint64_t key = 0; @@ -4564,7 +4560,7 @@ void PineExecutionAdapter::reconcile_deferred_exit_reservations( // the enlarged net position (ab9714be:pine_strategy_commands.cpp // :2739-2811 and test_exit_bracket_pending_entry_leg). units = std::min(std::abs(family.explicit_requested), available); - } else if (family.percent < 100.0 - kFullPercentEpsilon + } else if (family.percent < 100.0 - internal::kFullPercentEps && std::isfinite(family.existing)) { units = std::min(family.existing, available); } else { @@ -4574,14 +4570,14 @@ void PineExecutionAdapter::reconcile_deferred_exit_reservations( // land one binary64 step low and strand a sub-lot dust remainder // that never flattens (ab9714be pine_fills.cpp:1618 spells the // same whole-position coverage as `qty - kQtyEpsilon`). - double requested = family.percent >= 100.0 - kFullPercentEpsilon + double requested = family.percent >= 100.0 - internal::kFullPercentEps ? live_basis : live_basis * family.percent / 100.0; - if (family.percent < 100.0 - kFullPercentEpsilon) { + if (family.percent < 100.0 - internal::kFullPercentEps) { requested = quantize_percent_exit_units(requested, available); } units = std::min(requested, available); } - if (!(units > kQuantityEpsilon)) { + if (!(units > internal::kQtyEpsilon)) { cancel.insert(cancel.end(), family.handles.begin(), family.handles.end()); for (const auto index : family.queued) { if (index < pending_bracket_legs_.size()) @@ -4601,7 +4597,7 @@ void PineExecutionAdapter::reconcile_deferred_exit_reservations( found->second.qty_percent = normalized_percent; found->second.fixed_exit_reservation = std::isfinite(family.explicit_requested) - || family.percent < 100.0 - kFullPercentEpsilon + || family.percent < 100.0 - internal::kFullPercentEps || std::isfinite(family.existing); found->second.reservation_deferred_to_pending_entry = false; } @@ -4612,7 +4608,7 @@ void PineExecutionAdapter::reconcile_deferred_exit_reservations( snapshot.qty_percent = normalized_percent; snapshot.fixed_exit_reservation = std::isfinite(family.explicit_requested) - || family.percent < 100.0 - kFullPercentEpsilon + || family.percent < 100.0 - internal::kFullPercentEps || std::isfinite(family.existing); snapshot.reservation_deferred_to_pending_entry = false; } @@ -4623,7 +4619,7 @@ void PineExecutionAdapter::reconcile_deferred_exit_reservations( snapshot.qty_percent = normalized_percent; snapshot.fixed_exit_reservation = std::isfinite(family.explicit_requested) - || family.percent < 100.0 - kFullPercentEpsilon + || family.percent < 100.0 - internal::kFullPercentEps || std::isfinite(family.existing); snapshot.reservation_deferred_to_pending_entry = false; } @@ -7117,7 +7113,6 @@ double PineExecutionAdapter::close_reserved_other_units( bool PineExecutionAdapter::enqueue_pooc_fifo_close( const SourceId& id, const std::string& comment, std::uint64_t token, std::uint64_t) { - constexpr double epsilon = 1e-10; const auto point = detail::callback_point(require_host()); const int bar = point ? point->decision.coordinate.interval_index : -1; if (close_batch_bar_ != bar) { @@ -7177,8 +7172,8 @@ bool PineExecutionAdapter::enqueue_pooc_fifo_close( const double persistent_available = std::max(0.0, held - persistent_other); const double available = std::max(0.0, persistent_available - pending_reserved); const double target = std::min(unclosed, available); - if (!(target > epsilon)) { - if (unclosed > epsilon && !(persistent_available > epsilon)) { + if (!(target > internal::kQtyEpsilon)) { + if (unclosed > internal::kQtyEpsilon && !(persistent_available > internal::kQtyEpsilon)) { if (token == 0) { close_logical_units_.erase(id); close_reserved_units_.erase(id); @@ -7199,7 +7194,7 @@ bool PineExecutionAdapter::enqueue_pooc_fifo_close( close_batch_pending_debt_ += target; pending_same_bar_close_qty_ += target; if (token != 0) close_batch_admitted_total_ += target - replaced_target; - const bool retire_whole = unclosed > persistent_available + epsilon; + const bool retire_whole = unclosed > persistent_available + internal::kQtyEpsilon; auto& site = close_batch_callsites_[token]; if (!site.active) { From ffeff2c94b667c89526a8cd108a304f57cedda2d Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:08:36 +0800 Subject: [PATCH 72/83] Docs: the adapter's duplicates of kernel capabilities after R5 lane H-THIN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design §3.9 records the lane: X15 item by item (a, d, e, f gone; b and c kept as measured rows), AUDIT4 §6 re-derived, the quantity-epsilon census of the Pine host and adapter, the size against the owner's ruling, and two findings. Rows MG3, MG7, MG8, RP1, AG1/AG2 and the excursion projection gate say what moved; the ADR's seam, margin and dust bullets, its max_open_lots and quantity_tolerance rulings and a Wave H rule-2 table (the civil-date helper and the aggregation predicate) follow; native-settlement, abi-stability (the H-THIN hash step), native-engine (the new host query), pine-to-native and contributing-llm (the pyramiding lowering trial) likewise. Every anchor re-anchored onto this tree (1678/1678); the hash-pinned citations that named code E19 deleted, or that were pinned to the wrong window on the base tree, now name their symbol. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 465fe607e7bb66717ac274f8bc0edd0593f6bbfb) INT26 pick note: the lane wrote these pages on main 0d76a099; here they merge with the wave-H pages picked before them. Anchor digits merged by carrying this branch's numbers through the lane's line moves. Where H-DOCGATES had already turned a content pin into a symbol citation, the citation stays and the lane's text beside it lands (the pyramiding row, the design page's E1-E9, P8 and j rows, the ADR's pre-open admission paragraph); where the lane cites a symbol for a pin this branch had re-anchored by line (the P6 decision's `set_pine_risk_*` setters), the lane's symbol stands. Two sentences both sides rewrote are merged to be true of the merged tree: - design E2: its excursion sample "differed by chronology (the Pine host's own excursion model, retired by R5 lane H-THIN ...)": the citation this branch had given it, `margin_call_sample_bar`, left with that model. - ADR-0001, the pre-open slice: it parted from the kernel's point on the one-contract band and the +1e-6 floor until H-THIN's shared rule, and on the frozen units until PAR-MARGIN's freeze (whose tape evidence stays); with both it books the kernel's units -- the M10 frozen case of tests/test_adapter_margin_schedule_differential.cpp, as merged at the X15 d pick. The lane's "parts on the frozen units alone (4 lots on the frozen 19 ...)" was true of its own tree, not this one. The sentences it retires or rewrites, and who wrote them: ADR-0001's two on the Pine host's own per-lot MFE/MAE and its ab9714be retention (H-MEASURE's excursion measurements, 37090e1, picked from fd3c72e) and its sentence on the pre-open admission slice, now extended (H-MEASURE's margin measurements, bc866e5, picked from bafefba); the design page's RE1 sentence on the adapter's owner hook and its row's "retained: it is the host's excursion contract" (the range-end ruling, b16e11f, lane Q6 02ed0fe); and contributing-llm.md's "The adapter keeps its count pending a re-lowering ruling." (H-MEASURE's pyramiding tapes, e1d0fa3, picked from 99d3b95). --- docs/adr/0001-kernel-adapter-boundary.md | 186 ++++++------ docs/design/native-feature-parity.md | 355 +++++++++++++++-------- docs/native-settlement.md | 10 +- docs/pages/abi-stability.md | 11 + docs/pages/contributing-llm.md | 2 +- docs/pages/native-engine.md | 19 +- docs/pages/pine-to-native.md | 210 +++++++------- 7 files changed, 474 insertions(+), 319 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 232660b8..dc630377 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -85,14 +85,14 @@ summary of it. `NativeCoordinate::interval_index` (`market_driver.hpp:74`) is the sequential SCRIPT-bar index even when `input_tf` is finer than `script_tf`. The same script coordinate books `PyramidEntry::entry_bar_index`, trade-row indices and - the inclusive bar metrics (`engine_orders.cpp:113-114`, `engine_metrics.cpp:156`). + the inclusive bar metrics (`engine_orders.cpp:113-114`, `engine_metrics.cpp:140`). `NativeCoordinate::input_interval_index` (`market_driver.hpp:79`) names the input slot that supplied the point for a host that needs input cadence. Both coordinate values are folded by `hash_coordinate` (`native_execution_consumer.cpp:1051-1065`) in the v19 continuation. The Pine adapter keeps its pending-book and chart-sampling policy in the source layer, reading the named input coordinate through `projection_bar_index` - (`pine_adapter.cpp:845-851`) where Pine still needs input cadence, and + (`pine_adapter.cpp:678-684`) where Pine still needs input cadence, and does not rewrite the kernel's lot or row indices. - **Requests.** `submit` / `replace` / `cancel` / `submit_market` / `replace_market` (`native_host.hpp:1227-1232`), plus `cancel_all` (`native_host.hpp:1267`) and `cancel_where` @@ -114,7 +114,7 @@ summary of it. (`TermsUnresolved` `native_execution_consumer.cpp:4976`). - **Observing executions.** Implement `on_native_applied` (`native_host.hpp:938`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` - (`native_host.hpp:1329`). Do *not* implement + (`native_host.hpp:1317`). Do *not* implement `NativeExecutionConsumer`: it is the kernel's internal matcher, declared `final` (`src/native_execution_consumer.hpp:58`) and bound by the host. - **The rest of the host surface**, all on `NativeStrategyHost`: @@ -126,7 +126,7 @@ summary of it. | Cohorts | `cohort_open` (`native_host.hpp:1282`) / `cohort_add` (`:1285`) / `cohort_remove` (`:1288`) — the handle a `BindCohort` owner names | | Mid-callback execution | `current_execution_point` (`native_host.hpp:1087`), `inspect_current_execution` (`native_host.hpp:1099`) / `execute_current` (`native_host.hpp:1104`) | | Margin seams | four, not one: the run spec's own `margin` model (`NativeMarginModel` `native_run_spec.hpp:240`) with its three hooks — `resolve_margin_requirement` (`native_host.hpp:976`), `margin_check_allowed` (`native_host.hpp:988`), `resolve_margin_call_units` (`native_host.hpp:995`) — and `validate_execution_precommit` (`native_host.hpp:960`), whose `AdmitWithHostMargin` verdict hands the opening check to the host | - | Risk seam | `NativeRiskLimits` (`native_run_spec.hpp:310`) with `native_risk_state()` (`native_host.hpp:1323`); the breach appends a `NativeRiskEvent` to the command history | + | Risk seam | `NativeRiskLimits` (`native_run_spec.hpp:310`) with `native_risk_state()` (`native_host.hpp:1311`); the breach appends a `NativeRiskEvent` to the command history | | State reads | `physical_position()` (`native_host.hpp:1292`) / `native_marked_equity(mark)` (`native_host.hpp:1301`) / `native_open_lots(mark)` (`native_host.hpp:1297`), `trail_state(handle)` (`native_host.hpp:1092`), and the rest of the query surface in `docs/pages/native-engine.md`, "Reading the run back" | | Lookahead hazard | the `Bar` given to `on_native_bar_open` is the *complete* script bar; `current_partial_bar()` (`native_host.hpp:1073`) is the lookahead-free bar so far, and `NativeOpenBarView::OpenOnly` (`native_run_spec.hpp:109`) masks that one callback | - **The run spec is generic, and no longer narrow.** `NativeRunSpec` @@ -177,13 +177,13 @@ summary of it. `KernelRecordedAtHostMarks` records the same series at the host's own marks. `report_open_position_at_end` books the still-open position as a mark-to-market row under `KernelRecorded`. Under `HostRecorded` an unrecorded curve still degenerates — - `compute_equity_stats` answers its all-NaN value with no error (`src/engine_metrics.cpp:219`, + `compute_equity_stats` answers its all-NaN value with no error (`src/engine_metrics.cpp:203`, consumed by `fill_metrics_section`, `src/engine_report.cpp:119-139`) — which is why the policy exists. - **Reproducibility.** Two digests, one wrapping the other. `native_continuation_hash()` - (`native_host.hpp:1370`) returns the consumer's event/continuation hash. **A digest names + (`native_host.hpp:1358`) returns the consumer's event/continuation hash. **A digest names the run's inputs** (rule 2): the timezone identity enters it as the zone's *content* — kind, effective definition and `TimezoneIdentityDescriptor::resource_digest`, an FNV-1a - over the zone files the resolver read (`native_calendar.hpp:233`) — and never as + over the zone files the resolver read (`native_calendar.hpp:240`) — and never as `zoneinfo_root` or `resource_paths`, which are where this machine keeps that content and are not inputs of the run. R5 lane E23 ruled and made it so; before it, one binary on one host answered two digests for one run with nothing but `$TZDIR` between them, and no two @@ -207,21 +207,22 @@ into kernel driving — never by special-casing the kernel *for a specific scrip kernel and own these quirks, each at its site: - **Seams the adapter overrides.** The kernel is driven, not patched. `PineStrategyHost` overrides - `resolve_execution_terms` (`pine_strategy_host.cpp:677`) for TV sizing and fill spelling; - `validate_execution_precommit` (`pine_strategy_host.cpp:702`), whose - `validate_precommit` (`pine_adapter.cpp:13079`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:13324`) to own the opening margin decision; `owns_lot_excursions() = true` - (`pine_strategy_host.hpp:284-285`) with `closed_lot_excursion` (`pine_strategy_host.cpp:787`), so - the Pine host keeps its own per-lot MFE/MAE, `ab9714be`'s model (RULING A48). Measured against - TradingView by R5 lane H-MEASURE (`tests/test_e19_excursion_tape.cpp`, `lab tv` tapes): where - the host's model and the kernel sampler differ, the kernel's number is TradingView's — the host - masks an entry-bar extreme a stop filled at the open never saw, folds the exit bar's extreme - after an at-open exit, and folds the whole entry bar into a `calc_on_order_fills` scratch - TradingView reports as 0 / 0. The retention is `ab9714be` compatibility, a recorded - divergence; `on_native_tick` - (`pine_strategy_host.cpp:381`) / `on_native_applied` (`pine_strategy_host.cpp:477`) for - `calc_on_order_fills` re-entry. `process_orders_on_close` becomes - `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2319-2320`); + `resolve_execution_terms` (`pine_strategy_host.cpp:676`) for TV sizing and fill spelling; + `validate_execution_precommit` (`pine_strategy_host.cpp:701`), whose + `validate_precommit` (`pine_adapter.cpp:13034`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:12902`) to own the opening margin decision; `on_native_tick` + (`pine_strategy_host.cpp:299`) / `on_native_applied` (`pine_strategy_host.cpp:350`) for + `calc_on_order_fills` re-entry. It does not own lot excursions: since R5 lane H-THIN it keeps + the kernel's `owns_lot_excursions()` default, so the kernel's sampler books every Pine lot's + MFE/MAE, and `ab9714be`'s host model (RULING A48) is gone. R5 lane H-MEASURE had measured the + two against TradingView (`tests/test_e19_excursion_tape.cpp`, `lab tv` tapes): where they + differed, the kernel's number was TradingView's — the host masked an entry-bar extreme a stop + filled at the open never saw, folded the exit bar's extreme after an at-open exit, and folded + the whole entry bar into a `calc_on_order_fills` scratch TradingView reports as 0 / 0. The + lowering moved the excursion cells of 335 trades in 19 corpus probes: 324 now equal + TradingView's, 11 moved closer, none farther (`tests/fixtures/e19_allin_trim` tapes the all-in + residual's own entry-bar call). `process_orders_on_close` becomes + `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2318-2319`); calc cadence and language publication are `PineScheduler`'s (`pine_scheduler.hpp:20`). - **Batching and open-order priority.** Same-bar command batching and its deferred queues (`PendingSameBarCommand` pine_adapter.hpp:1601, `pending_bracket_legs_` pine_adapter.hpp:1985-2003); the retained parent-before-child ordering of live handles — an @@ -232,25 +233,31 @@ kernel and own these quirks, each at its site: (`select_exit_activation` src/compat/pine/exit_activation.cpp:42, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:10, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:46) 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:9626-9631`); the half-tick + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:9591-9596`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:333) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11706`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11671`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` - `pine_adapter.cpp:454-460`, twin `tv_money_round` `pine_policy_support.hpp:9-15`). + `pine_adapter.cpp:370-376`, twin `tv_money_round` `pine_policy_support.hpp:9-15`). - **Quantity dust.** After every applied execution the Pine host erases any lot of at most `kQtyEpsilon` (`1e-10`) from the book it shares with the kernel, with no closing row - (`on_native_applied` `pine_strategy_host.cpp:478`), inside a live book as well as at a flat + (`on_native_applied` `pine_strategy_host.cpp:350`), inside a live book as well as at a flat reset — broader than `ab9714be`, which reset only a whole book at or under that size. Measured by R5 lane H-MEASURE (`tests/test_pine_dust_sweep_paired.cpp`): TradingView's decimal quantities leave no remnant row on a `lab tv` tape, which the Pine host books row for row - while a bare host books four 2.8e-17-unit dust rows beside them. + while a bare host books four 2.8e-17-unit dust rows beside them. R5 lane H-THIN measured every + quantity-epsilon comparison of the Pine host and adapter, 85 of them, over three seeded + witness families (5,500 runs) and the 610 CTest rows that link the library: 32 decide + somewhere (their exact form answers differently), 50 never do, and three are unreached. Made + exact, the sweep and three floors' `+1e-6` (the lot floor, the percent-exit floor and the 4x + 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:2027-2038`); the POOC reservation-growth population predicate + ledger (`close_logical_units_` `pine_adapter.hpp:2023-2034`); 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:14119`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14188`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:14069`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:14105`) 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 @@ -261,20 +268,21 @@ 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:16892`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16804`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). The pre-open admission slice stays too, though the kernel's `AfterApplied` point - after the open fill measures the same bar. Measured by R5 lane H-MEASURE + after the open fill measures the same bar; since R5 lane H-THIN it sizes through the shared + `source_margin_units` rule instead of its own copy of the 4x. Measured by R5 lane H-MEASURE (`tests/test_adapter_margin_schedule_differential.cpp`: the same bars and orders through the adapter and through a bare host whose hooks answer TradingView's money and slice): the pre-open - slice parts from the kernel's point, and from `ab9714be`, on the one-contract band and on the - restore's `+1e-6` floor (on the frozen units too, until R5 lane PAR-MARGIN froze a - default-percent stop entry above 100 % at its snapped level, or at the signal close when - marketable, as eight `lab tv` tapes show TradingView does: its book and its entry-bar call are - that quotient's, never the fill's; `ab9714be` sized such an entry at the fill); a gap-open - breach executes at the + slice parted from the kernel's point, and from `ab9714be`, on the one-contract band and on the + restore's `+1e-6` floor until R5 lane H-THIN sized it through the shared rule, and on the frozen + units until R5 lane PAR-MARGIN froze a default-percent stop entry above 100 % at its snapped + level, or at the signal close when marketable, as eight `lab tv` tapes show TradingView does + (its book and its entry-bar call are that quotient's, never the fill's; `ab9714be` sized such an + entry at the fill); with both it books the kernel's units; a gap-open breach executes at the open, sized on the open's money, where no kernel check kind does; the opening gate declines a 1x long whose entry fee `AdmitWithHostMargin` admits, and admits a gap-up add the adapter refuses against its signal-time equity. A leveraged opening is checked on its own entry bar: @@ -309,23 +317,23 @@ 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:8317`), the first-open execution chain and its - own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:680`), and the two fills Pine refuses to - recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5666`). + (`next_source_path_waypoint` `pine_adapter.cpp:8282`), the first-open execution chain and its + own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:663`), and the two fills Pine refuses to + recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5641`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the three session flags, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` - `pine_strategy_host.hpp:857`), which `scheduler_update_session_state` - (`pine_strategy_host.cpp:1765`) selects from the kernel's session-day facts before each source + `pine_strategy_host.hpp:849`), which `scheduler_update_session_state` + (`pine_strategy_host.cpp:1385`) selects from the kernel's session-day facts before each source callback — the host computes no session-day rule of its own. Generated code reads two of them; a generated `session.ismarket` still calls the time-of-day predicate - (`pine_session_ismarket` `pine_strategy_host.hpp:606`), which on six `lab tv` tapes misses every + (`pine_session_ismarket` `pine_strategy_host.hpp:599`), which on six `lab tv` tapes misses every Sunday open of a weekday-masked overnight session and every bar of `0000-2400`, where the kernel's fact matches TradingView on every bar (R5 lane H-MEASURE, `tests/test_session_ismarket_tape.cpp`; routing it is a codegen change); `barstate_islast_`, still a `BacktestEngine` member (`engine.hpp:378`) only this host writes; the live-tail / - probe-suppress overrides (`set_realtime_tail` `pine_strategy_host.hpp:446`, - `set_probe_suppress_tail_logic` `pine_strategy_host.hpp:472`), whose live-probe protocol is + probe-suppress overrides (`set_realtime_tail` `pine_strategy_host.hpp:439`, + `set_probe_suppress_tail_logic` `pine_strategy_host.hpp:465`), whose live-probe protocol is also the one Pine policy left over the session flags: its batch's final bar reads the kernel's open-ended close. @@ -363,19 +371,20 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files 927-961 of that tree, between `round_to_mintick` and `bar_fill_price`), deleted by R5 lane E6; 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:454-460`). `strategy.close` batching and the entry-id ledger - are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2027-2038`) and + adapter-side (`source_money_round` `pine_adapter.cpp:370-376`). `strategy.close` batching and the entry-id ledger + are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2023-2034`) 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:424`); + `source::PineStrategyHost::set_margin_call_enabled` (`pine_strategy_host.hpp:417`); what enables the generic model is the presence of `NativeRunSpec::margin`. So is the string-sentinel decoding of adapter-written comments: `closed_trade_close_cause` (`src/engine_trade_accessors.cpp:158`) reads a typed `execution::CloseCause` off the row, compares no string, and answers `3` for a *kernel* liquidation as readily as for the adapter's. What is live is one pair of hashed Pine-shaped lot flags: `skip_entry_bar_high` / `skip_entry_bar_low` (`engine.hpp:153`), hashed (`skip_entry_bar_high` `src/engine_state_hash.cpp:66`) and, since R5 lane E6, set by no host at - all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` - `pine_strategy_host.cpp:549`) and the kernel derives the pair — the intrabar-fill excursion - mask. Rule 5 is now scoped to that pair's storage; the comment residue it also covered was + all: a host that owns lot excursions declares where its fill sat + (`declare_opened_lot_entry_bar_mask` `src/engine_path_resolve.cpp:95`; no in-tree host has + since R5 lane H-THIN, when the Pine host left excursions to the kernel's sampler) and the kernel + derives the pair — the intrabar-fill excursion mask. Rule 5 is now scoped to that pair's storage; the comment residue it also covered was deleted by R5 lane E6 (Section A below). - **The wider coupling inventory, re-derived.** `docs/design/native-feature-parity.md` §2.ii lists the kernel↔TradingView couplings; three of the ones earlier drafts named are closed. @@ -395,11 +404,11 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `src/c_abi.cpp` implements the **58** codegen-facing runtime `PF_API` symbols that `scripts/check_c_abi_runtime.py` pins by name, and `src/native_c_host.cpp` implements the **43** additive symbols of ``. A C host creates a host from a - callback table (`strategy_native_host_create_v1` `native_c_api.h:2646`), runs a batch - (`strategy_native_run_v1` `native_c_api.h:2659`), and **submits, replaces, cancels and - executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2687`), `_replace_v1` - (`native_c_api.h:2709`), `_cancel_v1` (`native_c_api.h:2749`), `_cancel_all_v1` (`native_c_api.h:2753`), `_cancel_where_v1` (`native_c_api.h:2774`), - `_execute_current_v1` (`native_c_api.h:2795`) — under the kernel's own legality rule. Streaming needs no new + callback table (`strategy_native_host_create_v1` `native_c_api.h:2639`), runs a batch + (`strategy_native_run_v1` `native_c_api.h:2652`), and **submits, replaces, cancels and + executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2680`), `_replace_v1` + (`native_c_api.h:2702`), `_cancel_v1` (`native_c_api.h:2742`), `_cancel_all_v1` (`native_c_api.h:2746`), `_cancel_where_v1` (`native_c_api.h:2767`), + `_execute_current_v1` (`native_c_api.h:2788`) — under the kernel's own legality rule. Streaming needs no new symbol: the whole `strategy_stream_*` family takes that handle unchanged. The header's COVERAGE block carries one line per public member of `NativeStrategyHost` with either its C spelling or the reason it has none, and `scripts/check_native_c_api_surface.py` proves the @@ -551,7 +560,7 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 |---|---|---| | a replacement that keeps its handle or carries a book binding -- `ReplaceOptions::keep_handle` / `keep_binding`, and the definition's `priority` and `kept_binding` beside them (`include/pineforge/native_order.hpp`); the book held in priority order with a re-priced row's alias (`WorkingRequestCore`, `src/native_order.cpp`) | any C++ host, per replace command (`NativeStrategyHost::replace(handle, request, options)`); the Pine adapter sets `keep_binding` on every re-issue (`PineExecutionAdapter::submit_or_replace`) and never `keep_handle`; no C spelling | **generic, opt-in** (R5 lane V19-D). Rule 2 (a), mechanism: re-pricing a working order in place and keeping a close bound to an unchanged position are venue-neutral order-management facts, spelled with no platform word. `keep_handle` keeps the predecessor's handle, lineage, dependents and a trail's arm ordinal, and ranks the request by a priority number taken from the incarnation counter (the number a plain successor would have taken), so ties order a re-priced request newest and `issued()` never sees an incarnation twice; `keep_binding` binds at the next point, unrecorded, to the very book close the `CloseBoundEvent` would have installed and takes that event's ordinal, and binds as a plain successor when the book moved. Rule 2 (b), knob: none -- a host names the option per command; a replace without options is the plain replace, value for value. Pinned by `tests/test_native_handle_stable_replace.cpp` (kernel-only: plain vs each option set over randomized scripts, direct and staged paths) and `tests/test_adapter_live_state_equivalence.cpp` (`--reissue-binding`: the Pine battery carrying vs plain). | | `ta::ema_na_warmup_flag()` — the calling thread's ambient default for `ta::EmaSeeding`, `false` = `FirstValue`, `true` = `SimpleAverage` (`include/pineforge/ta.hpp`, `src/ta_moving_averages.cpp`; held in the thread's runtime block, `src/runtime_ambient.hpp` -- the thread's own, or the running pump's while one runs, R5 lane D2-C) | three source-layer RAII scopes (`PineStrategyHost` chart dispatch, `pine_security_eval.cpp`, `pine_aux_security.cpp`) raise it around one evaluation context under the opt-in `chart_ema_na_warmup` / `security_range_start_na_warmup` run flags; nothing in the kernel raises it | **retained as a generic ambient indicator option** (R5 lane P2). An EMA seeds from its first finite input or from the simple average of its first `length` inputs; both are textbook, and the kernel names them per instance (`EMA(length, EmaSeeding)`), which is a bare host's spelling and touches no global. The ambient default is what an instance that names no seeding latches on its first `compute()`: a per-thread default for a per-computation option, the shape of a floating-point rounding mode, and the only zero-wiring way to seed every instance of one evaluation context when the generated strategy constructs its own `ta::EMA` members (so relocating it into the source layer would take a codegen change). The adapter uses it the way any host may; the kernel's spelling and comments carry no TradingView vocabulary. The accessor's name is kept: `na` is the kernel's own NaN spelling and eight twin-parity-frozen CHECK texts of `test_chart_ema_na_warmup` spell it. **Ruled non-state (R5 lane F6), so no identity folds it.** Nothing that identifies a run reads the flag -- not the broker-state hash, not the continuation digest, not a per-bar record -- and outside the three scopes it is always the process default `false`, because each scope restores the previous value on exit, unwinding included: between two host calls, where a stream continues, there is no value to carry. Its one reader, `EMA::compute()`, copies it once into the instance's own `na_warmup_` (`warmup_latched_`), which is state of that indicator instance, owned by the script that constructed it exactly as the recursion's running value is, and which a continued stream keeps -- already latched. What selects the value is configuration, not state: the two run flags that raise it, `chart_ema_na_warmup_` and `security_range_start_na_warmup_`, are `PineStrategyHost` configuration, waived as such in `scripts/broker_state_hash_waivers.txt`; folding them into the Pine run identity would be the adapter's decision, and folding the latch itself would fold a constant. Pinned by the ambient-default cases of `tests/test_ta_na_source_rules.cpp` (G7, kernel-only) and by `test_chart_ema_na_warmup_l4d`. | -| `skip_entry_bar_high` / `skip_entry_bar_low` — one physical lot's entry-bar excursion mask (`PyramidEntry`, `include/pineforge/engine.hpp`): the end of the lot's entry bar the modeled path had already reached before that lot's own opening fill, folded into the run hash (`src/engine_state_hash.cpp`) and handed back to the excursion owner on the closing row's `ClosedLotExcursionFacts` | **no host, since R5 lane E6.** The owner of the lot's excursion (RULING A48, `owns_lot_excursions`) declares where its fill sat — `declare_opened_lot_entry_bar_mask` with `OpenedLotFillPoint::OnPath` or `::AfterPath` — and the kernel derives both flags from the bar's own path, walked in the leg order the run declares (`NativeExecutionConsumer::path_high_first`, `first_touch_position`; the next row). A C host declares through `strategy_native_declare_opened_lot_entry_bar_mask_v1` (R5 lane E11), legal inside `on_applied` alone. The source adapter's two former writer sites in `PineStrategyHost::on_native_applied` are two calls; nothing outside the kernel assigns a lot's flags | **retained as the entry-side half of RULING A48** (R5 lane E6). It is generic under rule 2: what the declaration carries is where a fill sat on a bar, which any venue's host can answer, and the platform-specific half — which of ITS orders is a priced entry — stays in the adapter that knows it. It is not a knob under rule 2b: the leg order and the first-touch position are decisions the run has already made, in the matcher and in the closing row, and a host that wants the leg order stated says so once through `NativeRunSpec::path_order`, and since R5 lane E15 the seam derives the mask under exactly that declared order (until then it read the open-proximity rule whatever the run declared). Pinned by `tests/test_e6_entry_bar_mask_declaration.cpp` (kernel-only) and `tests/test_l10d_entry_bar_excursion_masks.cpp` (the adapter's rows). **Removable at the next epoch, and only then:** the two booleans are durable lot state folded into the run hash, inside a POD the settlement ABI freezes, so dropping them moves the hash stream and a layout. The mechanism that replaces them needs no kernel storage — the excursion owner already keys its own tables by `entry_incarnation`, so it can keep the mask beside them and pass it in the facts it receives — and the kernel's own second reader of the pair is gone: the fold behind `preceding_exit_path_prefix` had no writer anywhere in the tree, so R5 lane E10 deleted the branch and the member (`tests/test_e10_dead_path_prefix.cpp`). The pair is now read only where it is handed to the excursion owner. | +| `skip_entry_bar_high` / `skip_entry_bar_low` — one physical lot's entry-bar excursion mask (`PyramidEntry`, `include/pineforge/engine.hpp`): the end of the lot's entry bar the modeled path had already reached before that lot's own opening fill, folded into the run hash (`src/engine_state_hash.cpp`) and handed back to the excursion owner on the closing row's `ClosedLotExcursionFacts` | **no host, since R5 lane E6.** The owner of the lot's excursion (`owns_lot_excursions`; the Pine host was one, RULING A48, until R5 lane H-THIN) declares where its fill sat — `declare_opened_lot_entry_bar_mask` with `OpenedLotFillPoint::OnPath` or `::AfterPath` — and the kernel derives both flags from the bar's own path, walked in the leg order the run declares (`NativeExecutionConsumer::path_high_first`, `first_touch_position`; the next row). A C host declares through `strategy_native_declare_opened_lot_entry_bar_mask_v1` (R5 lane E11), legal inside `on_applied` alone. The source adapter's two former writer sites in `PineStrategyHost::on_native_applied` are two calls; nothing outside the kernel assigns a lot's flags | **retained as the entry-side half of RULING A48** (R5 lane E6). It is generic under rule 2: what the declaration carries is where a fill sat on a bar, which any venue's host can answer, and the platform-specific half — which of ITS orders is a priced entry — stays in the adapter that knows it. It is not a knob under rule 2b: the leg order and the first-touch position are decisions the run has already made, in the matcher and in the closing row, and a host that wants the leg order stated says so once through `NativeRunSpec::path_order`, and since R5 lane E15 the seam derives the mask under exactly that declared order (until then it read the open-proximity rule whatever the run declared). Pinned by `tests/test_e6_entry_bar_mask_declaration.cpp` (kernel-only) and `tests/test_l10d_entry_bar_excursion_masks.cpp` (the adapter's rows). **Removable at the next epoch, and only then:** the two booleans are durable lot state folded into the run hash, inside a POD the settlement ABI freezes, so dropping them moves the hash stream and a layout. The mechanism that replaces them needs no kernel storage — the excursion owner already keys its own tables by `entry_incarnation`, so it can keep the mask beside them and pass it in the facts it receives — and the kernel's own second reader of the pair is gone: the fold behind `preceding_exit_path_prefix` had no writer anywhere in the tree, so R5 lane E10 deleted the branch and the member (`tests/test_e10_dead_path_prefix.cpp`). The pair is now read only where it is handed to the excursion owner. | | the leg order under which `declare_opened_lot_entry_bar_mask` derives a lot's entry-bar mask | nothing new: the run's `NativeRunSpec::path_order`, declared once before the run, by a C++ host in the spec, by a C host in the spec extension's feed-policy block, and by the Pine adapter from its embedder's `set_path_order` / `strategy_set_path_order` (`PineStrategyHost` projects it at begin) | **the order the matcher walks** (R5 lane E15, rule 2). The seam asks the consumer (`NativeExecutionConsumer::path_high_first`: the declared order, the open-proximity rule under `Auto`, the resolution `deliver_confirmed_script` walks) and finds the fill's first touch on that same walk. Until E15 it read `internal::bar_path_uses_high_first`, whose thread-local override only `NativePathOrderScope` installed (both deleted in R5 lane D2-A), around the intrabar sampler alone and never around a host callback, so every declared order got the open-proximity mask: the low-first bar O100 H110 L99 C105 with an `OnPath` fill at 105 gave (high, low) = (0, 1) under all three orders, where the HIGH_FIRST walk reaches 105 on its first leg and masks nothing. The ruling: the mask is derived from the same path order the matcher walks. The run already made that decision through `path_order`, so this is not a knob; it is the existing choice applied consistently. Consequence: a run under a forced order now gets different masks, and so different owner excursions. Codegen declares no order, and the adapter declares one only when its embedder calls `set_path_order` (in this repository only `scripts/run_strategy.py --path-order`, which `run_corpus.sh` never passes and no `runtime_overrides` key carries), so the corpus runs `Auto` and stays byte-identical by construction, and the wave-D population sweep judges any forced-order probe. Pinned by the forced-order rows of `tests/test_e6_entry_bar_mask_declaration.cpp` and of the entry-bar mask scenario of `tests/test_native_c_api.c`. | | the latched continuation — `BacktestEngine::last_script_continuation_hash_` / `last_script_continuation_valid_` (`include/pineforge/engine.hpp`), the continuation a run's final `broker_state_hash()` folds | the Pine host at its last script point (`PineStrategyHost::capture_script_continuation_hash`: the last batch bar, each leftover input of an aggregated chart, every realtime bar); the consumer at every `KernelRecorded` report point (`record_script_report_point`); `BacktestEngine::reset_run_state` clears it at every begin | **taken at once** (R5 lane V19-A, reverting R5 lane PERF-P1's view). Rule 2 (a), mechanism: any host that latches its continuation latches it the same way, and nothing in it knows Pine. Rule 2 (b), knob: none. PERF-P1 latched a view -- the fold's bytes, the command history's and the driver log's digests left as holes at the logs' lengths, folded on first read -- because the v18 fold was those two logs, about a fifth of a Pine run. The v19 fold (`native-consumer/v9`) is the consumer's live state alone, so a view records as many words as the fold mixes and saves nothing a reader does not pay back; the view, its recording sink and the `defer_continuation_views` switch are deleted, and every value is the one the view answered. Pinned by `tests/test_native_continuation_view.cpp` (kernel-only) and `tests/test_adapter_continuation_view.cpp`, re-pinned once for v19 and unmoved by the revert. | | the engine fields a run projects from its spec — the capital, point value, FX scalar and curve, tick, commission and the instrument and zone strings, compared with the applied spec (`NativeExecutionConsumer::PumpScope`, `check_projection`, `src/native_execution_consumer.hpp`) | the kernel, once, when a run begins; no host writes them -- the Pine adapter declares every one through the spec, and a write by any host is outside the contract, which forbids writing protected engine fields | **compared per pump** (R5 lane V19-C): at run begin, before every in-callback execution (`execute_current`, whose own precondition it is), and at the two ends of every pump -- a batch before its first input and after its last, a stream at each public input -- where it was compared at every callback and policy-hook boundary. Rule 2 (a), mechanism: a check cadence of the kernel's own, identical for every host, with no platform branch. Rule 2 (b), knob: none -- no spec field or hook chooses it. What moves, for a host that writes a projected field inside a pump and only for it: the failure latches at the pump's end with the pump's operation (`Input` or `Stream`) and ordinal 0, and the run goes on with the configuration it wrote until then, fills and notifications included. Pinned by `tests/test_native_projection_witness.cpp` (every projected field against every hook, re-harvested to the pump-boundary outcome), `tests/test_native_fx_activation.cpp` and `tests/test_native_current_execution.cpp`. | @@ -671,16 +680,16 @@ 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:342`), `source_level_on_price_grid` (`:328`), `nearest_tick` (`:266`) / `source_bar_fill_tick` (`:287`) / `directional_tick` (`:311`), 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`, `tests/test_native_price_grid.cpp` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13953`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:15008`), `chart_day_key` (`pine_adapter.cpp:13850`), `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:324`) and `configure_native` (`pine_strategy_host.cpp:325`) 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:13271-13272`): 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:2361-2363`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). A recorded divergence: the retention stands pending a re-lowering ruling | `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:2388-2396`, `margin.maintenance_long` `pine_adapter.cpp:2403`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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` | +| `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:258`), `source_level_on_price_grid` (`:244`), `nearest_tick` (`:182`) / `source_bar_fill_tick` (`:203`) / `directional_tick` (`:227`), 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`, `tests/test_native_price_grid.cpp` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13953`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:15008`), `chart_day_key` (`pine_adapter.cpp:13850`), `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:242`) and `configure_native` (`pine_strategy_host.cpp:243`) 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:13226-13227`): 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:2193-2195`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). 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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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:2326-2328`) | 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` | +| `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:2325-2327`) | 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:1584`): 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:1565`) 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 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 close ending within it of an interior lot boundary is one selected `Flatten` of whole lots (`source_fifo_prefix_openings`, `src/source/pine_adapter.cpp`), 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 | `tests/test_native_quantity_tolerance.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 close ending within it of an interior lot boundary is one selected `Flatten` of whole lots (`source_fifo_prefix_openings`, `src/source/pine_adapter.cpp`), 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.9) and kept them: made exact, the sweep fails its own tape row (`test_pine_dust_sweep_paired`) | `tests/test_native_quantity_tolerance.cpp` | What the table does not cover, on purpose: request kinds and host members the adapter never emits or @@ -704,30 +713,30 @@ number names its symbol on this tree. | C++ hook | C route | witness in `tests/test_native_c_api.c` | |---|---|---| -| `WaitForApplied::first_match` / `WaitForApplied::scope` | `arm_first_match` (`native_c_api.h:2185`), `pf_native_request_v1`'s fifth published layout; a Book-scoped child may be the host-sized close `on_close_units` sizes | the arm-relation scenario | -| `native_sized_units` | `strategy_native_sized_units_v1` (`native_c_api.h:3060`), reading a SIZED request's own sizing block | the sizing-query scenario | -| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2571`): price, opening shape, grid policy, beside the units half `on_close_units` | the terms-hook scenario | -| `validate_execution_precommit` | `on_precommit` (`native_c_api.h:2580`): the plan, the inspection and the projected account, the closed rows' P&L borrowed for the call | the precommit scenario | -| `resolve_anchored_level` | `on_anchored_level` (`native_c_api.h:2587`) | the anchored-level scenario | -| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2598`): a 64-bit digest folded after the kernel's own bytes | the hash-extension scenario | +| `WaitForApplied::first_match` / `WaitForApplied::scope` | `arm_first_match` (`native_c_api.h:2178`), `pf_native_request_v1`'s fifth published layout; a Book-scoped child may be the host-sized close `on_close_units` sizes | the arm-relation scenario | +| `native_sized_units` | `strategy_native_sized_units_v1` (`native_c_api.h:3053`), reading a SIZED request's own sizing block | the sizing-query scenario | +| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2564`): price, opening shape, grid policy, beside the units half `on_close_units` | the terms-hook scenario | +| `validate_execution_precommit` | `on_precommit` (`native_c_api.h:2573`): the plan, the inspection and the projected account, the closed rows' P&L borrowed for the call | the precommit scenario | +| `resolve_anchored_level` | `on_anchored_level` (`native_c_api.h:2580`) | the anchored-level scenario | +| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2591`): a 64-bit digest folded after the kernel's own bytes | the hash-extension scenario | **What stays as it is, and why.** | surface | ruling | reason | |---|---|---| -| `cancel_all` (`native_host.hpp:1267`) / `cancel_where` answer a count, not a `CancelResult` per request (E12 f7) | **retained** | A bulk cancel is many commands in one call, and each withdrawn request records its own `CancelledEvent` with its own `CancelReason` — dependants of a cancelled owner included — in the history a host already polls (`strategy_native_events_v1`). That is the per-request answer; the count is the call's summary. A result vector would restate the history in a second, allocating shape that C could not take without a caller-sized array. The C spellings answer the same count (`strategy_native_cancel_all_v1` `native_c_api.h:2753`). | +| `cancel_all` (`native_host.hpp:1267`) / `cancel_where` answer a count, not a `CancelResult` per request (E12 f7) | **retained** | A bulk cancel is many commands in one call, and each withdrawn request records its own `CancelledEvent` with its own `CancelReason` — dependants of a cancelled owner included — in the history a host already polls (`strategy_native_events_v1`). That is the per-request answer; the count is the call's summary. A result vector would restate the history in a second, allocating shape that C could not take without a caller-sized array. The C spellings answer the same count (`strategy_native_cancel_all_v1` `native_c_api.h:2746`). | | `cohort_add` (`native_host.hpp:1285`) / `cohort_remove` answer `void` (E12 f7) | **retained; the typed receipt is a named follow-up** | The kernel judges every enrolment into a `CohortReceipt` (Applied, InvalidHandle, UnknownOrigin, TerminalOrigin) and folds it into the continuation (`hash_cohort_receipt` `native_execution_consumer.cpp:890`, since v19 once, at the enrolment, into the running digest the continuation carries), so a replay that diverges there diverges in the hash. Nothing that matches or settles reads it: a roster is a relation the close reads at its match, and a refused enrolment is a member the close does not take. A host that needs the receipt AT the call needs `NativeStrategyHost::cohort_add_result` first, a kernel-header change outside this lane; its C spelling would then be an `_ext_v1` with a receipt word. Until then `strategy_native_cohort_add_v1`'s `PF_NATIVE_OK` means the enrolment was issued. | -| `native_sized_units` (`native_host.hpp:1314`) / `native_liquidation_price` answer `std::optional` (E12 f7) | **retained** | Observation queries, not commands: each empty is documented cause by cause in its own comment (unconfigured, non-positive money or denominator, a below-one-step quotient; no margin model, no maintenance fraction for the side, a flat book, no finite solution), and every cause is a fact the host can read itself. A typed reason would name what the caller already knows. C answers `PF_NATIVE_ABSENT` and NaN (`strategy_native_liquidation_price_v1` `native_c_api.h:3032`). | -| `pf_native_working_v1` without the leg's anchor or owner relation (E13 f4) | **carried** | The C++ host reads them off `NativeWorkingRequest::definition`, so the C readout's relation tail carries them: `anchor` (`native_c_api.h:1690`) through `arm_scope`, the request as the kernel holds it now (an armed leg reads ABSOLUTE with its installed level). | +| `native_sized_units` (`native_host.hpp:1314`) / `native_liquidation_price` answer `std::optional` (E12 f7) | **retained** | Observation queries, not commands: each empty is documented cause by cause in its own comment (unconfigured, non-positive money or denominator, a below-one-step quotient; no margin model, no maintenance fraction for the side, a flat book, no finite solution), and every cause is a fact the host can read itself. A typed reason would name what the caller already knows. C answers `PF_NATIVE_ABSENT` and NaN (`strategy_native_liquidation_price_v1` `native_c_api.h:3025`). | +| `pf_native_working_v1` without the leg's anchor or owner relation (E13 f4) | **carried** | The C++ host reads them off `NativeWorkingRequest::definition`, so the C readout's relation tail carries them: `anchor` (`native_c_api.h:1683`) through `arm_scope`, the request as the kernel holds it now (an armed leg reads ABSOLUTE with its installed level). | | `hash_source_extension` (`engine.hpp:411`) | **C++-only** | The deprecated spelling of `hash_host_extension`, kept so an existing C++ subclass compiles and folds unchanged. A C host has only ever had the current spelling, `on_hash_extension`. | -| `NativeReportPolicy::KernelRecordedAtHostMarks` (`native_run_spec.hpp:64`) | **C++-only** (lane E22's ruling, restated) | Under it the host names each report point from inside its own callbacks, and the C table has no call that marks one: a C host could only declare a series nobody records. `pf_native_report_policy_e` (`native_c_api.h:657`) leaves its value unnamed, and the enum guard holds that exclusion. A C++ host now reaches the same kernel producer through `NativeStrategyHost::mark_native_report_point`; a mark outside a running HostMarks run returns false. | -| `strategy_set_path_order` (`pineforge.h:967`) clamps an out-of-range mode to AUTO | **retained** | A `void` ABI v4 setter cannot answer a refusal. Its values are now documented as `pf_native_path_order_e`'s (`native_c_api.h:900`); a C host that wants an unknown word refused declares `path_order` in `pf_native_run_spec_ext_v1`, which answers `PF_NATIVE_E_TAG`. | +| `NativeReportPolicy::KernelRecordedAtHostMarks` (`native_run_spec.hpp:64`) | **C++-only** (lane E22's ruling, restated) | Under it the host names each report point from inside its own callbacks, and the C table has no call that marks one: a C host could only declare a series nobody records. `pf_native_report_policy_e` (`native_c_api.h:661`) leaves its value unnamed, and the enum guard holds that exclusion. A C++ host now reaches the same kernel producer through `NativeStrategyHost::mark_native_report_point`; a mark outside a running HostMarks run returns false. | +| `strategy_set_path_order` (`pineforge.h:967`) clamps an out-of-range mode to AUTO | **retained** | A `void` ABI v4 setter cannot answer a refusal. Its values are now documented as `pf_native_path_order_e`'s (`native_c_api.h:904`); a C host that wants an unknown word refused declares `path_order` in `pf_native_run_spec_ext_v1`, which answers `PF_NATIVE_E_TAG`. | | the enumerated `int32` fields of `pf_pending_order_v1_t` (`strategy_pending_order_get` `pineforge.h:1061`) | **no C names in the header; generator-owned** | The mirror is generated from the Pine source host's resting-order record by `scripts/gen_pending_order_mirror.py` (held by its `--check` source guard) and self-described at run time by `strategy_pending_order_layout`. Its enumerated fields are the source layer's order vocabulary, not a kernel enumeration. A C name for them belongs in that generator, which owns the struct, so that it cannot drift from the mirror — never hand-written beside it. | | `strategy_configure_native_v1` (`c_abi.cpp:896`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:3982`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | | `strategy_configure_native_fx_curve_v1` (`pineforge.h:584`) | **typed additive route** | `strategy_configure_native_fx_curve_ext_v1` writes the C twin of `NativeFxCurveValidation` and its offending index. The old `-1` route remains unchanged, while a C caller can now distinguish a decreasing timestamp, non-positive rate, allocation failure or wrong phase. | | `on_native_timeframe_bar` (`native_host.hpp:885`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:809`) | **query routed** | The callback signature stays frozen. `strategy_native_timeframe_bar_interval_v1` is valid only during that callback and copies the kernel calendar interval without deriving it from the delivered bar or delivery timestamp. | | `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7663-7675`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1085-1090`) sees the same values, so no hash contract moves. | -| `pf_native_lot_excursion_v1` (`native_c_api.h:1876`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | +| `pf_native_lot_excursion_v1` (`native_c_api.h:1869`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | ## TradingView-calibrated kernel mechanisms (rule 2's own account) @@ -767,25 +776,25 @@ The table records, at the tree the doc wave read, what each block described and | `src/engine_run.cpp`, lines 162-186 | `process_orders_on_close` semantics | the spec field `NativeCloseExecution::AfterCalculation` (`NativeCloseExecution::AfterCalculation` `native_run_spec.hpp:35`) | | `include/pineforge/engine.hpp`, lines 1262-1330 | TradingView freezes default market-order sizing at the signal bar | `native_order::SizePrice` (`Signal`, `SignalOnTick`) with `SizeTime::AtAcceptance` — a request value that names no platform | | `include/pineforge/engine.hpp`, lines 1352-1381 | TradingView liquidates intrabar, before the bar-close script body | `NativeLiquidationCheck` (`NativeLiquidationCheck` `native_run_spec.hpp:158`), whose three values are three broker models | -| `include/pineforge/engine.hpp`, lines 918-952 (the row first named lines 73-180, which were and are live code: `ClosedLotExcursionFacts`, `PyramidEntry`, `Trade`) | the ten-significant-digit money rule | adapter-side arithmetic (`source_money_round` `pine_adapter.cpp:454-460`) | +| `include/pineforge/engine.hpp`, lines 918-952 (the row first named lines 73-180, which were and are live code: `ClosedLotExcursionFacts`, `PyramidEntry`, `Trade`) | the ten-significant-digit money rule | adapter-side arithmetic (`source_money_round` `pine_adapter.cpp:370-376`) | ### B. Live kernel mechanisms with a calibrated number — each passes the amended rule 2 | kernel site | the mechanism | (a) generic because | (b) a knob would be | pinned by | |---|---|---|---|---| -| `utc_floor_day_ms` `src/timeframe.cpp:153` (the `TradingView` comment `sha256:1868eeadaab06b664c5ba966a86e70d6de2ad74d3645afe1846ed736d0c2afce` :146-149) | daily boundaries are anchored at symbol-local midnight | the anchor is `spec.timezone`'s data, not a platform's; another venue is driven by naming its zone | a "which midnight" field over a zone the spec already names | `tests/test_native_calendar.cpp`, `tests/test_session_day_anchors.cpp` | -| `session_period_last_traded_close_ms` `src/timeframe.cpp:818` (the `TradingView` comment `sha256:a650d166e35a1e22188ff5c3ee50c5534519730913a3fe713cf0b93b0f66d4ca` :808-817) | a D/W/M period is finished at the last traded close of its last session | every venue calendar must answer "is this period over"; the answer is computed from `spec.session` and `spec.timezone` | a per-run completion-rule switch over a choice the session template already makes | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_session_calendar_extra.cpp` | -| the session-day split `crosses_boundary` `src/timeframe.cpp:906-912` | calendar periods split on the session-day clock, not the civil one | continuous-session instruments need a session-day clock whatever platform asks; the class comes from `spec.type` | hash-visible surface for what `spec.type` decides | `tests/test_session_day_anchors.cpp` | -| the aggregated bar's label `bar_label_ms` `src/timeframe.cpp:1068-1075` | an aggregated bar is dated by the period it opens | a label rule is needed to key any bucket; this one is the period key | a label-policy field for a key the aggregator owns | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_calendar_wm_open_utc_fastpath.cpp` | -| real-end and chart-close completion (`Real-end completion` `src/timeframe.cpp:1195-1216`, `Chart-close completion` `:1240-1259`) | a thin or session-clipped intraday bucket completes on the bar whose end reaches the bucket end, or on the session's last bar | both are "the period is over" tests over feed data; a gap-free 24x7 feed is bit-identical either way, which the comment states and the tests pin | a completion-mode field over a bucket the calendar already closes | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_native_htf_subscriptions.cpp` | -| the native period partition installed by `set_native_periods` `src/timeframe.cpp:1621` (the holiday-pause `TradingView` comment `sha256:7bd87c1e6dfe78729221d412952249a8cb427934ef498d3a4deb6812f7af05d7` :1405-1410) | a session that pauses and reopens the same day is one session day | the rule reads the feed's own stamps; the holiday is data | a holiday table in the kernel | `tests/test_calendar_aggregation_wm.cpp` | +| `utc_floor_day_ms` `src/timeframe.cpp:154` (the `TradingView` comment `sha256:1868eeadaab06b664c5ba966a86e70d6de2ad74d3645afe1846ed736d0c2afce` :146-149) | daily boundaries are anchored at symbol-local midnight | the anchor is `spec.timezone`'s data, not a platform's; another venue is driven by naming its zone | a "which midnight" field over a zone the spec already names | `tests/test_native_calendar.cpp`, `tests/test_session_day_anchors.cpp` | +| `session_period_last_traded_close_ms` `src/timeframe.cpp:814` (the `TradingView` comment `sha256:a650d166e35a1e22188ff5c3ee50c5534519730913a3fe713cf0b93b0f66d4ca` :808-817) | a D/W/M period is finished at the last traded close of its last session | every venue calendar must answer "is this period over"; the answer is computed from `spec.session` and `spec.timezone` | a per-run completion-rule switch over a choice the session template already makes | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_session_calendar_extra.cpp` | +| the session-day split `crosses_boundary` `src/timeframe.cpp:902-908` | calendar periods split on the session-day clock, not the civil one | continuous-session instruments need a session-day clock whatever platform asks; the class comes from `spec.type` | hash-visible surface for what `spec.type` decides | `tests/test_session_day_anchors.cpp` | +| the aggregated bar's label `bar_label_ms` `src/timeframe.cpp:1064-1071` | an aggregated bar is dated by the period it opens | a label rule is needed to key any bucket; this one is the period key | a label-policy field for a key the aggregator owns | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_calendar_wm_open_utc_fastpath.cpp` | +| real-end and chart-close completion (`Real-end completion` `src/timeframe.cpp:1191-1212`, `Chart-close completion` `:1236-1255`) | a thin or session-clipped intraday bucket completes on the bar whose end reaches the bucket end, or on the session's last bar | both are "the period is over" tests over feed data; a gap-free 24x7 feed is bit-identical either way, which the comment states and the tests pin | a completion-mode field over a bucket the calendar already closes | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_native_htf_subscriptions.cpp` | +| the native period partition installed by `set_native_periods` `src/timeframe.cpp:1617` (the holiday-pause `TradingView` comment `sha256:7bd87c1e6dfe78729221d412952249a8cb427934ef498d3a4deb6812f7af05d7` :1405-1410) | a session that pauses and reopens the same day is one session day | the rule reads the feed's own stamps; the holiday is data | a holiday table in the kernel | `tests/test_calendar_aggregation_wm.cpp` | | `bar_path_uses_high_first` `src/engine_path_resolve.cpp:28`, `compute_ohlc_path_legs` `src/magnifier.cpp:97`, `entry_stop_first_touch` `src/engine_internal.hpp:101-109` | with only OHLC the intrabar walk order is unknowable, so the open-proximity heuristic picks the first leg | **and the knob exists**: `NativeRunSpec::path_order` (`NativePathOrder` `native_run_spec.hpp:335`) states `HighFirst` / `LowFirst` explicitly | already spelled, and `Auto` is the default that moves no identity | `tests/test_native_run_spec.cpp`, `tests/test_native_price_grid.cpp` | | `round_to_mintick` `include/pineforge/engine.hpp:753` (the `TradingView` comment `sha256:ba90d695d99260d555d1f44bd05110f4977eeb1e54323e6362ea6bd451572399` :737-750) | nearest-tick rounding, ties away from zero | a tick ladder is the instrument's, declared as `price_tick`; the generic surface over it is `NativePriceGrid` + `NativeGridRounding` | TradingView's **per-order-kind** quantization is exactly the knob that would not be generic — it is the measured content of the `price_grid` native-only ruling above | `tests/test_native_price_grid.cpp` | | `ta::ATR` `include/pineforge/ta.hpp:321` (the `TradingView` comment `sha256:4f6f6939aec2773684b114a13dd002eb58990d6d96c3056ad87361753faf7a0a` :333-340) | average true range over Wilder's `RMA` | Wilder's ATR is the textbook definition; the comment records which convention and the tape that pinned the recompute behaviour | a seeding/averaging field on an indicator whose definition is the convention | `tests/test_ta_rma_warmup.cpp`, `tests/test_recompute.cpp` | | `ta::StdDev` `include/pineforge/ta.hpp:430` (the `TradingView` comment `sha256:7cc1bbcd32dfe673285d51a8ca7080890ac551858099b523480d998f46408666` :420-429) | the population (biased) standard deviation | biased vs sample is a textbook choice, and the kernel states which | a per-instance switch is the *right* answer where two conventions genuinely compete, which is why `ta::EmaSeeding` exists and this one does not need it | `tests/test_ta_indicators_extras.cpp` | | `float_band_eq` `include/pineforge/ta_compare_band.hpp:38` | an absolute 1e-10 float comparison for indicator equality | an epsilon band is needed by any float comparison; the width is the calibration | a band-width field on a comparison the indicators share | `tests/test_dmi_parity.cpp`, `tests/test_ta_osc_edge.cpp` | | the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8621-8624` (lookahead) and `clear_if_gapped` `:8615-8618` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | -| `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2235`, `::gaps` `include/pineforge/native_c_api.h:2239` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:795`, `pf_native_gaps_e` `:806`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | +| `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2228`, `::gaps` `include/pineforge/native_c_api.h:2232` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:799`, `pf_native_gaps_e` `:810`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | | the authoritative-feed partition `src/engine_aux_security.cpp` | a feed is the venue's own bars of one timeframe; its stamps are the period partition | already ruled: design §2.ii row s, and the residual table's own row | the policy knob is installing the feed or not | `tests/test_native_htf_subscriptions.cpp`, `tests/test_native_security_feed.cpp` | | `session_template_knows_early_close` `src/engine_security.cpp` | the instrument-class classification | already ruled: design §2.ii row t; `SymInfo::type`'s vocabulary is fixed by the frozen C ABI | — | `tests/test_native_wm_buckets.cpp` | | `skip_entry_bar_high` / `skip_entry_bar_low` `include/pineforge/engine.hpp:153` | the intrabar-fill excursion mask: the part of a bar traversed before a priced entry filled is not that trade's excursion | the mask is generic — an excursion is measured from the fill — and since R5 lane E6 no host raises the flags: the excursion owner declares where its fill sat and the kernel derives the pair from the bar's path (`declare_opened_lot_entry_bar_mask` `src/engine_path_resolve.cpp:95`); with no declaration the kernel samples the whole bar | already spelled the generic way: `owns_lot_excursions()` + `closed_lot_excursion` hand the whole measurement to a host | `tests/test_l11a_host_excursion.cpp` | @@ -872,6 +881,13 @@ pushes still refuse an in-session gap under either slot-label policy. This is a feed admission asymmetry, documented with the stream contract; it introduces no new kernel policy. +### Wave H H-THIN: rule 2 rulings + +| Kernel capability | Generic mechanism | Why no platform knob is added | Executed witness | +|---|---|---|---| +| One civil-date helper: `native_calendar::native_civil_date` and `native_civil_days` (`include/pineforge/native_calendar.hpp`) | Proleptic-Gregorian day arithmetic (Hinnant's `civil_from_days` / `days_from_civil`) in integers. The calendar, the time-of-day fields, the timeframe helpers, the report's month keys and the Pine adapter's chart-day key read it, where each kept its own copy. | A pure function of its arguments: a calendar has no venue variant and nothing to choose. | `tests/test_chart_day_key_arithmetic.cpp` and `tests/test_utc_month_key_arithmetic.cpp` against `gmtime_r`; `tests/test_native_calendar_hash_witness.cpp` unmoved | +| The aggregation predicate: `native_calendar::pairing_aggregates` and `NativeStrategyHost::native_aggregates_input_bars()` (`include/pineforge/native_host.hpp`) | Whether the pairing the kernel resolved for a run's input and script timeframes makes its script bars buckets gathered from the input (a same-unit or fixed multiple, a fixed or calendar input under a calendar script bar) rather than the input bars themselves. | A query of the pairing the run already resolved: it reads the spec and decides nothing. The Pine adapter's copy read the two literals and parted from the kernel on one-bucket and monthly pairings. | `tests/test_aggregates_input_bars_literals.cpp` | + ## Boundary rules (for contributors) 1. TradingView/Pine parity for *new* work goes only in `src/source/` / `src/compat/pine/` or in diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 1824b5ab..229a79b2 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,93 +47,93 @@ 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:2738-2749; `entry()` pine_adapter.cpp:5314, `order()` pine_adapter.cpp:10515) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5698-5745; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2737-2748; `entry()` pine_adapter.cpp:5289, `order()` pine_adapter.cpp:10480) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5698-5745; `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:8417 → `Flatten{}` pine_adapter.cpp:8446-8447; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2708-3915 → `host.replace` pine_adapter.cpp:2724-2804) | **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:4723-4769, `cancel_all` pine_adapter.cpp:18120-18135; `live_by_source_key_` pine_adapter.hpp:1999; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4065-4130) | **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. | +| OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:2707-3914 → `host.replace` pine_adapter.cpp:2723-2803) | **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:4722-4768, `cancel_all` pine_adapter.cpp:18032-18047; `live_by_source_key_` pine_adapter.hpp:1995; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5538-5573, `cohort_for` pine_adapter.cpp:2502; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7508-7525) | **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:2747-2753) | **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 | — | +| 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:5513-5548, `cohort_for` pine_adapter.cpp:2501; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7473-7490) | **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:2746-2752) | **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:13049; lowering `submit_one` pine_adapter.cpp:9075-9087, `BindCohort` pine_adapter.cpp:9576-9582, `submit_leg` pine_adapter.cpp:9600-9624; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8680-8683; `materialize_relative_exits` pine_adapter.cpp:17410-17451, pine_adapter.hpp:1824-1836) | **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:5624-5658. `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:1240-1256; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19194-19223) | **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:2001-2019); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:1142-1221); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3469-3636, `materialize_snapshot` pine_adapter.cpp:10915-10939), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1585-1608, `CloseCallsiteState` pine_adapter.hpp:1679-1694; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15719, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15953) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:881-913) | 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:1844); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1918). 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:13004; lowering `submit_one` pine_adapter.cpp:9040-9052, `BindCohort` pine_adapter.cpp:9541-9547, `submit_leg` pine_adapter.cpp:9565-9589; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8645-8648; `materialize_relative_exits` pine_adapter.cpp:17322-17363, pine_adapter.hpp:1820-1832) | **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:5624-5658. `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:1226-1242; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19083-19112) | **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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:975-1054); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10880-10904), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1146-1173, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15631, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15865) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:714-746) | 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:1677); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1751). What `tests/test_adapter_margin_revival_cancel.cpp` replays on the NYSE:F 15m chart of seven lab tv tapes: badapter-v19dp1-control (no cancel: the margin call closes 928 through X) and v19fix-gapped-stop-control (X's stop at 10.15, gapped by the 05-01 open and parked: the same close), and badapter-v19dp1-cancel, v19fix-cancel-before-pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair (a cancel on 05-01 09:30 while X is dormant, on 04-30 15:45 before the reversal pair and on 04-30 10:00 while X is live, and on 05-01 09:30 and after the pair while X is parked; one tape file: margin calls 12 @10.39 and 92 @10.57, 836 open). Untaped, the same row holds the tapes' trades for `strategy.cancel_all` on 05-01 and before the pair and for `strategy.cancel` after the pair, and on the same bars with no quantity grid — where the pair holds X in the cancel's own callback — every spelling books what the cancel on the dormant exit books | ### 1.2 Trailing | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:5710-5732; `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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11814-11826) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4464-4469) **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:11727-11864) stays A. | -| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4022-4068) | **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:287-289) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9626-9631, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11727-11864, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8650-8876, `trail_one_shot` pine_adapter.cpp:9715-9844 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR2 | Trail activation / offset in ticks or points, relative to the entry fill (`trail_points_to_ticks` include/pineforge/compat/pine/trail_ticks.hpp:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:279-281) | **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:11779-11791) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4464-4469) **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:11692-11829) stays A. | +| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4021-4067) | **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:282-284) is A. | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9591-9596, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11692-11829, retained best `retained_trail_best` pine_adapter.hpp:282-284, operand / restart rules `source_trail_points` pine_adapter.cpp:8615-8841, `trail_one_shot` pine_adapter.cpp:9680-9809 | 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:5927-5931, pine_adapter.cpp:5927) | **yes** — `Transact` in `OrderIntent` native_order.hpp:229 | K | — | F:C1 O:C1 S:S1 | — | +| SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5902-5906, pine_adapter.cpp:5902) | **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:10989-12193) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4935-4969 | 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:2596-2651) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4976-4978 **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: rounded equity `source_money_round` pine_adapter.cpp:2548, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:462 (→ SZ10) | -| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2557-2559) | **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:2609, `typed_entry_units` pine_adapter.cpp:2627; `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:2588; `percent_commission_live_equity` pine_adapter.cpp:4329-4369) | **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. | -| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2514-2529, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6680-6706) | **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`. | +| SZ3 | Percent-of-equity sizing (`default_sizing_units` pine_adapter.cpp:2595-2650) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4976-4978 **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: rounded equity `source_money_round` pine_adapter.cpp:2547, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:378 (→ SZ10) | +| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2556-2558) | **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:2608, `typed_entry_units` pine_adapter.cpp:2626; `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:2587; `percent_commission_live_equity` pine_adapter.cpp:4328-4368) | **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. | +| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2513-2528, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6645-6671) | **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:592, 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:4992-4995; 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:12524-12552, `compute_exit_reservation` pine_adapter.cpp:4412-4533) | **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:4464-4787, `reserved_exit_qty` pine_adapter.cpp:9002-9312) | **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:453-460, `source_money_floor_lot` pine_adapter.cpp:462; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4444-4459) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6741), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7948), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12780), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13376, pine_adapter.cpp:13429) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15767) | 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:2596), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2609, `typed_entry_units` pine_adapter.cpp:2627), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18790) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7210-7561; ledger `close_batch_bar_` pine_adapter.hpp:2042-2053; 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:12478-12506, `compute_exit_reservation` pine_adapter.cpp:4411-4532) | **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:4463-4786, `reserved_exit_qty` pine_adapter.cpp:8967-9277) | **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:369-376, `source_money_floor_lot` pine_adapter.cpp:378; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4443-4458) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | +| 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:6706), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7913), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12735), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13331, pine_adapter.cpp:13384) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15679) | 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:2595), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2608, `typed_entry_units` pine_adapter.cpp:2626), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18678) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7175-7526; ledger `close_batch_bar_` pine_adapter.hpp:2038-2049; 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:1074) | 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:2362) | **yes** — `allowed_open_directions` native_run_spec.hpp:597 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:595 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13271-13274) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6230-6279, cap left unset `max_open_lots` pine_adapter.cpp:2361-2363) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. A recorded divergence, retained pending a re-lowering ruling. | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13325-13390 and leaves the kernel gate unset pine_adapter.cpp:13324-13326) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13980-13981, `margin_pct` pine_adapter.cpp:13388) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:14085-14296, `submit_margin_call_units` pine_adapter.cpp:14257-14303, `schedule_margin_call_path` pine_adapter.cpp:18273-18374) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:14011-14047) | **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. | -| MG8 | Liquidation price query (`compute_liquidation_price` pine_strategy_host.cpp:180-201) | **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. | -| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16371-16387, `apply_fx_opening_margin_slice` pine_adapter.cpp:18375-18403) | **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:16862-16901) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13360-13363) | **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:13941-13955, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:15010-15040) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:15053-15287) | **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:13751-13830) | **no** — the calendar exists `session_day_ordinal` native_calendar.hpp:373-376, there is no risk-day ledger **Closed:** `NativeRiskDay::{SessionDay, CalendarDayInTimezone}` keys the risk day, and `native_risk_state()` native_host.hpp:1323 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 | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2361) | **yes** — `allowed_open_directions` native_run_spec.hpp:597 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:595 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13226-13229) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6018-6064, cap left unset `max_open_lots` pine_adapter.cpp:2193-2195) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. 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:8735-8741). | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13280-13345 and leaves the kernel gate unset pine_adapter.cpp:13279-13281) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13935-13936, `margin_pct` pine_adapter.cpp:13343) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:14035-14213, `submit_margin_call_units` pine_adapter.cpp:14174-14220, `schedule_margin_call_path` pine_adapter.cpp:18185-18262) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13583-13619) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; `tests/test_adapter_margin_schedule_differential.cpp` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). | +| MG8 | Liquidation price query (`margin_liquidation_price` pine_strategy_host.cpp:114-130) | **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:16283-16299, `apply_fx_opening_margin_slice` pine_adapter.cpp:18263-18291) | **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:16774-16813) | **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:1192-1200; gate `max_cons_loss_days` pine_adapter.cpp:13315-13318) | **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:13896-13910, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14922-14952) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14965-15199) | **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:13706-13785) | **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:1311 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:13969, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14028, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14774-14814, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14298-14425, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14298) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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, `tests/fixtures/margin_entry_bar/pm2-*`). 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 (`tests/fixtures/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; `tests/fixtures/half_tick_rounding`, PAR-MARGIN-2). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3818-3885, `record_market_review` pine_adapter.cpp:15291-15349) | n/a | A | — | S:R2 | (single-source) | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3817-3884, `record_market_review` pine_adapter.cpp:15203-15261) | 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:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2319-2320) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7540-7544 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16206) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2318-2319) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7540-7544 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16118) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `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:7027 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7078 (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:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680; `begin_coof_recalc` pine_adapter.cpp:5635, `flush_coof_tail` pine_adapter.cpp:16349, `end_coof_recalc` pine_adapter.cpp:5656) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | -| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7166-7170; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:285-287. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16261, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7166-7170; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:203-205. | | 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:7534-7653 **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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | -| CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:234-320) 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:5782), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:680, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1660-1667 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | +| CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:152-238) 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:5757), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1659-1666 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1484-1487, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1392-1548; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1485 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:486), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | +| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1483-1486, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1391-1547; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1484 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:486), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | | HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI `strategy_set_native_security_feed` c_abi.cpp:839) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1429-1445 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1517; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1447-1452 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:488 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8309 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:46-56). | -| HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:637) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:928 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | +| HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:630) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:928 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | | HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:21-220, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:241-413; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:515) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:255-336) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | | HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:875; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7906-7923, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7913, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | @@ -144,25 +144,25 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | 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:5698-5745, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6281-6289; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | | FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:584 applied once native_execution_consumer.cpp:4611-4612, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12032-12041) is A | -| FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:472-479) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:265-268, `source_bar_fill` pine_adapter.cpp:11997-12006, `directional_tick` pine_adapter.cpp:310-314) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4073; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6363-6364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | -| FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:341-389) is A on top. | +| FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:388-395) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:181-184, `source_bar_fill` pine_adapter.cpp:11962-11971, `directional_tick` pine_adapter.cpp:226-230) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4073; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6363-6364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:257-305) 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:8721). **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:348-396 (used `source_trigger_threshold` pine_adapter.cpp:9106-9111, `exit_limit_trigger` pine_adapter.cpp:9640-9661), `source_bar_fill_tick` pine_adapter.cpp:286-299, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11757) | 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:264-312 (used `source_trigger_threshold` pine_adapter.cpp:9106-9111, `exit_limit_trigger` pine_adapter.cpp:9640-9661), `source_bar_fill_tick` pine_adapter.cpp:202-215, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11757) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:2613-2626, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1021-1025 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). TV entry-bar masks are A (`sample_open_trade_extremes` pine_adapter.hpp:1226-1233). **Measured against TradingView (R5 lane H-MEASURE, `tests/test_e19_excursion_tape.cpp`):** the host-owned model is `ab9714be`'s, not TradingView's — where it and the kernel sampler differ the kernel's number is TradingView's (entry-bar over-mask, exit-bar over-fold after an at-open exit, a `calc_on_order_fills` scratch TradingView reports as 0 / 0); both miss a bar's open print ahead of a same-bar stop fill. On the corpus the two models differ on 341 of 431 390 trades, and of the 326 of those a TradingView export identifies, the kernel's number is TradingView's on 315 and the host's on none. Recorded divergences, retained for `ab9714be` compatibility. | -| RP2 | Closed-trade list, trade statistics, report struct | **yes** — `fill_report` engine_report.cpp:39-66, `get_trade` engine.hpp:1791-1828, `compute_trade_stats` engine_metrics.cpp:138 | K | — | F:H2 O:I3 S:X2 | — | +| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:2613-2626, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1021-1025 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). **Lowered by R5 lane H-THIN:** the Pine host keeps the kernel's `owns_lot_excursions()` default, so the sampler is the Pine excursion; the host model, its entry-bar masks and its caches are deleted, and the source layer's fold no longer carries them. **Measured against TradingView first (R5 lane H-MEASURE, `tests/test_e19_excursion_tape.cpp`):** the host-owned model was `ab9714be`'s, not TradingView's — where it and the kernel sampler differ the kernel's number is TradingView's (entry-bar over-mask, exit-bar over-fold after an at-open exit, a `calc_on_order_fills` scratch TradingView reports as 0 / 0); both miss a bar's open print ahead of a same-bar stop fill. On the corpus the two models differ on 341 of 431 390 trades, and of the 326 of those a TradingView export identifies, the kernel's number is TradingView's on 315 and the host's on none. The lowering moved the cells of 335 trades in 19 corpus probes: 324 now equal TradingView's, 11 moved closer, none farther; the all-in residual's own entry-bar call is tape `tests/fixtures/e19_allin_trim`. | +| RP2 | Closed-trade list, trade statistics, report struct | **yes** — `fill_report` engine_report.cpp:39-66, `get_trade` engine.hpp:1791-1828, `compute_trade_stats` engine_metrics.cpp:122 | K | — | F:H2 O:I3 S:X2 | — | | RP3 | Equity curve | **no** — `update_equity_extremes` engine.hpp:1362 / `record_equity_point` engine.hpp:1380 are protected and were called only by the adapter **Closed:** `NativeReportPolicy::KernelRecorded` native_run_spec.hpp:63 has the consumer fold the extremes and append one point per script calculation, labelled with the script interval's open; `KernelRecordedAtHostMarks` records the same series at the host's own marks (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I4 S:X2 | S's "partial" folds RP2 in | -| RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:219; `fill_metrics_section` engine_report.cpp:119-139 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | -| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1684) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1409) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | +| RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:203; `fill_metrics_section` engine_report.cpp:119-139 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | +| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1306) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1408) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | | RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9286, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | -| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1370, native_execution_consumer.cpp:10672; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | +| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1358, native_execution_consumer.cpp:10682; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | | RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:538; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1931 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:411 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:405 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:411 is its deprecated spelling, which the generic default forwards to (`tests/test_native_host_hash_extension.cpp`). | K | L2 (the virtual rides the shared bump) | F:I3 O:J1 S:X3 | — | @@ -171,13 +171,13 @@ 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:1856, refused in-run by `guard_native_mutation` engine.hpp:1860) | **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:2255-2256, `chart_timezone` pine_adapter.cpp:2296-2298) | **yes** — spec fields `ticker` native_run_spec.hpp:568-578; `parse_session` native_calendar.hpp:173-419; 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:2254-2255, `chart_timezone` pine_adapter.cpp:2295-2297) | **yes** — spec fields `ticker` native_run_spec.hpp:568-578; `parse_session` native_calendar.hpp:180-462; 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:2158, `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:896, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:940, probe `strategy_execution_contract` c_abi.cpp:873, stream `strategy_stream_begin` c_abi.cpp:624 **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:548 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:907 among them (F §2.iii) | +| OT5 | C-level run configuration, stream, FX curve, contract probe | **yes** — `strategy_configure_native_v1` c_abi.cpp:896, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:940, probe `strategy_execution_contract` c_abi.cpp:873, stream `strategy_stream_begin` c_abi.cpp:624 **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:548 omitted `intrabar`, `path_order` and `abort_reporting`; they now travel in the extension's policy tail, `pf_native_abort_reporting_e` native_c_api.h:911 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 58 runtime exports (and eight generated per-strategy exports) (`tests/test_native_c_api.c`, `tests/test_native_c_api_frozen_header.cpp`). | K | L13 | F:J6 O:J5 | R5-7: post-v1. S covers it in its §2.3 without a row. | -| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1461 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7286), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1765 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:857; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | -| OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:288-290) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | +| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1454 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7286), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1385 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:849; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | +| OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:206-208) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | --- @@ -202,17 +202,17 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | # | Item | Where / what it is used for | Class | Plan | Src | |---|---|---|---|---|---| -| a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:82-91) and of the rich `BacktestEngine::run` overload (engine.hpp:1780-1785, 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:292-295 | 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) | F O S | +| a | `source::StrategyOverrides` opaque pointer | Forward declarations in the kernel headers (since removed: no kernel header names the adapter type now); parameter of `run_rich` (execution_consumer.hpp:46-55, native_execution_consumer.hpp:82-91) and of the rich `BacktestEngine::run` overload (engine.hpp:1780-1785, 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:210-213 | 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) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:29 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:356-359 (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:901-903 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8782`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2309-2310. 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:260-261. **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:741) | 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:454-460) | 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 | +| 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:906-908 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8782`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2308-2309. 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:260-261. **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:370-376) | 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:6434 | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19194-19223 | 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:523, 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:66) and, since R5 lane E6, written by no host at all: the excursion owner declares where its fill sat (`declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549) 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:308-585) 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:604. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:424; 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 | +| 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:19083-19112 | 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:522, 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:66) 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:303-580) 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:604. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:417; 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:1274 | 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) | | m | `request.security` machinery | `register_security_eval` engine_security.cpp:27, `set_native_security_feed` engine_aux_security.cpp:76-127, `SecurityEvalState` engine.hpp:1156-1360 carry Pine semantics (lookahead, gaps, Heikin-Ashi, KI-55 range start) and are driven only by the source layer | HASH-VISIBLE / measured path | F planned to move both TUs and the state into the source host after native HTF. **R5-6 narrows this:** L6 reuses the feed store and routing, so those stay kernel; only the Pine-only evaluator semantics remain candidates. Do last. **Status (lane L12b): done** — the evaluator semantics are `source::PineSecurityEvalState` + `src/source/pine_security_eval.cpp`; the kernel keeps the registry, aggregator, feed store and one generic step | F S | | n | S-only proposals, not adopted | A generic `FillModel` interface with a TV model installed by the adapter (for the TradingView money / tick / path comment blocks in `engine.hpp` and the dead helpers of item e); "emit generic FIFO facts and let an adapter policy produce the KI-62 scratch" | — | Superseded by R5-3 (price-grid lane) and R5-1 (TV fill specifics stay behind `resolve_execution_terms`); the dead-helper part is item e; there is no scratch code left to re-lower (item d) | S | @@ -272,7 +272,7 @@ PF_API int pf_native_events_v1(pf_strategy_t, uint64_t after_ordinal, pf_native_ | Hardening | Tagged, size-prefixed PODs; `struct_size` + version; unknown-tag refusal; never cast the C request to a C++ variant; a C fixture compiles against the frozen previous header. S puts the API in a separate header `include/pineforge/native_c_api.h` | S | | SOP | Every symbol goes into `src/c_abi.cpp`, `pineforge.h`, `EXPECTED_RUNTIME` + both counts (check_c_abi_runtime.py:19-79), the README table and the Python harnesses, or the `source-guard-c-abi` stage fails, in preflight and every `ci_verify` profile (`CLAUDE.md`) | F O S | | Not in v1 of the C API | `resolve_execution_terms` / `validate_execution_precommit` are the adapter's seam; a C host that needs sizing uses `Sized` (F). O offers an optional `resolve_terms` callback: deferred. *History: R5 lane F4 routed both (`on_execution_terms`, `on_precommit`); the terms hook's units answer outside a host-sized close is still C++-only (the 1.0 C boundary table)* | F O | -| Open | Naming: F and O use `pf_native_*`; S uses `strategy_native_*`, which matches the existing runtime symbols. The v1 C spec `pf_native_run_spec_v1` pineforge.h:548 omitted `intrabar`, `path_order` and `abort_reporting`; the extension's policy tail carries all three now (`pf_native_abort_reporting_e` native_c_api.h:907) | F S | +| Open | Naming: F and O use `pf_native_*`; S uses `strategy_native_*`, which matches the existing runtime symbols. The v1 C spec `pf_native_run_spec_v1` pineforge.h:548 omitted `intrabar`, `path_order` and `abort_reporting`; the extension's policy tail carries all three now (`pf_native_abort_reporting_e` native_c_api.h:911) | F S | Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the epoch batch. @@ -281,14 +281,14 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the | | F (its §2.iv A) | O (its G10) | S (its P7) | Chosen | |---|---|---|---|---| | API shape | spec `subscriptions` + push callback `on_native_timeframe_bar` | `register_native_series` + pull `native_series_bar(id)` / `native_series_slot_is_new(id)` | spec `security_requests` + `configure_native_security_feed` + `native_security(id)` view | F's subscription in the spec + push callback, plus O's pull accessor | -| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:656-671, `contribute_input` native_execution_consumer.cpp:8030, `seal_script` native_execution_consumer.cpp:8007) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | +| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:661-676, `contribute_input` native_execution_consumer.cpp:8030, `seal_script` native_execution_consumer.cpp:8007) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | | Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7116), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | | Authoritative bars | `authoritative_bars` on the subscription | `Source::ExchangeFeed` through `set_native_security_feed` | `configure_native_security_feed(id, bars, tf)` | F's field, installed through the existing store | | Lookahead | none, by construction | `lookahead = false` default, available | `lookahead_on = false`, `gaps_on = true` | open → §4 U1 | 1. **Spec:** `std::vector subscriptions` (Appendix A.5), hashed only when non-empty (digest like `native_intrabar_path_digest`). 2. **Wiring**, all inside the native consumer at Ready → begin and only when `subscriptions` is non-empty (zero work for the adapter): register one evaluator state per subscription through the existing `register_security_eval` (engine.hpp:1295-1297, *fresh*); install `authoritative_bars` into the existing store (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129); call `prepare_native_security_feeds` (engine.hpp:1639, defined `BacktestEngine::prepare_native_security_feeds` engine_aux_security.cpp:128, *fresh*) — the "made native-callable" step: reachable from the native consumer, still refused from host code in-run; pump `feed_security_eval_state` (engine.hpp:1312-1314, *fresh*) from the accepted-input path; deliver each completed bucket through `on_native_timeframe_bar`. -3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1305-1389 and its call order. The per-run evaluator init is today a *source-layer* member (`PineStrategyHost::init_security_eval_states_for_run` pine_scheduler.cpp:9-39, declared `init_security_eval_states_for_run` pine_strategy_host.hpp:694, *fresh*), so L6 adds a kernel-side twin instead of moving it. +3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1305-1389 and its call order. The per-run evaluator init is today a *source-layer* member (`PineStrategyHost::init_security_eval_states_for_run` pine_scheduler.cpp:9-39, declared `init_security_eval_states_for_run` pine_strategy_host.hpp:687, *fresh*), so L6 adds a kernel-side twin instead of moving it. 4. **Delivery rule (F):** a subscription bar is delivered on the input bar that completes it — its last contributing input bar, or, for a `LazyComplete` bucket, the next period's first input bar — immediately before that input's script calculation, never earlier — and never before an earlier script interval's lazily sealed calculation has run: the pump is ordered against the script interval, `seal(k) → deliver(i+1) → calc(i+1)` (L6d). 5. **Interim, documented in L0:** `on_native_input` + `TimeframeAggregator` (HT6). Doc notes (O): `set_native_security_feed` is public but inert without evaluators and throws in-run (`guard_native_mutation` engine_aux_security.cpp:78, which latches `UnsupportedSource` native_execution_consumer.cpp:1455); `set_aux_security_feed` returns false in the kernel engine_consumer.cpp:158-160. 6. **Entanglement to put in the lane brief** (*fresh* read of `set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:46-78): the feed machinery is TV-calibrated. "W" / "M" buckets are built from the installed native daily bars, and the native stamps define the period partition (a holiday session folds into the next trade date's bar). A bare host that supplies `authoritative_bars` inherits those rules. Document them as the contract or gate them behind a partition policy — decide in the lane, not silently (§4 E8). @@ -298,7 +298,7 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the ### 2.v Promote the examples (L10) - Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3018-3086) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2969-3037) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:45, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -324,7 +324,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:1730, `guard_native_mutation` engine_aux_security.cpp:78 | 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). No kernel TU includes a source header | `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13 | 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:739 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:454-460 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` pine_strategy_host.cpp:549; the ledger `close_logical_units_` pine_adapter.hpp:2027-2038; `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:370-376 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:1996-2007; `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) | 58 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 58 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:13933f70987f6bfe37d3015ae065a8d5ee5937c37431d48e2ecd1f17a4c29958` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:862, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:590-709, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:515-567, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:494-511, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:802-833, c_abi.cpp:929 | 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 | @@ -408,11 +408,11 @@ 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:2358, 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:2357, 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:5629-5633) and `drain_applied_notifications` (native_execution_consumer.cpp:6290-6303) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | -| L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1305-1389 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | +| L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1304-1388 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | | L7 | `native_working_requests()`, `cancel_all()`, `cancel_where(label)`, `TriggerAnchor`, `TrailTicks`, zero offset, `ReplaceOptions`; toolkit `submit_bracket`, `OrderBook` → A.6 | new kinds unused by the adapter; anchor defaults to `Absolute`; the adapter's `tick * 0.5` sentinel keeps working | bracket + trailing probe through the builder: exact where TR5 / FP7 are inactive (on-grid levels) (F); the zero-offset trail test asserts the run no longer fails (O); parent-limit + stop sibling, parent rejection, replacement, parent-cycle revival (S) | | L8 | `NativePriceGrid { None, QuantizeFills, QuantizeFillsAndTriggers }`, `NativeGridRounding` → A.7 | `None` for the adapter, permanently — **native-only by ruling, audit lane P6: §3.6.2** | a host with `QuantizeFills` books on-grid prices; a second test pins that `QuantizeFillsAndTriggers` still differs from TV's half-tick threshold (FP5) (O) | | L9 | `NativeRiskLimits`, `NativeRiskEvent`, `MatchRejectReason::RiskLimit` → A.8 | `risk` unset for the adapter; its ledger (`SourceDayLedger` pine_adapter.hpp:1200) untouched — **retained after measurement, audit lane N12; native-only by ruling, audit lane P6: §3.6.1** | risk probes: same halt bar (F); each limit + the day-boundary basis (O); breach, forced close, cancellation, next-day reset (S) | @@ -499,8 +499,8 @@ inside the per-bar body (corpus 442d497, `set_pine_risk_direction(1)` / `set_pine_risk_max_position_size(2)` / `set_pine_risk_max_intraday_filled_orders(3)`), so the adapter first sees a limit on script bar 0, from `PineStrategyHost::set_pine_risk_*` -(pine_strategy_host.cpp:953-975) — after `prepare_native_begin` has already -projected the spec (pine_strategy_host.cpp:245) and `configure_native` has +(pine_strategy_host.cpp:952-974) — after `prepare_native_begin` has already +projected the spec (pine_strategy_host.cpp:163) and `configure_native` has folded it into the continuation digest. Measured: an env-gated probe printing the adapter's risk configuration inside `project()` on the corpus reads `max_drawdown=0 cap.active=0` for every risk probe. `NativeRunSpec::risk` is a @@ -521,12 +521,12 @@ line numbers are this tree's (683a82f); kernel semantics are | 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:2362); 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:13271-13272; 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:16862, `update_risk_state` pine_adapter.cpp:13912-13939): 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:13264), 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:1200-1208): +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:17724-17733); 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:16365); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16477), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:15040-15049, `submit_intraday_loss_close` pine_adapter.cpp:15010-15038), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17219); 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:5986, `:7655`, `:8379`, `:8507`, `:11484`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18118-18121) | `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:13903-13905); 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:13809-13830, `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:2361); 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:13226-13227; 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:16774, `update_risk_state` pine_adapter.cpp:13867-13894): 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:13219), 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:1195-1203): +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:17724-17733); 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:16277); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16389), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14952-14961, `submit_intraday_loss_close` pine_adapter.cpp:14922-14950), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17131); 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:5961, `:7620`, `:8344`, `:8472`, `:11449`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18030-18033) | `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:13858-13860); 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:13764-13785, `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, @@ -555,8 +555,8 @@ Pine run needs the source change the patch makes, and the in-process pins remain the five probes' baseline rows and the `SW` check above. **The P6 decision: native-only.** Re-verified on main `b8e7976e`: the ordering -stands (`project()` at pine_strategy_host.cpp:324, `configure_native` at :325, -the `set_pine_risk_*` setters at pine_strategy_host.cpp:992-1014, and corpus 442d497 +stands (`project()` at pine_strategy_host.cpp:242, `configure_native` at :243, +the `set_pine_risk_*` setters (`set_pine_risk_direction` pine_strategy_host.cpp:584-607), and corpus 442d497 `corpus/validation/cap-risk-gates-allow-max-intraday-01`'s generated strategy still emits the three calls inside `on_source_bar`); `rg "spec\.risk|NativeRiskLimits" src/source include/pineforge/source` is still empty; `test_adapter_risk_relower` still @@ -740,7 +740,7 @@ rule (L8b) is the opposite on purpose — one rule for every kind. Rule 2's test applies literally: the mask cannot be justified without the word "TradingView". And what the adapter keeps is not a duplicate of the kernel mechanism that a re-lowering would delete: `source_trigger_threshold` -(pine_adapter.cpp:342), `source_level_on_price_grid` (:328) and the +(pine_adapter.cpp:258), `source_level_on_price_grid` (:244) and the `nearest_tick` (pine_adapter.cpp:257) / `source_bar_fill_tick` (:278) / `directional_tick` (:302) spellings are a *different* rule — a half-tick threshold per order kind with the fill booked elsewhere (E7) — which is why re-lowering moves outcomes @@ -789,7 +789,7 @@ still in the table. | `price_grid`, `grid_rounding` | native-only | §3.6.2 | | `risk` | native-only | §3.6.1 | | `max_abs_units` | adapter-policy: the pre-fill live-book gate is TradingView's | row MG2; N12's `PS` measurement (book 4 against 2) | -| `max_open_lots` | adapter-policy: Pine pyramiding is a per-cycle entry count (measured against TradingView: not its rule, row MG3) | row MG3; the contract comment in `project()`; `tests/test_pyramiding_count_differential.cpp` | +| `max_open_lots` | adapter-policy: Pine pyramiding is a per-cycle entry count (measured against TradingView: not its rule, row MG3; R5 lane H-THIN's lowering trial moved four corpus probes away) | row MG3; the contract comment in `project()`; `tests/test_pyramiding_count_differential.cpp` | | `initial_margin_fraction` | adapter-policy: TradingView's money admission answers `AdmitWithHostMargin`; the declared `margin` model is maintenance-only | row MG4; the wave-4 ruling in `project()` | | `report_open_position_at_end` | adapter-policy: TradingView's range-end report is report shape, not a mark-to-market row | row RP5 | | `open_bar_view` | adapter-policy: TradingView's open scheduling and its fill callback read the whole bar | rows CT4, E5 | @@ -848,8 +848,8 @@ so the gross open profit is 800. | **the equity re-mark** | `last.open_profit = 0; last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl` | none — the kernel leaves the curve exactly as the run marked it | `RE1` (no fee): the adapter reports the curve's last point as `eq=100800 op=0`, the kernel as `eq=100800 op=800` — same equity, the open profit moved into the realized side. `RE2` (0.1 %): `eq=100758.8 op=0` against `eq=100800 op=800`; the adapter's equity is **41.2 lower**, exactly the round-trip commission of both lots (20.5 + 20.7), because the re-mark carries the NET row P&L where the run's own mark carried the GROSS open profit | retained: a report that restates the last equity point off net row P&L is TradingView's report shape. Re-marking the curve from the kernel would move a figure every bare host already reads | | **the extreme re-fold** | `max_equity_`/`min_equity_` reset to `initial_capital_`, `max_drawdown_`/`max_runup_` to 0, then `fold_equity_extreme` over the whole curve | none — the extremes stay as `update_equity_extremes` sampled them | `RE3` (a dip to 90 at bar 1, a peak of 108 at bar 3, 0.1 %): both sides agree on `maxeq=101400 mineq=99000 maxdd=2400`, and the run-up diverges — adapter `maxru=1758.8000000000029`, kernel `maxru=1800`. The re-fold is a no-op **except** where the re-marked last point differs, so the divergence is again the 41.2 round trip, modulo the binary fold order | retained: it is a consequence of the re-mark above, not a mechanism of its own; it cannot be re-lowered while the re-mark is adapter policy | | **the same-bar exit re-sort** | `sort_same_bar_exit_trades(trades_, adapter_)` — same-bar bracket exits ordered by script command sequence | none — the kernel never reorders `trades_` | not exercised by the four scenarios (it reorders CLOSED rows, not range-end rows); it is TradingView command-sequence ordering by construction (`git show ab9714be:src/source/pine_fills.cpp`, lines 664-670) | retained: script command sequence is a source-language fact | -| **the mark cadence** | three call sites, each clearing and rebuilding: the terminal sub-bar of the last source bar (`scheduler_record_range_end` pine_strategy_host.cpp:460), after every applied execution once terminal (`record_applied_range_end`, :650), and the scheduler's terminal-source-bar hook (`PineScheduler::applied` pine_scheduler_native.cpp:549, before the bar's broker hash) | once, after the last point of the run (`run_batch` ×3, `stream_end`) | structural, not a number: the adapter's rows are re-derived at each mark so the report reflects the book as of the last event on the terminal bar, and its re-marked equity is visible to that bar's broker hash. A one-shot run-end producer cannot occupy those three points | retained: three marks on a terminal bar is a TradingView report cadence, not a generic run-end fact | -| **the excursion projection gate** | `excursion_range_end_projection_` suppresses the owner's exit-bar path fold for the projection (`excursion_range_end_projection_` pine_strategy_host.cpp:857) | none — the generic builder folds what it folds | `RE1`: identical run-up/draw-down on both halves (`ru=500 dd=0`, `ru=300 dd=0`) because the adapter's own owner hook returns the carried magnitudes for a projection; the gate exists to keep it that way when a host owns lot excursions | retained: it is the host's excursion contract (`owns_lot_excursions`), which the kernel deliberately does not model | +| **the mark cadence** | three call sites, each clearing and rebuilding: the terminal sub-bar of the last source bar (`scheduler_record_range_end` pine_strategy_host.cpp:333), after every applied execution once terminal (`record_applied_range_end`, :649), and the scheduler's terminal-source-bar hook (`PineScheduler::applied` pine_scheduler_native.cpp:549, before the bar's broker hash) | once, after the last point of the run (`run_batch` ×3, `stream_end`) | structural, not a number: the adapter's rows are re-derived at each mark so the report reflects the book as of the last event on the terminal bar, and its re-marked equity is visible to that bar's broker hash. A one-shot run-end producer cannot occupy those three points | retained: three marks on a terminal bar is a TradingView report cadence, not a generic run-end fact | +| **the excursion projection gate** | `excursion_range_end_projection_` suppressed the owner's exit-bar path fold for the projection | none — the generic builder folds what it folds | `RE1`: identical run-up/draw-down on both halves (`ru=500 dd=0`, `ru=300 dd=0`) | retired by R5 lane H-THIN: the Pine host owns no lot excursion since, so no owner fold is left to suppress; `RE1` still reads the same magnitudes, now the kernel's | | **the tail / warm-up suppression** | `if (stream_warmup_mode_ \|\| realtime_tail_ \|\| equity_curve_.empty()) return;` | none — the kernel emits at `stream_end` whatever the run left open | structural: the kernel has no warm-up or realtime-tail notion, and `run_corpus`'s probe-tail suppression (`suppress_probe_tail`) is a harness fact | retained: a tail is a source-layer replay concept | **Control and witness.** `RE4` closes the lot at bar 2: neither side emits a @@ -908,15 +908,16 @@ this lane re-pins the affected v19 witnesses once. | ID | What TradingView and `ab9714be` do | What R4 slice C did | Owner and fix | Proof | |---|---|---|---|---| -| AG1 | A trade row counts **chart** bars: its entry and exit bar are the `bar_index` the script read there, so `bar_index - strategy.opentrades.entry_bar_index(0)` is the bars held, and a tape row one chart bar long says "Duration (bars)" 1 | The pre-K-IDX kernel used the first contributing INPUT slot as its public interval index on an aggregated run. That made a two-script-bar trade report input indices and made `avg_bars_in_trade` count input slots. | **Kernel (Option A).** `NativeExecutionConsumer::ScriptBucket::script_index` assigns the sequential script index when a bucket seals. `NativeCoordinate::interval_index`, `PyramidEntry::entry_bar_index`, `Trade::exit_bar_index`, open-lot rows and the metrics calculation all consume that script index. `NativeCoordinate::input_interval_index` preserves the first input slot. `PineStrategyHost::on_native_applied` no longer rewrites lots or rows; Pine's excursion and cohort sampling still pass their explicit chart source index to the source-layer sampler. | `kc_a310` / `kc_a310_c` in the K-IDX scratch: batch, stream and both magnified modes report `0/1/2`, entry/exit `1/2`, avg `2`; `tests/test_aggregated_path_regressions.cpp` and `tests/test_pooc_fill_stamp_aggregation.cpp` remain green. TradingView: `tests/test_aggregated_entry_bar_index_tape.cpp` (R5 lane H-MEASURE), a tape whose exits read the indices, booked field for field on all three paths. | -| AG2 | Every fill of a chart bar is dated at the bar's **open**: the e25-f-islastbar tape dates all 510 of its `process_orders_on_close` rows there, and `ab9714be`'s aggregated rows equal its chart rows | The kernel dates a close point (the calculation, the close after it, a modeled bar's close leg) at `calculation_time()`, the bucket's close, and a fill the host executes at the current point (a `calc_on_order_fills` first-open chain, a same-bar close-then-entry, the KI-62 cover) at its decision floor, which on the no-path route the bucket's inputs have already carried to the close. The chart timeframe's zero-width interval hides both, since every point of a bar sits at its label | **Adapter, at projection time.** `on_native_applied` dates the lot a fill opened (`PyramidEntry::time`, hence `Trade::entry_time`) and the rows it closed (`Trade::exit_time`) at the chart bar's open: on an aggregated chart every point of a confirmed bar's modeled path (its open, the segments, the calculation, the close after it), where the chart timeframe's own points all sit, and under the magnifier every sub-bar point too (each sub-bar's open, path and `ModeledOHLCClose`; lane E27 had cut it to the close points, R5 lane PAR-ORDERS widened it, TradingView's dating). A realtime print keeps its own instant, as the chart-timeframe stream keeps it, and so does a stream's quiet carried interval | the audit's pooc probe rows (`ab9714be`'s 19:45Z / 13:30Z on all three paths), row 4, the KI-62 row and the stream row (a chart-timeframe stream and the same stream fed 1m agree on every row, a 10:30:01 print fill and an 11:00 carried fill included); E27's stamp rows flipped. Measured: the chart path unchanged byte for byte, the magnified run moves only its POOC close-fill instants. TradingView itself dates a magnified fill at its chart bar's open too (218 of 218 fills on sixteen `lab tv` tapes, R5 lane H-MEASURE); the magnified run kept a sub-bar fill's own instant (71 of the 152 instants `tests/test_magnified_aggregated_tape.cpp` replays sat inside their bar) until R5 lane PAR-ORDERS, and the row now asserts every instant exactly | +| AG1 | A trade row counts **chart** bars: its entry and exit bar are the `bar_index` the script read there, so `bar_index - strategy.opentrades.entry_bar_index(0)` is the bars held, and a tape row one chart bar long says "Duration (bars)" 1 | The pre-K-IDX kernel used the first contributing INPUT slot as its public interval index on an aggregated run. That made a two-script-bar trade report input indices and made `avg_bars_in_trade` count input slots. | **Kernel (Option A).** `NativeExecutionConsumer::ScriptBucket::script_index` assigns the sequential script index when a bucket seals. `NativeCoordinate::interval_index`, `PyramidEntry::entry_bar_index`, `Trade::exit_bar_index`, open-lot rows and the metrics calculation all consume that script index. `NativeCoordinate::input_interval_index` preserves the first input slot. `PineStrategyHost::on_native_applied` no longer rewrites lots or rows; Pine's cohort sampling still passes its explicit chart source index to the source-layer sampler (its excursion sampling is the kernel's since R5 lane H-THIN). | `kc_a310` / `kc_a310_c` in the K-IDX scratch: batch, stream and both magnified modes report `0/1/2`, entry/exit `1/2`, avg `2`; `tests/test_aggregated_path_regressions.cpp` and `tests/test_pooc_fill_stamp_aggregation.cpp` remain green. TradingView: `tests/test_aggregated_entry_bar_index_tape.cpp` (R5 lane H-MEASURE), a tape whose exits read the indices, booked field for field on all three paths. | +| AG2 | Every fill of a chart bar is dated at the bar's **open**: the e25-f-islastbar tape dates all 510 of its `process_orders_on_close` rows there, and `ab9714be`'s aggregated rows equal its chart rows | The kernel dates a close point (the calculation, the close after it, a modeled bar's close leg) at `calculation_time()`, the bucket's close, and a fill the host executes at the current point (a `calc_on_order_fills` first-open chain, a same-bar close-then-entry, the KI-62 cover) at its decision floor, which on the no-path route the bucket's inputs have already carried to the close. The chart timeframe's zero-width interval hides both, since every point of a bar sits at its label | **Adapter, at projection time.** On a chart whose script bars the kernel gathers from the input (`native_aggregates_input_bars()`, the kernel's `pairing_aggregates` of the resolved pairing, since R5 lane H-THIN; the adapter's own literal test parted from it on one-bucket pairings such as `1D` on `D`), `on_native_applied` dates the lot a fill opened (`PyramidEntry::time`, hence `Trade::entry_time`) and the rows it closed (`Trade::exit_time`) at the chart bar's open: on an aggregated chart every point of a confirmed bar's modeled path (its open, the segments, the calculation, the close after it), where the chart timeframe's own points all sit, and under the magnifier every sub-bar point too (each sub-bar's open, path and `ModeledOHLCClose`; lane E27 had cut it to the close points, R5 lane PAR-ORDERS widened it, TradingView's dating). A realtime print keeps its own instant, as the chart-timeframe stream keeps it, and so does a stream's quiet carried interval | the audit's pooc probe rows (`ab9714be`'s 19:45Z / 13:30Z on all three paths), row 4, the KI-62 row and the stream row (a chart-timeframe stream and the same stream fed 1m agree on every row, a 10:30:01 print fill and an 11:00 carried fill included); E27's stamp rows flipped. Measured: the chart path unchanged byte for byte, the magnified run moves only its POOC close-fill instants. TradingView itself dates a magnified fill at its chart bar's open too (218 of 218 fills on sixteen `lab tv` tapes, R5 lane H-MEASURE); the magnified run kept a sub-bar fill's own instant (71 of the 152 instants `tests/test_magnified_aggregated_tape.cpp` replays sat inside their bar) until R5 lane PAR-ORDERS, and the row now asserts every instant exactly | | AG3 | A `calc_on_order_fills` recalculation runs the script **on the bar the fill is on** and reads that bar's `session.ismarket` / `isfirstbar` / `islastbar`; `ab9714be` set them before any fill of the bar | The scheduler set the flags in the close callback only, so a recalculation published the PREVIOUS bar's: a fill at the next session day's 09:30 read `islastbar`. On the chart timeframe the close then read its lookahead from a count the recalculation had already advanced, one retained bar too far: a fill on 15:30 flagged 15:30 `islastbar` and, through E26's dual, 15:45 `isfirstbar` | **Adapter** (Pine language state, OT7). `scheduler_update_session_state` stays the one writer and runs once per script bar, at the bar's first publication: `PineScheduler::recalculate` asks before the script runs, `PineScheduler::bar` pine_scheduler_native.cpp:446 only when no recalculation published the bar. Since R5 lane F5 it selects the kernel's session-day facts of that bar, which every callback of the bar carries alike, so the scheduler's lookahead is gone. E26's session-day rule and its `isfirstbar` dual are unchanged | the audit's coof_session probes and the 15:30 case in `tests/test_aggregated_path_regressions.cpp`; section 7 of `tests/test_session_islastbar_aggregation.cpp` (TradingView's NYSE:F days with `calc_on_order_fills`, chart, aggregated and magnified: every callback of a bar reads the tape's flags) | **Recorded, not changed here.** -- Under the magnifier on an aggregated chart, the entry-bar masks and - same-bar cohort checks still compare against the explicit chart source index - selected by the Pine sampler. They no longer mutate the kernel's script-bar +- Under the magnifier on an aggregated chart, the same-bar cohort checks + still compare against the explicit chart source index selected by the Pine + sampler (the entry-bar masks went with the host's excursion model, R5 lane + H-THIN). They no longer mutate the kernel's script-bar lot index; the K-IDX probes cover the kernel index and input companion. **Closed (R5 lane H-MEASURE):** all three entry-bar tests of a magnified aggregated run (the slippage mask, the same-bar excursion test, the @@ -1002,15 +1003,15 @@ row C). | **D** | the kernel's cohort rosters on a Pine host: O(openings) per row (V19-A's finding) | ×2.45, ×2.80, ×3.06 per doubling from 4,000 to 32,000 bars of a flip recorded per bar on `91d65ad6` (0.206 s to 4.33 s), against ×2.02 with recording off | **resolved** by R5 lane V19-FIX: the adapter takes an origin that can no longer be bound off its kernel roster, so the continuation folds the openings that still work or hold a lot | **A. The adapter's recording fold.** Until R5 lane V19-E (below), -`PineExecutionAdapter::hash_state` pine_state_hash.cpp:237 folded, at every +`PineExecutionAdapter::hash_state` pine_state_hash.cpp:236 folded, at every 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:287) — one per accepted request, cleared only by -`reset_for_run` pine_adapter.cpp:2109 — and reflected the whole admission +pine_state_hash.cpp:286) — one per accepted request, cleared only by +`reset_for_run` pine_adapter.cpp:1942 — and reflected the whole admission journal (since replaced by a running digest of its events, `admission_journal` -pine_state_hash.cpp:575). `PineScheduler::hash_state` pine_state_hash.cpp:613 +pine_state_hash.cpp:574). `PineScheduler::hash_state` pine_state_hash.cpp:612 re-folded the consumed source prefix too (`consumed` -pine_state_hash.cpp:620-642). Measured with recording on and off, in two +pine_state_hash.cpp:619-641). Measured with recording on and off, in two shapes: - the audit's order-and-cancel probe (one `strategy_order` accepted and @@ -1041,22 +1042,22 @@ moving a single value, but not from inside lane F9's files: 2. **The transforms need a home.** A row's transform, or a composed segment's, has to outlive the call. The fold is const; the sink is created per call (engine_state_hash.cpp:32); and none of `PlacementTable` - pine_adapter.hpp:366, `admission::Journal` market_admission.hpp:188 or + pine_adapter.hpp:361, `admission::Journal` market_admission.hpp:188 or `PineScheduler` has a member it could keep one in. Adding one is a header change in `include/pineforge/source/`, outside this lane. 3. **A cache must know which rows changed, and nothing records it.** `PlacementTable` hands out mutable rows — the non-const `find` - pine_adapter.hpp:502, the non-const `at` pine_adapter.hpp:518 and its + pine_adapter.hpp:497, the non-const `at` pine_adapter.hpp:513 and its iterators — and the adapter rewrites retained rows in place through them, not only rows still working. Lane F9's probe hashed every row callback to callback: the order-and-cancel shape rewrites none, the re-issue shape 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:8791). Full-table scans - rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1696, - `revive_brackets_after_margin` pine_adapter.cpp:1918, - `preserved_by_close_all` pine_adapter.cpp:11515), and + (`has_full_entry_bracket` pine_adapter.cpp:8756). Full-table scans + rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1529, + `revive_brackets_after_margin` pine_adapter.cpp:1751, + `preserved_by_close_all` pine_adapter.cpp:11480), 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. @@ -1141,16 +1142,16 @@ doubling from 4,000 to 32,000 bars (11.2 s at 32,000), and standing one (OL14), and a gapped stop a declined reversal had parked for the next margin-call slice came back after a cancel too. A script cancel now retires every leg of each exit row it names and drops such a parked stop -(`retire_cancelled_exits` pine_adapter.cpp:1844), the revival skips a withdrawn +(`retire_cancelled_exits` pine_adapter.cpp:1677), 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:2931). Both shapes now cost ×1.89 to ×2.12 per doubling up +pine_adapter.cpp:2930). 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:3362); a lot opened +it off (`release_closed_cohort_origins` pine_adapter.cpp:3361); 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 @@ -1207,8 +1208,8 @@ member of the heap-owned execution consumer (`ExecutionConsumerSlot` engine.hpp:1666) is not. The other is a contract that the six folded fields of a booked row are never rewritten. In the tree they are not; the only writer after booking, the Pine host, rewrites `close_cause`, `exit_from_bracket`, -`exit_bar_index`, `exit_comment` and `entry_incarnation` only; its -writes are `close_cause` pine_strategy_host.cpp:489, `exit_bar_index` :827, `exit_comment` :618, `close_cause` :632-637, `exit_from_bracket` :1248 and `entry_incarnation` :1429-1437. +`exit_comment` and `entry_incarnation` only; its +writes are `close_cause` pine_strategy_host.cpp:362, `exit_comment` :469, `close_cause` :483-488, `exit_from_bracket` :869 and `entry_incarnation` :1050-1058. But `trades_` engine.hpp:546 is a protected member any host can write, so a lane that takes the consumer-side digest has to make that contract explicit. Folding `(count, digest)` in place of the rows saves the 256-entry table but @@ -1239,9 +1240,9 @@ the bars cost 3.8 times the CPU with a round trip every ten bars `broker_state_hash()` folds the continuation as it stood at the run's last script point, not after the run's teardown, so that it is one value with recording on or off (`broker_state_hash_projection` -pine_strategy_host.cpp:176-184). `capture_script_continuation_hash` -pine_strategy_host.cpp:359-367 takes that snapshot at the last batch bar -(`last_batch` pine_strategy_host.cpp:463-473), and taking it is one full +pine_strategy_host.cpp:103-111). `capture_script_continuation_hash` +pine_strategy_host.cpp:277-285 takes that snapshot at the last batch bar +(`last_batch` pine_strategy_host.cpp:336-346), and taking it is one full continuation fold per run. Measured on the gated replay — `ReissueReplay` on 43,008 bars, `run()` process CPU, best of 5, three rounds — it is 0.2116 / 0.2137 / 0.2141 s with the capture and 0.1632 / 0.1614 / 0.1652 s with it @@ -1290,6 +1291,126 @@ unchanged after it. --- +### 3.9 Audit lane H-THIN — the adapter's duplicates of kernel capabilities + +The fourth audit's G2 residual (AUDIT4-opus §6 rows G2-01 to G2-39, its +item X15, and AUDIT4-codex A4-THIN, A4-PATH-ORDER and A4-DUST) asked that the +Pine adapter keep no mechanism that duplicates a kernel capability, that every +mechanism it does keep have a row here or in the ADR and a witness, and that +the size target be met or re-set by a measured ruling. The lane took R5 lane +H-MEASURE's measurements as its inputs. + +**X15, item by item.** "Byte-identical" means the corpus (312 probes), the +identity battery with its hash columns, the 100 public slots and the Pine +witness all print the same bytes before and after. + +| item | the adapter's copy | now | evidence | +|---|---|---|---| +| a | `compute_liquidation_price`, a second closed-form solve of the margin level | gone: `strategy.margin_liquidation_price` reads `native_liquidation_price()` in TradingView's tick spelling (row MG8) | bit-identical at every bar of the 1,021 margin-call runs of a 1,200-configuration seeded sweep; the 179 runs with margin calls switched off declare no margin model and read na (`tests/test_margin_call_l4a.cpp`) | +| b | `open_entry_fees_`, the cash-fee ledger the sizing basis restores | **kept, measured**: it is not a mirror of the kernel's per-lot `open_entry_commission` | the two part after a FIFO `strategy.exit(from_entry)` closes an older lot of another id (the ledger drains the named id's fee, the kernel the lot it closed): 1 of 400 seeded fee runs and three publication-witness configurations move. The `lab tv` tape `hthin-x15b-fifo-fee-basis` shows TradingView restores neither (§3.9 finding 1), so neither answer is TradingView's and the ledger stays byte-identical | +| c | `CohortFacts::live_units_by_origin`, the per-id unit ledger | **kept, measured**: it is the per-id logical exposure, not the kernel's lots | against the kernel lots booked under each id at every callback of 600 seeded runs it differs in 1,494 of 287,549 checks (1,489 beyond 1e-9): a close named by one id drains another id's older lot, and the ledger debits the named id. P10's reservation reads it as the entry's own quantity | +| d | the pre-open slice's own ×4 | gone: `schedule_preopen_margin_slice` sizes through `source_margin_units` (row MG7) | byte-identical; the M10 differential's band and fudge cases now book the shared rule's 1 and 1, the kernel's and `ab9714be`'s | +| e | six copies of the civil-date arithmetic | gone: `native_calendar::native_civil_date` / `native_civil_days` (ADR-0001, H-THIN rule 2 rulings) | byte-identical by construction; `tests/test_chart_day_key_arithmetic.cpp`, `tests/test_utc_month_key_arithmetic.cpp` | +| f | `aggregates_input_bars`, read from the two timeframe literals | gone: the kernel's `NativeStrategyHost::native_aggregates_input_bars()` (ADR-0001, H-THIN rule 2 rulings; row AG2) | byte-identical on the corpus and every witness; `tests/test_aggregates_input_bars_literals.cpp` counts the two classes where the copy parted from the kernel | + +Beside X15 the lane lowered the host-owned excursion model onto the kernel's +sampler (row RP1, E19), tried the pyramiding count on `max_open_lots` and kept +it (row MG3, M13), and fixed the one reservation divergence measured beside it +(P10). E19 and P10 are the lane's only moves: its one hash step re-pins every +witness they move, and the corpus baseline moves on the 19 probes whose +excursion cells moved toward TradingView. + +**AUDIT4 §6, re-derived** (MR measured retention, RU ruled unmeasured, UF +unfinished, RM removed). + +| rows | mechanism | AUDIT4 | now | +|---|---|---|---| +| 01, 02, 05, 07, 08, 11, 14, 16-20, 24-26, 30 | sizing floors and freezing, the FX step, the liquidation slice, margin money, the FX broker-open slice, `max_position_size`, the risk rules, the price grid, trail conventions, the second bracket path, the range-end shape, the auxiliary drive, `calc_on_order_fills`, percent-exit reservation, cancel loops | MR | MR (unchanged) | +| 03/04/06, 29 | sizing quotients and the fill-quantity probe, preview aggregation | RM | RM | +| 09, 10, 13 | margin-check scheduling, checkpoints as market executions, `AdmitWithHostMargin` | RU | MR (H-MEASURE M7, M8, M11) | +| 12 | the pre-open slice and its own ×4 | RU + copy | MR; the copy RM (X15 d) | +| 15 | pyramiding as a per-cycle count | RU | MR (H-MEASURE's tapes; H-THIN's lowering trial, row MG3); P10 fixed | +| 21 | short-seed report swaps | RU + GAP | MR (H-MEASURE's tape) | +| 22 | host-owned excursions | MR (partial) | RM (E19, row RP1) | +| 23 | `request.security` exclusions | RU + THIN | MR (H-MEASURE, the ADR `subscriptions` row) | +| 27 | aggregated re-stamp and re-dating | MR | MR; its predicate is the kernel's (X15 f, row AG2) | +| 28 | the dead hashed `path_order_` | RM / UF | RM: `fac8389a` deleted it and its fold | +| 31 | the second liquidation-price solve | UF | RM (X15 a) | +| 32 | the `1e-10` dust sweep | RU + GAP | MR (H-MEASURE's tape; the census below) | +| 33 | the entry-fee ledger | UF | MR (X15 b) | +| 34 | begin-bar validation | RU | RU: the kernel's ingress validation, ruled by E12 | +| 35 | the presented clock and index writes | UF | ruled: Pine presentation, not a kernel cursor copy -- `bar_index` counts script bars with the temporary index of a not-yet-sealed callback, and `last_bar_index` / `last_bar_time` freeze at the live tail's horizon, neither of which the kernel's `interval_index` models | +| 36 | `session.ismarket` through the legacy predicate | UF | RM on the kernel side (H-MEASURE's six tapes: `in_session` is TradingView's); the generated predicate is codegen's to route | +| 37 | the per-origin unit ledger | UF | MR (X15 c) | +| 38 | civil-date copies | RU | RM (X15 e) | +| 39 | `aggregates_input_bars` | UF | RM (X15 f) | + +**The quantity-epsilon census (A4-DUST).** Every quantity comparison with an +epsilon in `pine_strategy_host.cpp` and `pine_adapter.cpp`, 85 of them at 57 +sites, was measured three ways in a scratch copy of the tree that evaluates +each in its epsilon form and its exact form: over three seeded witness +families (5,500 runs of decimal quantities, dust-sized closes, pyramiding, +reversals, POOC, calc-on-fills, margin at 5-100 %, lot grids 1 to 0.001, fees, +multi-command bars, paired entries, sticky and trailing exits), over the 610 +CTest rows that link the library, and with each deciding comparison made exact +on its own. None is removed or moved: the kernel's opt-in `quantity_tolerance` +charges a snapped reduce its request and books every dust lot it closes as a +row, where these rules do neither (ADR-0001, its row). Three local copies of +the two shared constants were folded into them, byte-identical. The census +reads, per site (witness and CTest evaluations; "band" = evaluations where the +two forms disagree; runs moved of 5,500 and CTest rows failing with the exact +form): + +| sites | what the epsilon decides | evaluations | band | exact form | +|---|---|---|---|---| +| A1 `on_native_applied` sweep | a lot of at most `1e-10` is erased, without a row | 1,615,742 | 1,167 | 30 runs; fails `test_pine_dust_sweep_paired` (tape `hm-g232-decimal-dust`) | +| A2 flat reset; A3-A5 dust lots in the fee sum, the POOC FIFO view, the KI-62 cover | a dust book or lot is treated as none | 2,015,064 | 17 | no run, no row | +| B1, B2 POOC close flush | a flat book, and a close within `1e-10` of it as a `Flatten` | 3,110 | 4 | 4 runs | +| B3 `close` whole-id | a close within `1e-10` of the id's basis, or of 100 %, is whole | 190,291 | 4,648 | 365 runs | +| B4 `resolve_terms` scope cover | fill-time units within `1e-10` of the live scope are the scope | 367,513 | 284 | 214 runs | +| B5-B13 exit and reservation classes | full versus partial exits, re-issues, reservation expansion, the ANY-rule percent | 169,586,206 | 1,868 | no run, no row | +| C1 `source_fifo_prefix_openings`, EQ `enqueue_pooc_fifo_close` | a POOC FIFO close's whole-lot prefix and its batching | 3,978 | 4 | no run, no row | +| D1 `floor_quantity_grid` | the lot floor's `+1e-6` of a step | 227,717 | 14,182 | 1,437 runs; fails 14 rows | +| D2 `quantize_close_units` | a full close is the basis snapped to the grid | 383,394 | 11,172 | 349 runs | +| D3 `quantize_percent_exit_units` | the percent-exit floor's `+1e-6` of a step | 19,749 | 1,084 | 511 runs; fails 5 rows | +| D4-D9, D11-D15 reservation arithmetic | dust capacity, full families, re-issued brackets | 81,360,970 | 270 | D7: 54 runs; the rest none | +| D10 `close` all-in percent | an all-in close's reversal handling | 36,778 | 1,571 | 3 runs | +| E1, E2 cohort drain | a cohort origin at most `1e-10` is drained | 608,401 | 895 | 16 runs | +| M1-M4 `source_margin_units` | a dust restore, the second floor's `+1e-6`, the one-contract band, a dust slice | 56,680 | 395 | M2: 74 runs, fails 3 rows (`test_tv_money_band_l4b` among them); the rest none | +| M5-M12 margin-call carries and revivals | the one-unit call, deferred short checks, resting whole closes | 69,554 | 1,256 | no run, no row; M9 and M10's second test unreached | +| Q1-Q6, O1-O2 paired-market and short-seed identities | own versus gross frozen units, exact quantity identities | 1,099,295 | 0 | none | + +Of the 85, 32 decide somewhere and 50 never do; three (B12's explicit-quantity +test, M9, M10's explicit-quantity test) are reached by no seeded run and no +CTest row. Every site keeps its reason at the line (an `ab9714be` citation or +a TradingView rule); the four whose exact form fails a pinned row are the +sweep and the three `+1e-6` floors. + +**Size (AUDIT4's metric: the direct children of `src/source`, +`include/pineforge/source`, `src/compat/pine` and +`include/pineforge/compat/pine`, `.cpp` and `.hpp`).** 29,701 lines at the +audit's tree (`91d65ad6`), 29,920 at this lane's base (`0d76a099`), 29,059 +after it: 861 fewer, below the owner's ruling (at most 29,693, aiming at +about 29,350 once the measured duplicates went). The earlier target of 27,008 +stays out of reach for the reason H-MEASURE measured: the growth since round +3 is V19 state lifetime and the performance lanes, not duplicated mechanism. + +**Findings, recorded and not changed here.** + +1. TradingView sizes a percent-of-equity entry under a cash-per-order fee as + `floor((pct × E − fee) / close)` on the tick-printed quantity grid, where + `E` is the equity with every open lot's pro-rata entry fee charged: the + tape `hthin-x15b-fifo-fee-basis` fits that rule on all six of its sized + entries, and the corpus probe `order-percent-equity-cash-commission-01` + on all 366 of its trades. The engine, before and after this lane, restores + the open lots' cash fees and reserves no fee for the new order, so that + probe's quantities differ from TradingView's in the fourth decimal. +2. A `calc_on_order_fills` run of the seeded witnesses can stop with the + kernel's "native current evaluated allowance mismatch": 117 of the second + family's 2,000 runs, every one with calc-on-fills. The base tree stops 111 + of those seeds, 108 of them the same runs; the lane's E19 and P10 moves + shift which runs reach the failing state, not the failure. + ## 4. Risks and open questions ### 4.1 Entanglements: a generic feature tangled with a TV quirk @@ -1297,7 +1418,7 @@ unchanged after it. | # | Risk | Why it bites | Containment | Src | |---|---|---|---|---| | E1 | **Spec hashing.** `hash_spec` folds every field unconditionally (native_execution_consumer.cpp:234-256) | 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:1620); 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 and its excursion sample differs by chronology (`margin_call_sample_bar` pine_adapter.cpp:729) | 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 | +| 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5782) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6290-6303); 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:16206-16235) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | @@ -1320,7 +1441,7 @@ unchanged after it. | P5 | **Test asymmetry:** 430 adapter test files vs 14 native | native-only tests in every lane (§3.1 b) | O | | P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, non-empty FX curves, security feeds and source setters (`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:298) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:992 → `set_risk_direction` pine_adapter.cpp:18293 (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:293) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:585 → `set_risk_direction` pine_adapter.cpp:18416 (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) @@ -1345,7 +1466,7 @@ unchanged after it. | **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:16371-16387) 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:2646) 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:16283-16299) 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:2645) 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. | @@ -1368,7 +1489,7 @@ NativeMetricsView native_metrics() const; // S: net_pro ``` - `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` + `record_equity_point(script_open_ms)` (engine.hpp:1380, engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7210, native_execution_consumer.cpp:7210, native_execution_consumer.cpp:7210) 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:2358), 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:2357), with `report_open_position_at_end == false`. - Publish read-only `closed_trade_*` accessors (RP6). - Alternative: F's virtual `owns_equity_recording()` (the A48 ownership pattern of `owns_lot_excursions` native_host.hpp:1023). Same semantics, but it costs a host-epoch bump, so the spec form was chosen; the virtual was never added, and `NativeReportPolicy` is what landed (§3.4 row 3). - TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5073-5115), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. @@ -1531,14 +1652,14 @@ 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:1329) (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:373), 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:13735-13833); 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:13690-13788); 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:4675-4688 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted | Proposal | Src | Why not now | |---|---|---| -| `NativeExcursionMode { PathExtremes, FillOnly, HostOwned }` + per-lot sample events | S P8 | The kernel sampler + A48 ownership already cover RP1 (F, O). TradingView's tapes found gaps in the host-owned half, not in the kernel's (RP1, R5 lane H-MEASURE); a mode enum would not close them | +| `NativeExcursionMode { PathExtremes, FillOnly, HostOwned }` + per-lot sample events | S P8 | The kernel sampler + A48 ownership already cover RP1 (F, O). TradingView's tapes found gaps in the host-owned half, not in the kernel's (RP1, R5 lane H-MEASURE); a mode enum would not close them, and R5 lane H-THIN lowered the Pine host onto the sampler | | `NativeFxEpoch` + `on_native_fx_epoch` (stream-safe FX) | S P10 | No R5 lane; F rules it out of scope → U4 | | Generic `FillModel` interface | S §2.2, lane G | R5-3 + R5-1 (§2.ii n) | | TA `ComparePolicy` split | S §2.2 | not neutral (§2.ii b) → U6 | diff --git a/docs/native-settlement.md b/docs/native-settlement.md index 985cb156..5827f1f9 100644 --- a/docs/native-settlement.md +++ b/docs/native-settlement.md @@ -236,8 +236,14 @@ The adapter does not declare the kernel's quantity tolerance (ADR-0001, "Kernel capabilities the Pine adapter does not declare"): its FIFO endpoint test settles a snapped prefix as a selected `Flatten`, charged the lots it holds, where the tolerance charges a `Reduce` its request, and its sweep -erases dust without a row, where the kernel books every lot it closes. Whether -the adapter can move onto the tolerance is a measurement for a later lane. +erases dust without a row, where the kernel books every lot it closes. R5 +lane H-THIN measured the sweep with every other quantity epsilon of the Pine +host and adapter, 85 comparisons, each evaluated in its epsilon form and its +exact form over 5,500 seeded runs and the 610 CTest rows that link the +library (design §3.9): 32 decide somewhere, 50 never do, three are reached +by nothing. Made exact, the sweep moves 30 of the runs and fails its tape +row, as do the lot floor's, the percent-exit floor's and the margin restore's +`+1e-6` slack on theirs. The adapter keeps all of them. ## Reversal to an exact exposure diff --git a/docs/pages/abi-stability.md b/docs/pages/abi-stability.md index 357ac30b..213ed727 100644 --- a/docs/pages/abi-stability.md +++ b/docs/pages/abi-stability.md @@ -322,6 +322,17 @@ v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair show TradingView never does; the values the tree pins were re-pinned once more, each marked "expectation corrected (V19-FIX, ...)". +R5 lane H-THIN moves them once more, inside the unreleased v19 epoch and with +the source extension still v4, in one hash step. The Pine host no longer owns +lot excursions: it keeps the kernel's `owns_lot_excursions()` default, so the +extension's fold no longer carries the retired host model's state (its +excursion caches and level fills), and every value that folds the extension +moves. A default `strategy.exit(from_entry)` under FIFO now reserves its +entry's own quantity, the oldest lots first, as TradingView's pyramiding tape +books it, so the witnesses whose seeded storms exercise it move their trades +too. The corpus moves only where the excursion cells moved toward TradingView +(19 probes); the values the tree pins were re-pinned once more, each marked +"expectation corrected (H-THIN, ...)" with the value's cause. Stable `RunIdentity` / `RequestHandle` / `Birth` remain `native_order_v1`; request, core, and event values own `native_order_v7`. Terms receipts, attempted terms, deferred diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index d22a2043..80ce5484 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -233,7 +233,7 @@ decision, and removing one is a regression: | the kernel's price grid (`NativeRunSpec::price_grid`) and the adapter's own tick rules | TradingView quantizes per *order kind* — stop and limit legs on the quantized bar, the trail stop and the `calc_on_order_fills` cursors raw — and the kernel grid is one rule for the run. A per-kind mask would spell that inconsistency into the kernel. | ADR 0001 ruling table, row `price_grid`; design `native-feature-parity.md:527` | | the kernel's risk limits (`NativeRunSpec::risk`) and the adapter's `strategy.risk.*` | structurally, Pine's risk calls are per-bar statements that arrive after the spec has been digested; substantively, four measured divergences in the latch, the streak, the close price and the day key. | ADR 0001 ruling table, row `risk`; design `native-feature-parity.md:438` | | the kernel's `max_abs_units` and the adapter's `max_position_size` | the kernel caps the *resulting* book, TradingView gates the *live* book before the fill. | ADR 0001 ruling table, row `max_abs_units` | -| the kernel's `max_open_lots` and Pine's `pyramiding` | the adapter counts *entries per cycle*, the kernel counts *physical lots*. Measured against TradingView the kernel's count is the closer one (14 of 15 tape scenarios against the adapter's 8, `tests/test_pyramiding_count_differential.cpp`): TradingView checks an entry once, at its first eligible point, against the trades then open. The adapter keeps its count pending a re-lowering ruling. | ADR 0001 ruling table, row `max_open_lots` | +| the kernel's `max_open_lots` and Pine's `pyramiding` | the adapter counts *entries per cycle*, the kernel counts *physical lots*. Measured against TradingView the kernel's count is the closer one (14 of 15 tape scenarios against the adapter's 8, `tests/test_pyramiding_count_differential.cpp`): TradingView checks an entry once, at its first eligible point, against the trades then open. Lowered onto the cap, four corpus probes move away from TradingView, for want of that rule, so the adapter keeps its count. | ADR 0001 ruling table, row `max_open_lots` | | the kernel's margin model and the adapter's money admission | the adapter answers TradingView's ten-significant-digit admission itself and declares a *maintenance-only* model, because a positive initial requirement would decline openings TradingView takes and admit adds it refuses (measured both ways, `tests/test_adapter_margin_schedule_differential.cpp`). | ADR 0001 ruling table, row `initial_margin_fraction` | | `NativeRunSpec::report_open_position_at_end` and the adapter's range-end rows | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it: report *shape*, not a mark-to-market row. | ADR 0001 ruling table, row `report_open_position_at_end`; design `native-feature-parity.md:624` | | `subscriptions` in the spec and the adapter's begin-time declaration | the adapter declares the same kernel subscriptions through a hook instead of the field, so a plain `request.security` site really is a kernel subscription. | ADR 0001 ruling table, row `subscriptions` | diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 6f44a43f..d57d4260 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -491,7 +491,7 @@ Serialized external C++ calls may command only **between realtime inputs**, never reentrantly during input processing. A host written in C issues the same five commands through `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` -(`native_c_api.h:2737-2787`), under the same legality rule; see *Driving the +(`native_c_api.h:2730-2780`), under the same legality rule; see *Driving the kernel from C* below. `native_order::Request` values belong to `native_order_v7` @@ -1484,7 +1484,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:14119-14152`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:14069-14093`), 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 @@ -1931,11 +1931,12 @@ there lists what it leaves out. | `current_execution_point()` | `NativeCurrentPointView` `native_host.hpp:658`: the active callback's decision context, its price, the `NativeCurrentQuoteKind` `native_host.hpp:451` and the ordinal the quote came from; `nullopt` outside a decision point | `pf_native_decision_v1::price` / `::quote_kind` (partial: no quote origin) | | `native_risk_state()` | `NativeRiskState` `native_host.hpp:643`: whether openings are blocked and why, the risk day, the fills counted in it, the loss-day streak, the peak equity and the day's opening equity | `strategy_native_risk_state_v1` | | `native_liquidation_price()` | the solved level `L`, or `nullopt` | `strategy_native_liquidation_price_v1` | +| `native_aggregates_input_bars()` | whether the run's script bars are buckets the kernel gathers from its input bars: `native_calendar::pairing_aggregates` of the pairing it resolved for the spec's two timeframes; false on a chart-timeframe run | none — a C host names both timeframes in its own spec | | `current_partial_bar()` | the lookahead-free bar so far, through the last path point consumed | `strategy_native_partial_bar_v1` | | `native_series_bar(i)` | the latest delivered bucket of subscription `i` | `strategy_native_series_bar_v1` | | `native_recalculation_count()` / `native_recalculations_skipped()` | the calculations the cadence drove and the ones its per-point bound dropped | `strategy_native_recalculations_v1` | -| `native_decision_floor()` (`native_host.hpp:1355`) | the run's monotonic decision floor in epoch ms — the same value `NativeStateView::decision_floor_ms` carries, and the lower bound every request's birth is compared against | `pf_native_state_v1::decision_floor_ms` | -| `native_consumed_high_water()` (`native_host.hpp:1360`) | the highest `run_number` this host has consumed. It lives **outside** per-run reset, so the next configure on the same host needs a strictly larger number; a fresh host reads 0 | `pf_native_state_v1::consumed_high_water` | +| `native_decision_floor()` (`native_host.hpp:1343`) | the run's monotonic decision floor in epoch ms — the same value `NativeStateView::decision_floor_ms` carries, and the lower bound every request's birth is compared against | `pf_native_state_v1::decision_floor_ms` | +| `native_consumed_high_water()` (`native_host.hpp:1348`) | the highest `run_number` this host has consumed. It lives **outside** per-run reset, so the next configure on the same host needs a strictly larger number; a fresh host reads 0 | `pf_native_state_v1::consumed_high_water` | | `native_continuation_hash()` | the consumer's continuation identity: a fold of its live state (*What the continuation and the broker-state hash fold*), the timezone folded by its content, so the same spec over the same bars and zone rules answers the same value on every host | `strategy_native_continuation_hash_v1` | | `native_sized_units(sized, price, equity, fx)` | the kernel's own `Sized` resolution as a pure query | `strategy_native_sized_units_v1` | | `inspect_current_execution(cmd)` | `NativeCurrentExecutionPreview`, with a `NativeCurrentRefusal` `native_host.hpp:697` when the command cannot be consumed here | none in 1.0 (scheduled for 1.1.0): `strategy_native_execute_current_v1` applies the command and is no preview | @@ -1977,8 +1978,8 @@ bar instead, for a host whose last input is still forming; a D/W/M bar holds whole days, so all four are true. Every callback of one script bar carries the same four, fills and fill recalculations included, and a C host reads the first three from `pf_native_decision_v1`'s session bytes -(`native_c_api.h:1461`). The kernel resolves each session day once through -`native_calendar::session_day_at` (`native_calendar.hpp:412`), which a host +(`native_c_api.h:1454`). The kernel resolves each session day once through +`native_calendar::session_day_at` (`native_calendar.hpp:407`), which a host may call too. `tests/test_native_session_day_facts.cpp` replays the TradingView session tapes through a bare host. It is a presentation snapshot copied onto the callback stack: writing to it cannot move the floor, the @@ -2085,7 +2086,7 @@ per script calculation — after the callback returns, and after the with the **script interval's open**, at the same instant as that calculation's extremes fold, so the curve is identical with and without an intrabar path and a re-walk of it reproduces the scalars above bit for bit -(`compute_equity_stats`, `engine_metrics.cpp:219`, "MUST mirror +(`compute_equity_stats`, `engine_metrics.cpp:203`, "MUST mirror update_equity_extremes"). The result is one point per script bar, a finite drawdown/run-up walk, and metrics computed over a real series. @@ -2973,7 +2974,7 @@ These are existing refusals, not implied future features: - In-session gaps on stream/warmup - Source `calc_on_every_tick` / `calc_on_order_fills` enabled (the runner rejects an explicit true override, and the Pine host refuses a stream begin - with `calc_on_order_fills`, `pine_strategy_host.cpp:284-287`). This is a + with `calc_on_order_fills`, `pine_strategy_host.cpp:202-205`). This is a **source-route** refusal, not a limit on the native hooks: `on_native_tick` and `on_native_applied` are delivered on a stream, and a native host's own `NativeRunSpec::calculation` is accepted there, where `EveryModeledPoint` @@ -2995,7 +2996,7 @@ These are existing refusals, not implied future features: A C host has the same stream and the same commands. Streaming needs no new symbol — `strategy_stream_begin` and its family (`pineforge.h:748`) take a `pf_strategy_t` from `strategy_native_host_create_v1` unchanged — and -`strategy_native_submit_v1` (`native_c_api.h:2687`) obeys the one legality +`strategy_native_submit_v1` (`native_c_api.h:2680`) obeys the one legality rule its C++ spelling does. Rebuild strategy libraries against this engine. An ABI-v4 module without the diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index f98ff542..81ad3f7c 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -70,7 +70,7 @@ gain: (native_order.hpp:1001), `ExecutionAppliedEvent` (native_order.hpp:1146), `MatchRejectedEvent` (native_order.hpp:1108), `CancelledEvent` (native_order.hpp:1043) — readable in order from `native_events` - (native_host.hpp:1329). + (native_host.hpp:1317). - **Any language.** The same kernel drives from C through a callback table (`pf_native_callbacks_v1` native_c_api.h:2463), so a host in Rust, Go, Python or Zig needs no C++ unless it needs a capability the 1.0 C surface does not spell (the 1.0 C boundary table, @@ -114,8 +114,8 @@ A Pine script declares its broker once, in the `strategy()` call, and inherits the rest from the chart. A native host declares *everything* in one `NativeRunSpec` (native_run_spec.hpp:554) applied by `configure_native` (native_host.hpp:1196); a C host sends `pf_native_run_spec_v1` -(pineforge.h:548) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2274) -through `strategy_configure_native_ext_v1` (native_c_api.h:3120). Setup is +(pineforge.h:548) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2267) +through `strategy_configure_native_ext_v1` (native_c_api.h:3113). Setup is atomic: `Ready` → begin → `Completed`, and an incomplete value is refused rather than defaulted. @@ -128,16 +128,16 @@ rather than defaulted. | `commission_value` | `fee_value` native_run_spec.hpp:590 | `fee_value` pineforge.h:540 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | | `slippage` | `slippage_ticks` native_run_spec.hpp:584 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3971-3972. TradingView's round-then-slip order is adapter-only. | | `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7540-7541, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | -| `pyramiding` | `max_open_lots` native_run_spec.hpp:596 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:2914). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). | -| `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:344 with `size_basis` native_c_api.h:2111 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | -| `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2115 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | -| `calc_on_order_fills` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2285 | `native_calc_on_fills_strategy.cpp` | `BarCloseAndFills` recalculates once at the cursor of each applied execution, bounded by `max_recalculations_per_point` native_run_spec.hpp:627. The generic cadence only: TradingView's waypoint-only refill, its two-fills-at-open rule and its script-state rollback stay in the adapter. | -| `calc_on_every_tick` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2285 | `native_calc_on_fills_strategy.cpp` | `EveryModeledPoint` additionally recalculates at every modeled point of the delivered path and at every observed print. In a stream, `on_native_tick` native_host.hpp:878 is one call per accepted realtime print whatever the trigger is. | -| `margin_long` | `NativeMarginModel::initial_long` native_run_spec.hpp:241 | `margin_initial_long` native_c_api.h:2295 | `native_margin_strategy.cpp` | A fraction, not a percent. `0.0` is the maintenance-only spelling for that side: the kernel enforces no opening requirement there and the host owns opening admission. | -| `margin_short` | `NativeMarginModel::initial_short` native_run_spec.hpp:242 | `margin_initial_short` native_c_api.h:2299 | `native_margin_strategy.cpp` | Per side, unlike the one-scalar `initial_margin_fraction` native_run_spec.hpp:598 the model replaces. `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 add the liquidation half Pine has no word for. | +| `pyramiding` | `max_open_lots` native_run_spec.hpp:596 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:2914). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). Lowered onto the cap, four corpus probes move away from TradingView, for want of its first-eligible-point rule, so the adapter keeps its count. | +| `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:348 with `size_basis` native_c_api.h:2104 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | +| `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2108 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | +| `calc_on_order_fills` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2278 | `native_calc_on_fills_strategy.cpp` | `BarCloseAndFills` recalculates once at the cursor of each applied execution, bounded by `max_recalculations_per_point` native_run_spec.hpp:627. The generic cadence only: TradingView's waypoint-only refill, its two-fills-at-open rule and its script-state rollback stay in the adapter. | +| `calc_on_every_tick` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2278 | `native_calc_on_fills_strategy.cpp` | `EveryModeledPoint` additionally recalculates at every modeled point of the delivered path and at every observed print. In a stream, `on_native_tick` native_host.hpp:878 is one call per accepted realtime print whatever the trigger is. | +| `margin_long` | `NativeMarginModel::initial_long` native_run_spec.hpp:241 | `margin_initial_long` native_c_api.h:2288 | `native_margin_strategy.cpp` | A fraction, not a percent. `0.0` is the maintenance-only spelling for that side: the kernel enforces no opening requirement there and the host owns opening admission. | +| `margin_short` | `NativeMarginModel::initial_short` native_run_spec.hpp:242 | `margin_initial_short` native_c_api.h:2292 | `native_margin_strategy.cpp` | Per side, unlike the one-scalar `initial_margin_fraction` native_run_spec.hpp:598 the model replaces. `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 add the liquidation half Pine has no word for. | | `close_entries_rule` | none — a native close names the lots it closes | — | `native_selected_strategy.cpp` | Pine picks FIFO or ANY for the whole script. A native reduce is explicit instead: `BindOpening` native_order.hpp:445 closes one opening's lots, `BindOpenings` native_order.hpp:454 a chosen set, a bare `Reduce` native_order.hpp:220 the book in FIFO order. The adapter keeps the run-level rule (`close_entries_rule` pine_adapter.hpp:88). | | `backtest_fill_limits_assumption` | none — a generic limit fills when the level is reached | — | `tests/test_native_order_core.cpp` | TradingView's assumption is "the price must exceed the limit by *n* ticks". The generic knob beside it is `Limit::fill_through` native_order.hpp:239, which makes a limit market-if-touched; the *n*-tick assumption itself is a TradingView rule and stays in the adapter. | -| `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:378 | `intrabar_kind` native_c_api.h:2338 | `tests/test_native_calc_timing.cpp` | The host owns the lower bars, as `lower_tf` native_run_spec.hpp:394 or `synthesized` native_run_spec.hpp:408. The C magnifier arguments of `run_backtest_full` pineforge.h:648 configure a *compiled Pine strategy*, never a native host. | +| `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:378 | `intrabar_kind` native_c_api.h:2331 | `tests/test_native_calc_timing.cpp` | The host owns the lower bars, as `lower_tf` native_run_spec.hpp:394 or `synthesized` native_run_spec.hpp:408. The C magnifier arguments of `run_backtest_full` pineforge.h:648 configure a *compiled Pine strategy*, never a native host. | | `fill_orders_on_standard_ohlc` | none — the kernel matches the bars you feed it | — | — | TradingView's switch exists because a Heikin-Ashi chart's candles are not the traded prices. A native host feeds the standard OHLC it wants matched, and derives any transformed series itself. | | `max_bars_back` | none — a native host owns its own history | — | `hello_kernel.cpp` | Pine infers a buffer length for every series. `pineforge::Series` series.hpp:94 is a fixed-capacity ring you size yourself, so nothing is inferred and nothing silently truncates. | | `risk_free_rate` | none — the ratio is a report field with TradingView's fixed rate | `sharpe_monthly` pineforge.h:276 | `native_sized_report_strategy.cpp` | `pf_metrics_t::equity.sharpe_monthly` and `pf_metrics_t::equity.sortino_monthly` (`sortino_monthly` pineforge.h:282) use a fixed 2 %/yr, the TradingView default; nothing in the kernel takes a declared rate. Their pre-1.0 spellings `sharpe_tv` / `sortino_tv` were removed for 1.0; the serialized report keys stay `sharpe_tv` / `sortino_tv`. A host that needs another rate computes it from `pf_report_t::equity_curve`. | @@ -152,7 +152,7 @@ not canonical; `NativePathOrder` native_run_spec.hpp:335 forces the intrabar O→H/L→C leg order for path-dependent probing; `NativeAbortReporting` native_run_spec.hpp:41 makes a cooperative abort a quiet status instead of an error. All four reach C through `PF_NATIVE_SPEC_EXT_FEED_POLICY` -native_c_api.h:741. +native_c_api.h:745. ## Design §1 inventory crosswalk {#pine_to_native_inventory} @@ -258,11 +258,11 @@ source-only features state why a native counterpart does not exist. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.fixed` | `Transact` native_order.hpp:41 | `PF_NATIVE_INTENT_TRANSACT` native_c_api.h:335 | `hello_kernel.cpp` | Signed units, the default native sizing: `> 0` long, `< 0` short. | -| `strategy.cash` | `CashValue` native_order.hpp:91 | `PF_NATIVE_SIZE_BASIS_CASH` native_c_api.h:368 | `native_sized_report_strategy.cpp` / `native_fee_reserve_strategy.cpp` | `units = cash / (price * point_value * fx)`. `Sized::reserve_percent_fee` native_order.hpp:179 divides the cash by `(1 + fee)` under `NativeFeeKind::Percent`, which is the fee reserve Pine has no spelling for. The fee-reserve example sizes 46 shares with it where 47 fit without, and asserts the fee each leg pays. | -| `strategy.percent_of_equity` | `EquityFraction` native_order.hpp:97 | `PF_NATIVE_SIZE_BASIS_EQUITY_FRACTION` native_c_api.h:369 | `native_sized_report_strategy.cpp` | A fraction of the marked equity at the sizing point: `0.10` is ten percent, never `10`. | -| `strategy.long` | `Side::Long` native_order.hpp:65 | `PF_NATIVE_SIDE_LONG` native_c_api.h:362 | `native_selected_strategy.cpp` | For `Transact` it is the sign of `signed_units` order_action.hpp:10; `Sized::side` native_order.hpp:165 and `HostSized::side` native_order.hpp:75 name it explicitly. | -| `strategy.short` | `Side::Short` native_order.hpp:65 | `PF_NATIVE_SIDE_SHORT` native_c_api.h:363 | `native_selected_strategy.cpp` | Same. | +| `strategy.fixed` | `Transact` native_order.hpp:41 | `PF_NATIVE_INTENT_TRANSACT` native_c_api.h:339 | `hello_kernel.cpp` | Signed units, the default native sizing: `> 0` long, `< 0` short. | +| `strategy.cash` | `CashValue` native_order.hpp:91 | `PF_NATIVE_SIZE_BASIS_CASH` native_c_api.h:372 | `native_sized_report_strategy.cpp` / `native_fee_reserve_strategy.cpp` | `units = cash / (price * point_value * fx)`. `Sized::reserve_percent_fee` native_order.hpp:179 divides the cash by `(1 + fee)` under `NativeFeeKind::Percent`, which is the fee reserve Pine has no spelling for. The fee-reserve example sizes 46 shares with it where 47 fit without, and asserts the fee each leg pays. | +| `strategy.percent_of_equity` | `EquityFraction` native_order.hpp:97 | `PF_NATIVE_SIZE_BASIS_EQUITY_FRACTION` native_c_api.h:373 | `native_sized_report_strategy.cpp` | A fraction of the marked equity at the sizing point: `0.10` is ten percent, never `10`. | +| `strategy.long` | `Side::Long` native_order.hpp:65 | `PF_NATIVE_SIDE_LONG` native_c_api.h:366 | `native_selected_strategy.cpp` | For `Transact` it is the sign of `signed_units` order_action.hpp:10; `Sized::side` native_order.hpp:165 and `HostSized::side` native_order.hpp:75 name it explicitly. | +| `strategy.short` | `Side::Short` native_order.hpp:65 | `PF_NATIVE_SIDE_SHORT` native_c_api.h:367 | `native_selected_strategy.cpp` | Same. | | `strategy.default_entry_qty()` | none — the kernel has no declaration-level default quantity | — | `native_sized_report_strategy.cpp` | Every native request carries its own size, so there is no run-level default to read back. The adapter keeps Pine's (`default_qty_value` pine_adapter.hpp:67), and `strategy_set_override` pineforge.h:690 overrides it for a compiled Pine strategy. | `HostSized` native_order.hpp:73 is the sixth intent and the escape hatch: the @@ -280,21 +280,21 @@ Everything here is one call: `submit` native_host.hpp:1213, or `submit_market` native_host.hpp:1227 for the market-only shorthand, which *refuses* non-market extras rather than dropping them. `submit` returns a `RequestHandle` native_order_identity.hpp:135; keep it, it is the order id. In C the same call -is `strategy_native_submit_v1` native_c_api.h:2687 over a -`pf_native_request_v1` native_c_api.h:2102. +is `strategy_native_submit_v1` native_c_api.h:2680 over a +`pf_native_request_v1` native_c_api.h:2095. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2687 | `native_market_strategy.cpp` | Pine's entry *reverses* an opposite position; `Transact` does not. Spell the reversal explicitly with the row below. | -| `strategy.order()` | the same `Request` native_order.hpp:483 | `strategy_native_submit_v1` native_c_api.h:2687 | `native_market_strategy.cpp` | `strategy.order` is the non-reversing entry, so it maps 1:1 with no caveat. | -| `strategy.exit()` | `submit_bracket` native_toolkit.hpp:160 over a `BracketSpec` native_toolkit.hpp:37 | `strategy_native_submit_v1` native_c_api.h:2687 per leg | `native_bracket_strategy.cpp` | The full bracket is described below in [Brackets](@ref pine_to_native_brackets). | -| `strategy.close()` | `Reduce` native_order.hpp:220 with `OwnerOpenedUnits` native_order.hpp:191 and `BindOpening` native_order.hpp:445 | `PF_NATIVE_REDUCE_OWNER_OPENED` native_c_api.h:350 with `PF_NATIVE_OWNER_BIND_OPENING` native_c_api.h:455 | `native_selected_strategy.cpp` | Closes the lots one opening produced. The cycle comes from `ExecutionAppliedEvent::cycle_after` native_order.hpp:1165. A fractional close is `ScopeFraction` native_order.hpp:207 — `qty_percent = 50` is `fraction = 0.5`, with `ScopeClaim` native_order.hpp:195 and `ScopeBasis` native_order.hpp:202 saying whether siblings are netted and when the scope is measured. On a `quantity_grid` a fraction below 1 is floored onto the grid, and `qty_percent = 100` (`fraction = 1`) of the gross scope, settled in one fill, closes the whole scope unfloored. | -| `strategy.close_all()` | `Flatten` native_order.hpp:36 | `PF_NATIVE_INTENT_FLATTEN` native_c_api.h:333 | `hello_kernel.cpp` / `hello_kernel_c.c` | The whole book, in one settlement cycle. | -| `strategy.cancel()` | `cancel_where` native_host.hpp:1278 with `NativeRequestField::Label` native_host.hpp:691 | `strategy_native_cancel_where_v1` native_c_api.h:2774 | `native_trail_risk_strategy.cpp` | Withdraws exactly the live requests whose `Request::label` native_order.hpp:485 is that id, and answers how many. `cancel` native_host.hpp:1249 is the single-request form for a host that kept the handle. | -| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1267 | `strategy_native_cancel_all_v1` native_c_api.h:2753 | `native_trail_risk_strategy.cpp` | Neither field is indexed: every form walks the live book once. A `PendingUntilArmed` native_order.hpp:394 child is absent from `native_working_requests` native_host.hpp:1263 before its arm, and all three cancels still address it. | -| `strategy.oca.cancel` | `GroupEffect::Cancel` native_order.hpp:466 | `PF_NATIVE_GROUP_CANCEL` native_c_api.h:474 | `native_bracket_strategy.cpp` | Membership is `Member` native_order.hpp:468 inside `Group` native_order.hpp:473. | -| `strategy.oca.reduce` | `GroupEffect::Reduce` native_order.hpp:466 | `PF_NATIVE_GROUP_REDUCE` native_c_api.h:475 | `tests/test_native_resting_group_contract.cpp` | The sibling's quantity is reduced instead of cancelled. | -| `strategy.oca.none` | `NoGroup` native_order.hpp:467 | `PF_NATIVE_GROUP_NONE` native_c_api.h:468 | `hello_kernel.cpp` | The default: no group, no sibling effect. | +| `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2680 | `native_market_strategy.cpp` | Pine's entry *reverses* an opposite position; `Transact` does not. Spell the reversal explicitly with the row below. | +| `strategy.order()` | the same `Request` native_order.hpp:483 | `strategy_native_submit_v1` native_c_api.h:2680 | `native_market_strategy.cpp` | `strategy.order` is the non-reversing entry, so it maps 1:1 with no caveat. | +| `strategy.exit()` | `submit_bracket` native_toolkit.hpp:160 over a `BracketSpec` native_toolkit.hpp:37 | `strategy_native_submit_v1` native_c_api.h:2680 per leg | `native_bracket_strategy.cpp` | The full bracket is described below in [Brackets](@ref pine_to_native_brackets). | +| `strategy.close()` | `Reduce` native_order.hpp:220 with `OwnerOpenedUnits` native_order.hpp:191 and `BindOpening` native_order.hpp:445 | `PF_NATIVE_REDUCE_OWNER_OPENED` native_c_api.h:354 with `PF_NATIVE_OWNER_BIND_OPENING` native_c_api.h:459 | `native_selected_strategy.cpp` | Closes the lots one opening produced. The cycle comes from `ExecutionAppliedEvent::cycle_after` native_order.hpp:1165. A fractional close is `ScopeFraction` native_order.hpp:207 — `qty_percent = 50` is `fraction = 0.5`, with `ScopeClaim` native_order.hpp:195 and `ScopeBasis` native_order.hpp:202 saying whether siblings are netted and when the scope is measured. On a `quantity_grid` a fraction below 1 is floored onto the grid, and `qty_percent = 100` (`fraction = 1`) of the gross scope, settled in one fill, closes the whole scope unfloored. | +| `strategy.close_all()` | `Flatten` native_order.hpp:36 | `PF_NATIVE_INTENT_FLATTEN` native_c_api.h:337 | `hello_kernel.cpp` / `hello_kernel_c.c` | The whole book, in one settlement cycle. | +| `strategy.cancel()` | `cancel_where` native_host.hpp:1278 with `NativeRequestField::Label` native_host.hpp:691 | `strategy_native_cancel_where_v1` native_c_api.h:2767 | `native_trail_risk_strategy.cpp` | Withdraws exactly the live requests whose `Request::label` native_order.hpp:485 is that id, and answers how many. `cancel` native_host.hpp:1249 is the single-request form for a host that kept the handle. | +| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1267 | `strategy_native_cancel_all_v1` native_c_api.h:2746 | `native_trail_risk_strategy.cpp` | Neither field is indexed: every form walks the live book once. A `PendingUntilArmed` native_order.hpp:394 child is absent from `native_working_requests` native_host.hpp:1263 before its arm, and all three cancels still address it. | +| `strategy.oca.cancel` | `GroupEffect::Cancel` native_order.hpp:466 | `PF_NATIVE_GROUP_CANCEL` native_c_api.h:478 | `native_bracket_strategy.cpp` | Membership is `Member` native_order.hpp:468 inside `Group` native_order.hpp:473. | +| `strategy.oca.reduce` | `GroupEffect::Reduce` native_order.hpp:466 | `PF_NATIVE_GROUP_REDUCE` native_c_api.h:479 | `tests/test_native_resting_group_contract.cpp` | The sibling's quantity is reduced instead of cancelled. | +| `strategy.oca.none` | `NoGroup` native_order.hpp:467 | `PF_NATIVE_GROUP_NONE` native_c_api.h:472 | `hello_kernel.cpp` | The default: no group, no sibling effect. | | `strategy.direction.all` | `NativeOpenDirections::Both` native_run_spec.hpp:76 | `allowed_open_directions` pineforge.h:541 | `native_risk_limits_strategy.cpp` | The default. | | `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:541 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2947. | | `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:541 | `tests/test_native_run_spec.cpp` | Same, the other way. | @@ -318,11 +318,11 @@ when a rejection's `RequestRejectReason` native_order.hpp:821 matters. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `limit=` | `Limit` native_order.hpp:237 | `PF_NATIVE_TRIGGER_LIMIT` native_c_api.h:387 | `native_calc_on_fills_strategy.cpp` | `fill_through` native_order.hpp:239 makes it market-if-touched: the level gates when the request becomes executable, and the fill is no longer bounded by it. | -| `stop=` | `Stop` native_order.hpp:246 | `PF_NATIVE_TRIGGER_STOP` native_c_api.h:388 | `native_price_grid_strategy.cpp` | Stop-limit is `StopLimit` native_order.hpp:252. | -| `trail_points` | `FromOwnerFill{+ticks, true}` native_order.hpp:331 | `anchor = PF_NATIVE_ANCHOR_FROM_OWNER_FILL`, `anchor_offset = ticks`, `anchor_offset_in_ticks = 1` native_c_api.h:2119-2126 | `native_bracket_strategy.cpp` | Pine points are the arm distance from the entry, in ticks. The adapter lowers that anchor distance separately from the ride offset; it is not `TrailTicks`. | -| `trail_offset` | `TrailTicks` native_order.hpp:293 (resolved into `Trail::offset` native_order.hpp:291) | `p1 = ticks`, `trail_offset_in_ticks = 1` native_c_api.h:2120-2124 | `native_trail_risk_strategy.cpp` | Pine offset is the ride distance behind the running best, also expressed in ticks. Zero is legal and means "ride the best"; the first adverse move past it exits. | -| `trail_price` | `Trail::arm_price` native_order.hpp:292 | `trail_has_arm_price` native_c_api.h:2125 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1092 (C: `strategy_native_trail_state_v1` native_c_api.h:2999). The kernel compares `arm_price` with the **raw** path. TradingView tests the activation on the **tick-quantized** bar, with or without an offset and the placement close included (R5 lanes E5 and E9's tapes), so the Pine adapter arms at the activation's half-tick threshold — a placement close whose tick reaches the activation arms there — and books from a running best that starts at the activation; a one-shot (`trail_offset` 0) books the tick its resting threshold stands for, rounded away from the position. Since R5 lane E14 that running best is the **kernel's** own: the trailing leg names the activation — or the favourable one of the activation and the placement print when that print armed it — in the generic `Trail::best_seed` native_order.hpp:294, so the ride no longer starts half a tick short of the activation. | +| `limit=` | `Limit` native_order.hpp:237 | `PF_NATIVE_TRIGGER_LIMIT` native_c_api.h:391 | `native_calc_on_fills_strategy.cpp` | `fill_through` native_order.hpp:239 makes it market-if-touched: the level gates when the request becomes executable, and the fill is no longer bounded by it. | +| `stop=` | `Stop` native_order.hpp:246 | `PF_NATIVE_TRIGGER_STOP` native_c_api.h:392 | `native_price_grid_strategy.cpp` | Stop-limit is `StopLimit` native_order.hpp:252. | +| `trail_points` | `FromOwnerFill{+ticks, true}` native_order.hpp:331 | `anchor = PF_NATIVE_ANCHOR_FROM_OWNER_FILL`, `anchor_offset = ticks`, `anchor_offset_in_ticks = 1` native_c_api.h:2112-2119 | `native_bracket_strategy.cpp` | Pine points are the arm distance from the entry, in ticks. The adapter lowers that anchor distance separately from the ride offset; it is not `TrailTicks`. | +| `trail_offset` | `TrailTicks` native_order.hpp:293 (resolved into `Trail::offset` native_order.hpp:291) | `p1 = ticks`, `trail_offset_in_ticks = 1` native_c_api.h:2113-2117 | `native_trail_risk_strategy.cpp` | Pine offset is the ride distance behind the running best, also expressed in ticks. Zero is legal and means "ride the best"; the first adverse move past it exits. | +| `trail_price` | `Trail::arm_price` native_order.hpp:292 | `trail_has_arm_price` native_c_api.h:2118 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1092 (C: `strategy_native_trail_state_v1` native_c_api.h:2992). The kernel compares `arm_price` with the **raw** path. TradingView tests the activation on the **tick-quantized** bar, with or without an offset and the placement close included (R5 lanes E5 and E9's tapes), so the Pine adapter arms at the activation's half-tick threshold — a placement close whose tick reaches the activation arms there — and books from a running best that starts at the activation; a one-shot (`trail_offset` 0) books the tick its resting threshold stands for, rounded away from the position. Since R5 lane E14 that running best is the **kernel's** own: the trailing leg names the activation — or the favourable one of the activation and the placement print when that print armed it — in the generic `Trail::best_seed` native_order.hpp:294, so the ride no longer starts half a tick short of the activation. | `limit=` and `stop=` are listed once here rather than once per command, because a native trigger is a property of the `Request` native_order.hpp:483 @@ -377,10 +377,10 @@ of a predicate, and it reports the kernel verbatim too: `ReplaceResult` and `SubmitResult` of every command that actually reached the kernel, and `cancel_outcome` says whether a cancel was commanded at all. Pine's `strategy.cancel` tells a script neither. In C -each leg is its own `strategy_native_submit_v1` native_c_api.h:2687 with -`PF_NATIVE_OWNER_WAIT_FOR_APPLIED` native_c_api.h:454, and its `first_match` +each leg is its own `strategy_native_submit_v1` native_c_api.h:2680 with +`PF_NATIVE_OWNER_WAIT_FOR_APPLIED` native_c_api.h:458, and its `first_match` and `scope` are the request's `arm_first_match` / `arm_scope` -native_c_api.h:2185-2186, so a C bracket child arms after its owner's print or over +native_c_api.h:2178-2179, so a C bracket child arms after its owner's print or over the whole book exactly as a C++ one does. The Pine adapter lowers its own queued relative legs exactly this way — a @@ -401,16 +401,16 @@ measured at three points of every script bar: its open, its own close calculation, and after each applied drain. A breach does what `NativeRiskAction` native_run_spec.hpp:278 says — refuse openings, or first flatten the book with one kernel-originated request and then refuse. In C the -block is the `PF_NATIVE_SPEC_EXT_RISK` native_c_api.h:735 tail. +block is the `PF_NATIVE_SPEC_EXT_RISK` native_c_api.h:739 tail. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | | `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:597 | `allowed_open_directions` pineforge.h:541 | `native_risk_limits_strategy.cpp` | The one `strategy.risk.*` rule that is already the kernel's, per ADR-0001's `risk` ruling row. Rejects with `OpeningDirection` native_execution_consumer.cpp:2947. | | `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:595 | `max_abs_units` pineforge.h:542 | `tests/test_native_run_spec.cpp` | Tests the *resulting* book. TradingView gates the *live* book before the fill, so an entry is refused only once the book already holds the limit — ruled **adapter-policy** in ADR-0001's `max_abs_units` row, with lane N12's measurement (the adapter ends at 4 where the kernel cap ends at 2). | -| `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2321 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:259 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:261 is set. Blocks openings to the end of the run. | -| `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:312 | `risk_max_intraday_loss` native_c_api.h:2324 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | -| `strategy.risk.max_cons_loss_days()` | `NativeRiskLimits::max_consecutive_loss_days` native_run_spec.hpp:313 | `risk_max_consecutive_loss_days` native_c_api.h:2326 | `native_risk_limits_strategy.cpp` | Counts *days* that close with a realized loss; TradingView's streak counts trades. `NativeRiskDay` native_run_spec.hpp:270 chooses the session day or the civil date in the spec timezone. | -| `strategy.risk.max_intraday_filled_orders()` | `NativeRiskLimits::max_fills_per_day` native_run_spec.hpp:314 | `risk_max_fills_per_day` native_c_api.h:2328 | `native_trail_risk_strategy.cpp` | Applied fills counted as they settle, so the fills of one matching point all settle and the limit blocks the next admission. Read the ledger back with `native_risk_state` native_host.hpp:1323. | +| `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2314 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:259 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:261 is set. Blocks openings to the end of the run. | +| `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:312 | `risk_max_intraday_loss` native_c_api.h:2317 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | +| `strategy.risk.max_cons_loss_days()` | `NativeRiskLimits::max_consecutive_loss_days` native_run_spec.hpp:313 | `risk_max_consecutive_loss_days` native_c_api.h:2319 | `native_risk_limits_strategy.cpp` | Counts *days* that close with a realized loss; TradingView's streak counts trades. `NativeRiskDay` native_run_spec.hpp:270 chooses the session day or the civil date in the spec timezone. | +| `strategy.risk.max_intraday_filled_orders()` | `NativeRiskLimits::max_fills_per_day` native_run_spec.hpp:314 | `risk_max_fills_per_day` native_c_api.h:2321 | `native_trail_risk_strategy.cpp` | Applied fills counted as they settle, so the fills of one matching point all settle and the limit blocks the next admission. Read the ledger back with `native_risk_state` native_host.hpp:1311. | The whole block is **native-only by ruling**: no adapter run declares it, and lane N12 measured why — TradingView's drawdown latch samples at the close and @@ -429,20 +429,20 @@ liquidation level, and the ticket the forced close is booked under. | concept | C++ | C | Runs in | | --- | --- | --- | --- | -| opening requirement per side | `initial_long` native_run_spec.hpp:241 / `initial_short` native_run_spec.hpp:242 | `margin_initial_long` native_c_api.h:2295 | `native_margin_strategy.cpp` | -| maintenance requirement per side | `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 | `margin_maintenance_long` native_c_api.h:2300 | `native_margin_strategy.cpp` | -| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:140 | `margin_sizing` native_c_api.h:2290 | `native_margin_strategy.cpp` | -| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:158 | `margin_check` native_c_api.h:2291 | `native_margin_strategy.cpp` | -| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2358 | `tests/test_native_margin_model.cpp` | -| which base the level is solved from | `NativeLiquidationLevelBase` native_run_spec.hpp:182 | `margin_level_base` native_c_api.h:2359 | `tests/test_native_margin_model.cpp` | -| the forced close's ticket | `liquidation_label` native_run_spec.hpp:251 | `margin_liquidation_label` native_c_api.h:2360 | `native_margin_strategy.cpp` | -| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:960 | — C hosts gate through `on_precommit` native_c_api.h:2580 | `native_margin_strategy.cpp` | -| the host's own requirement | `resolve_margin_requirement` native_host.hpp:976 | `on_margin_requirement` native_c_api.h:2517 | `native_margin_strategy.cpp` | -| which check points run at all | `margin_check_allowed` native_host.hpp:988 | `on_margin_check` native_c_api.h:2526 | `native_margin_strategy.cpp` | -| a step of the account FX curve, checked though no price moved | `NativeMarginCheckKind::FxRoll` native_host.hpp:553 | `PF_NATIVE_MARGIN_CHECK_FX_ROLL` native_c_api.h:931 | `native_fx_roll_strategy.cpp` | +| opening requirement per side | `initial_long` native_run_spec.hpp:241 / `initial_short` native_run_spec.hpp:242 | `margin_initial_long` native_c_api.h:2288 | `native_margin_strategy.cpp` | +| maintenance requirement per side | `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 | `margin_maintenance_long` native_c_api.h:2293 | `native_margin_strategy.cpp` | +| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:140 | `margin_sizing` native_c_api.h:2283 | `native_margin_strategy.cpp` | +| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:158 | `margin_check` native_c_api.h:2284 | `native_margin_strategy.cpp` | +| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2351 | `tests/test_native_margin_model.cpp` | +| which base the level is solved from | `NativeLiquidationLevelBase` native_run_spec.hpp:182 | `margin_level_base` native_c_api.h:2352 | `tests/test_native_margin_model.cpp` | +| the forced close's ticket | `liquidation_label` native_run_spec.hpp:251 | `margin_liquidation_label` native_c_api.h:2353 | `native_margin_strategy.cpp` | +| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:960 | — C hosts gate through `on_precommit` native_c_api.h:2573 | `native_margin_strategy.cpp` | +| the host's own requirement | `resolve_margin_requirement` native_host.hpp:976 | `on_margin_requirement` native_c_api.h:2510 | `native_margin_strategy.cpp` | +| which check points run at all | `margin_check_allowed` native_host.hpp:988 | `on_margin_check` native_c_api.h:2519 | `native_margin_strategy.cpp` | +| a step of the account FX curve, checked though no price moved | `NativeMarginCheckKind::FxRoll` native_host.hpp:553 | `PF_NATIVE_MARGIN_CHECK_FX_ROLL` native_c_api.h:935 | `native_fx_roll_strategy.cpp` | | a magnified bar's later samples, each checked where the path is (TradingView's bar magnifier) | `NativeMarginCheckKind::IntrabarSample` native_host.hpp:554 | `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE` native_c_api.h:939 | `tests/test_native_margin_intrabar_samples.cpp` | -| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:995 | `on_margin_call_units` native_c_api.h:2533 | `native_margin_strategy.cpp` | -| the liquidation event | `MarginCallEvent` native_order.hpp:1262 delivered to `on_native_margin_call` native_host.hpp:1001 | `on_margin_call` native_c_api.h:2481 | `native_margin_strategy.cpp` | +| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:995 | `on_margin_call_units` native_c_api.h:2526 | `native_margin_strategy.cpp` | +| the liquidation event | `MarginCallEvent` native_order.hpp:1262 delivered to `on_native_margin_call` native_host.hpp:1001 | `on_margin_call` native_c_api.h:2474 | `native_margin_strategy.cpp` | The one-scalar `initial_margin_fraction` native_run_spec.hpp:598 predates the model and remains the simple spelling; the two are mutually exclusive. It is @@ -457,20 +457,20 @@ part both ways (`tests/test_adapter_margin_schedule_differential.cpp`, M11). | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| script body at bar close | `on_native_bar` native_host.hpp:893 | `on_bar` native_c_api.h:2471 | `hello_kernel.cpp` / `hello_kernel_c.c` | The one pure virtual. Called once per closed script bar — and, with the default cadence, at every calculation there is: the kernel routes all of them through `on_native_recalculate` native_host.hpp:913, whose default forwards here. | -| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:913 with `NativeCalculationReason` native_host.hpp:825 | `on_recalculate` native_c_api.h:2499 | `native_calc_on_fills_strategy.cpp` | **Installing it REPLACES `on_native_bar` for every calculation** — in C++ you override the forwarding away, and in C a non-NULL `on_recalculate` is what the kernel calls instead of `on_bar`. A host that wants both calls the other itself from here; a C host that installs only `on_recalculate` and still expects `on_bar` silently trades nothing. | -| `barstate.isconfirmed` | `NativeCalculationReason::BarClose` native_host.hpp:826 | `on_recalculate` native_c_api.h:2499 | `native_calc_on_fills_strategy.cpp` | The script bar's own calculation is delivered for every run, whatever the cadence is. | -| `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1763 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | -| bar-open pre-pass | `on_native_bar_open` native_host.hpp:889 | `on_bar_open` native_c_api.h:2470 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | -| the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1073 | `strategy_native_partial_bar_v1` native_c_api.h:2984 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | -| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2473 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5387. | -| raw input before aggregation | `on_native_input` native_host.hpp:875 | `on_input` native_c_api.h:2468 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | -| a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:928 | `on_sub_bar` native_c_api.h:2509 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | -| `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1461 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. | -| `session.ismarket` | `in_session` market_driver.hpp:161 | `in_session` native_c_api.h:1479 | `tests/test_native_session_day_facts.cpp` | The script bar's label is in session on the run's own calendar (`NativeRunSpec::session` / `timezone`); a D/W/M bar holds whole session days, so it is always true there. The calendar applies a session's day mask to the session day's trading date: Sunday 17:00 CT of a CME-style `1700-1600:23456` is Monday's session, in session. Pine's own `session.ismarket` is still generated as the time-of-day predicate `pine_session_ismarket` session_time.hpp:247, whose mask reads each instant's weekday, so on such a masked overnight session the two differ: on six `lab tv` tapes TradingView flags every bar in market, the calendar's fact matches it on every bar, and the predicate misses every Sunday open under a weekday mask (`1700-1600:23456`) and every bar of `0000-2400`, which it cannot parse as a whole day (`tests/test_session_ismarket_tape.cpp`). Unmasked sessions (`1700-1600`, `1700-1700`, `24x7`, `0930-1600`) agree on all three. In C, read the three session bytes only when `session_facts` native_c_api.h:1478 is 1. | -| `session.isfirstbar` | `opens_session_day` market_driver.hpp:162 | `opens_session_day` native_c_api.h:1480 | `tests/test_native_session_day_facts.cpp` | In session, and the bar before it is not, or is on another session day. The session day rolls at the session's first window start and is keyed to its trading date, so an overnight session is one day across local midnight (Tokyo `2230-0500`: its days open at 22:30). "The bar before" is the one the run holds — the batch input, the stream warmup — else the calendar's previous eligible input slot across declared breaks; a run's first bar opens its day. | -| `session.islastbar` | `closes_session_day` market_driver.hpp:163, `closes_session_day_open_ended` market_driver.hpp:164 | `closes_session_day` native_c_api.h:1481 | `tests/test_native_session_day_facts.cpp` | In session, and the bar after it is not, or is on another session day, "the bar after" read the same way. A batch's final bar closes its day (a batch is complete input); `closes_session_day_open_ended` reads the calendar there instead, for a host recomputing a batch whose last input is still forming (C++ only: a C host's live edge is a stream). A stream's bars read the calendar, so a stream cannot see an early close its session string does not declare: the NYSE half day's 12:45 closes the day in a batch (the next bar held is the next day's) but not in a stream (ADR-0001, ruling "Session-day facts at a bar with nothing held after it"). | -| the price at the current point | `current_execution_point` native_host.hpp:1087 | `price` native_c_api.h:1540 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1099 is an observation, never an apply token; `execute_current` native_host.hpp:1104 is the apply. | +| script body at bar close | `on_native_bar` native_host.hpp:893 | `on_bar` native_c_api.h:2464 | `hello_kernel.cpp` / `hello_kernel_c.c` | The one pure virtual. Called once per closed script bar — and, with the default cadence, at every calculation there is: the kernel routes all of them through `on_native_recalculate` native_host.hpp:913, whose default forwards here. | +| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:913 with `NativeCalculationReason` native_host.hpp:825 | `on_recalculate` native_c_api.h:2492 | `native_calc_on_fills_strategy.cpp` | **Installing it REPLACES `on_native_bar` for every calculation** — in C++ you override the forwarding away, and in C a non-NULL `on_recalculate` is what the kernel calls instead of `on_bar`. A host that wants both calls the other itself from here; a C host that installs only `on_recalculate` and still expects `on_bar` silently trades nothing. | +| `barstate.isconfirmed` | `NativeCalculationReason::BarClose` native_host.hpp:826 | `on_recalculate` native_c_api.h:2492 | `native_calc_on_fills_strategy.cpp` | The script bar's own calculation is delivered for every run, whatever the cadence is. | +| `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1756 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | +| bar-open pre-pass | `on_native_bar_open` native_host.hpp:889 | `on_bar_open` native_c_api.h:2463 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | +| the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1073 | `strategy_native_partial_bar_v1` native_c_api.h:2977 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | +| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2466 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5387. | +| raw input before aggregation | `on_native_input` native_host.hpp:875 | `on_input` native_c_api.h:2461 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | +| a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:928 | `on_sub_bar` native_c_api.h:2502 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | +| `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1454 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. | +| `session.ismarket` | `in_session` market_driver.hpp:161 | `in_session` native_c_api.h:1472 | `tests/test_native_session_day_facts.cpp` | The script bar's label is in session on the run's own calendar (`NativeRunSpec::session` / `timezone`); a D/W/M bar holds whole session days, so it is always true there. The calendar applies a session's day mask to the session day's trading date: Sunday 17:00 CT of a CME-style `1700-1600:23456` is Monday's session, in session. Pine's own `session.ismarket` is still generated as the time-of-day predicate `pine_session_ismarket` session_time.hpp:247, whose mask reads each instant's weekday, so on such a masked overnight session the two differ: on six `lab tv` tapes TradingView flags every bar in market, the calendar's fact matches it on every bar, and the predicate misses every Sunday open under a weekday mask (`1700-1600:23456`) and every bar of `0000-2400`, which it cannot parse as a whole day (`tests/test_session_ismarket_tape.cpp`). Unmasked sessions (`1700-1600`, `1700-1700`, `24x7`, `0930-1600`) agree on all three. In C, read the three session bytes only when `session_facts` native_c_api.h:1471 is 1. | +| `session.isfirstbar` | `opens_session_day` market_driver.hpp:162 | `opens_session_day` native_c_api.h:1473 | `tests/test_native_session_day_facts.cpp` | In session, and the bar before it is not, or is on another session day. The session day rolls at the session's first window start and is keyed to its trading date, so an overnight session is one day across local midnight (Tokyo `2230-0500`: its days open at 22:30). "The bar before" is the one the run holds — the batch input, the stream warmup — else the calendar's previous eligible input slot across declared breaks; a run's first bar opens its day. | +| `session.islastbar` | `closes_session_day` market_driver.hpp:163, `closes_session_day_open_ended` market_driver.hpp:164 | `closes_session_day` native_c_api.h:1474 | `tests/test_native_session_day_facts.cpp` | In session, and the bar after it is not, or is on another session day, "the bar after" read the same way. A batch's final bar closes its day (a batch is complete input); `closes_session_day_open_ended` reads the calendar there instead, for a host recomputing a batch whose last input is still forming (C++ only: a C host's live edge is a stream). A stream's bars read the calendar, so a stream cannot see an early close its session string does not declare: the NYSE half day's 12:45 closes the day in a batch (the next bar held is the next day's) but not in a stream (ADR-0001, ruling "Session-day facts at a bar with nothing held after it"). | +| the price at the current point | `current_execution_point` native_host.hpp:1087 | `price` native_c_api.h:1533 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1099 is an observation, never an apply token; `execute_current` native_host.hpp:1104 is the apply. | ## Position, account and report {#pine_to_native_map_report} @@ -508,7 +508,7 @@ For event readback, C++ `NativeRunSpec::event_retention` defaults to `Window`: the kernel retires events at script-bar boundaries, so a host that inspects `native_events(0)` only after a run must request `Full`. A host that consumes events during the run can keep `Window` and acknowledge its cursor with -`native_acknowledge_events` native_host.hpp:1344. A C caller that omits the +`native_acknowledge_events` native_host.hpp:1332. A C caller that omits the extension's retention word keeps `Full` for ABI compatibility. Pine's `strategy.openprofit` is the **gross** mark-to-market move of open @@ -530,7 +530,7 @@ range-end reporting is off. | --- | --- | --- | --- | --- | | `strategy.account_currency` | `currency` native_run_spec.hpp:571 | `currency` pineforge.h:537 | `hello_kernel.cpp` / `native_fx_roll_strategy.cpp` | Declared, never inferred. `account_fx` native_run_spec.hpp:582 is the one positive scalar that converts quote to account; a timestamped curve is `configure_native_fx_curve` native_host.hpp:1202. The FX-roll example runs a JPY account on a USD-quoted stock through such a curve, and a step of it is a margin check point of its own. | | `strategy.initial_capital` | `initial_capital` native_run_spec.hpp:580 | `initial_capital` pineforge.h:539 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | -| `strategy.equity` | `current_equity() + open_profit(mark)` engine.hpp:970-1024; or `native_marked_equity(mark)` native_host.hpp:1301 plus open entry commissions | `strategy_native_marked_equity_v1` native_c_api.h:3024 plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine marks at the current `close`. The native marked call takes the mark explicitly and returns balance plus open lots' **net** `unrealized_pnl` native_host.hpp:341; add their remaining entry commissions to reproduce Pine's gross-open-profit equity. | +| `strategy.equity` | `current_equity() + open_profit(mark)` engine.hpp:970-1024; or `native_marked_equity(mark)` native_host.hpp:1301 plus open entry commissions | `strategy_native_marked_equity_v1` native_c_api.h:3017 plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine marks at the current `close`. The native marked call takes the mark explicitly and returns balance plus open lots' **net** `unrealized_pnl` native_host.hpp:341; add their remaining entry commissions to reproduce Pine's gross-open-profit equity. | | `strategy.netprofit` | `net_profit` engine.hpp:967 | `net_profit` pineforge.h:412 | `native_open_lots_strategy.cpp` | Realized only. | | `strategy.netprofit_percent` | derive: `net_profit` engine.hpp:967 over `initial_capital` native_run_spec.hpp:580 | `pf_metrics_t` pineforge.h:358 | `native_sized_report_strategy.cpp` | The kernel keeps the facts, not the ratio. | | `strategy.grossprofit` | `gross_profit` engine.hpp:968 | `pf_trade_stats_t` pineforge.h:291 | `native_open_lots_strategy.cpp` | | @@ -555,10 +555,10 @@ range-end reporting is off. | `strategy.max_contracts_held_all` | `max_contracts_held_all` engine.hpp:1836 | — | `native_open_lots_strategy.cpp` | `pf_trade_stats_t` has no max-contracts-held field. A C host must derive the peak from position/open-lot observations; there is no direct C report counterpart. | | `strategy.max_contracts_held_long` | `max_contracts_held_long` engine.hpp:1837 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the long-side peak from position/open-lot observations. | | `strategy.max_contracts_held_short` | `max_contracts_held_short` engine.hpp:1838 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the short-side peak from position/open-lot observations. | -| `strategy.position_size` | `NativePhysicalPosition::signed_units` native_host.hpp:297 | `strategy_native_position_v1` native_c_api.h:2800 | `hello_kernel.cpp` | Signed: no separate direction field. | -| `strategy.position_avg_price` | `NativePhysicalPosition::average_price` native_host.hpp:298 | `strategy_native_position_v1` native_c_api.h:2800 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | -| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1297 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2841 | `native_open_lots_strategy.cpp` | Pine reports the newest entry's id; the native book lets you read any lot's. | -| `strategy.margin_liquidation_price` | `native_liquidation_price` native_host.hpp:1320 | `strategy_native_liquidation_price_v1` native_c_api.h:3032 | `native_margin_strategy.cpp` | Solved from the declared maintenance requirement, so it answers `nullopt` when the model declares none or the slope is degenerate (a long at full maintenance has no finite level). | +| `strategy.position_size` | `NativePhysicalPosition::signed_units` native_host.hpp:297 | `strategy_native_position_v1` native_c_api.h:2793 | `hello_kernel.cpp` | Signed: no separate direction field. | +| `strategy.position_avg_price` | `NativePhysicalPosition::average_price` native_host.hpp:298 | `strategy_native_position_v1` native_c_api.h:2793 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | +| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1297 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2834 | `native_open_lots_strategy.cpp` | Pine reports the newest entry's id; the native book lets you read any lot's. | +| `strategy.margin_liquidation_price` | `native_liquidation_price` native_host.hpp:1320 | `strategy_native_liquidation_price_v1` native_c_api.h:3025 | `native_margin_strategy.cpp` | Solved from the declared maintenance requirement, so it answers `nullopt` when the model declares none or the slope is degenerate (a long at full maintenance has no finite level). | | `strategy.convert_to_account()` | none — the kernel converts with the FX you declare | `strategy_configure_native_fx_curve_v1` pineforge.h:584 | `native_fx_roll_strategy.cpp` / `tests/test_native_fx_curve.cpp` | Pine's converter needs TradingView's own FX series. Natively, `account_fx` native_run_spec.hpp:582 is one scalar and `configure_native_fx_curve` native_host.hpp:1202 stages an immutable timestamped curve; codegen emits the Pine call as the identity, because no fixed table reproduces TradingView's series. | | `strategy.convert_to_symbol()` | none — same reason, the other direction | `strategy_configure_native_fx_curve_v1` pineforge.h:584 | `tests/test_native_fx_curve.cpp` | Same ruling. | @@ -568,9 +568,9 @@ range-end reporting is off. `native_open_lots(mark)` native_host.hpp:1297 answers one `NativeOpenLot` native_host.hpp:328 per open physical lot, in book order (oldest first, `NativePhysicalPosition::lot_count` native_host.hpp:299 rows). The C twin is -`strategy_native_open_lot_count_v1` native_c_api.h:2833 then -`strategy_native_open_lot_get_v1` native_c_api.h:2841 into a -`pf_native_open_lot_v1` native_c_api.h:1739, whose two strings borrow the +`strategy_native_open_lot_count_v1` native_c_api.h:2826 then +`strategy_native_open_lot_get_v1` native_c_api.h:2834 into a +`pf_native_open_lot_v1` native_c_api.h:1732, whose two strings borrow the snapshot until the next count call. `mark` is the price the three marked fields are computed at; Pine's builtins mark at the current `close`. Reading it moves no fill, no hash and no row. `native_open_lots_strategy.cpp` is the runnable @@ -583,19 +583,19 @@ native_order.hpp:445 names. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.opentrades` | `native_open_lots` native_host.hpp:1297 | `strategy_native_open_lot_count_v1` native_c_api.h:2833 | `native_open_lots_strategy.cpp` | The row count, which equals `lot_count` native_host.hpp:299. | -| `strategy.opentrades.entry_id()` | `entry_label` native_host.hpp:333 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | The opening request's `label` native_order.hpp:485. | -| `strategy.opentrades.entry_comment()` | `entry_comment` native_host.hpp:334 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | The opening request's `comment` native_order.hpp:486. | -| `strategy.opentrades.entry_bar_index()` | `entry_bar_index` native_host.hpp:336 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | Script-bar index of the opening fill. | -| `strategy.opentrades.entry_time()` | `entry_time_ms` native_host.hpp:335 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | Effective time of the opening fill, Unix ms. | -| `strategy.opentrades.entry_price()` | `entry_price` native_host.hpp:337 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | The booked price: slippage and the price grid are already applied. | -| `strategy.opentrades.size()` | `signed_units` native_host.hpp:338 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | Pine's is unsigned with a separate direction; here `> 0` is long, `< 0` short, and `side` native_host.hpp:332 says the same. | -| `strategy.opentrades.commission()` | `entry_commission` native_host.hpp:339 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | The entry fee still on the lot, account currency. A partial close takes its share onto the closed row: the example's FIFO reduce leaves 1.5 units carrying 1.5 of a 2.00 ticket. | -| `strategy.opentrades.profit()` | `unrealized_pnl` native_host.hpp:341 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | The move from `entry_price` to `mark` in account currency, **net of `entry_commission`** — the lot's own term of the marked equity. Gross is `unrealized_pnl + entry_commission`. | +| `strategy.opentrades` | `native_open_lots` native_host.hpp:1297 | `strategy_native_open_lot_count_v1` native_c_api.h:2826 | `native_open_lots_strategy.cpp` | The row count, which equals `lot_count` native_host.hpp:299. | +| `strategy.opentrades.entry_id()` | `entry_label` native_host.hpp:333 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The opening request's `label` native_order.hpp:485. | +| `strategy.opentrades.entry_comment()` | `entry_comment` native_host.hpp:334 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The opening request's `comment` native_order.hpp:486. | +| `strategy.opentrades.entry_bar_index()` | `entry_bar_index` native_host.hpp:336 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Script-bar index of the opening fill. | +| `strategy.opentrades.entry_time()` | `entry_time_ms` native_host.hpp:335 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Effective time of the opening fill, Unix ms. | +| `strategy.opentrades.entry_price()` | `entry_price` native_host.hpp:337 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The booked price: slippage and the price grid are already applied. | +| `strategy.opentrades.size()` | `signed_units` native_host.hpp:338 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Pine's is unsigned with a separate direction; here `> 0` is long, `< 0` short, and `side` native_host.hpp:332 says the same. | +| `strategy.opentrades.commission()` | `entry_commission` native_host.hpp:339 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The entry fee still on the lot, account currency. A partial close takes its share onto the closed row: the example's FIFO reduce leaves 1.5 units carrying 1.5 of a 2.00 ticket. | +| `strategy.opentrades.profit()` | `unrealized_pnl` native_host.hpp:341 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The move from `entry_price` to `mark` in account currency, **net of `entry_commission`** — the lot's own term of the marked equity. Gross is `unrealized_pnl + entry_commission`. | | `strategy.opentrades.profit_percent()` | derive: `unrealized_pnl` native_host.hpp:341 over entry cost | — | `native_open_lots_strategy.cpp` | Entry cost is `entry_price * abs(signed_units) * point_value * account_fx`. | -| `strategy.opentrades.max_runup()` | `favorable_excursion` native_host.hpp:342 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | Largest move for the lot the kernel has sampled along the delivered path, account currency, gross of fees, with `mark` folded in. | +| `strategy.opentrades.max_runup()` | `favorable_excursion` native_host.hpp:342 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Largest move for the lot the kernel has sampled along the delivered path, account currency, gross of fees, with `mark` folded in. | | `strategy.opentrades.max_runup_percent()` | derive from `favorable_excursion` native_host.hpp:342 | — | `native_open_lots_strategy.cpp` | The snapshot carries the facts, not the ratio. | -| `strategy.opentrades.max_drawdown()` | `adverse_excursion` native_host.hpp:343 | `pf_native_open_lot_v1` native_c_api.h:1739 | `native_open_lots_strategy.cpp` | Largest move against the lot, likewise; both excursions are magnitudes `>= 0`. | +| `strategy.opentrades.max_drawdown()` | `adverse_excursion` native_host.hpp:343 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Largest move against the lot, likewise; both excursions are magnitudes `>= 0`. | | `strategy.opentrades.max_drawdown_percent()` | derive from `adverse_excursion` native_host.hpp:343 | — | `native_open_lots_strategy.cpp` | | | `strategy.opentrades.capital_held` | `open_trades_capital_held` engine.hpp:1016 | — | `native_open_lots_strategy.cpp` | `pf_trade_stats_t` carries no capital-held field. Derive it from the open lots and declared price/value/FX terms, or read the protected C++ accessor. | @@ -624,7 +624,7 @@ rows of that array. | `strategy.closedtrades` | `closed_trade_count` engine.hpp:1807 | `total_trades` pineforge.h:406 | `native_open_lots_strategy.cpp` | `closed_trade_count` counts booked closed rows. The C report's `total_trades` also includes range-end rows when `report_open_position_at_end` is enabled; filter `pf_trade_t::open_at_end == 0` to match Pine `strategy.closedtrades`. | | `strategy.closedtrades.first_index` | none — the kernel keeps every row | — | `native_open_lots_strategy.cpp` | TradingView drops old rows past a 9000-trade cap and advances `first_index` when it does. The kernel caps nothing, so the first index is always 0 and codegen emits the literal. | | `strategy.closedtrades.entry_id()` | `entry_id` engine.hpp:183 | `strategy_closed_trade_entry_id` pineforge.h:1192 | `native_open_lots_strategy.cpp` | The same string the lot carried as `entry_label` native_host.hpp:333. | -| `strategy.closedtrades.entry_comment()` | `entry_comment` engine.hpp:184 | — | `native_open_lots_strategy.cpp` | No C accessor: `strategy_closed_trade_entry_id` pineforge.h:1192 answers the id, not the comment, and `pf_trade_t` carries no strings. A C host that needs it reads the open lot's `entry_comment` (`pf_native_open_lot_v1` native_c_api.h:1739) before the close. | +| `strategy.closedtrades.entry_comment()` | `entry_comment` engine.hpp:184 | — | `native_open_lots_strategy.cpp` | No C accessor: `strategy_closed_trade_entry_id` pineforge.h:1192 answers the id, not the comment, and `pf_trade_t` carries no strings. A C host that needs it reads the open lot's `entry_comment` (`pf_native_open_lot_v1` native_c_api.h:1732) before the close. | | `strategy.closedtrades.entry_bar_index()` | `entry_bar_index` engine.hpp:181 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | Copied from the lot. | | `strategy.closedtrades.entry_time()` | `entry_time` engine.hpp:173 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | Unix ms. | | `strategy.closedtrades.entry_price()` | `entry_price` engine.hpp:175 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | | @@ -658,12 +658,12 @@ classifies its own rows by order family instead. Which leg closed a row is its | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `request.security()` | `NativeTimeframeSubscription` native_run_spec.hpp:486 in `subscriptions` native_run_spec.hpp:633, or `declare_timeframe_subscriptions` native_host.hpp:1136 inside `on_native_run_begin` native_host.hpp:872 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2905 | `native_htf_strategy.cpp` | A subscription is a series instance: several may share one timeframe. Completed buckets arrive at `on_native_timeframe_bar` native_host.hpp:885 and the latest is pulled with `native_series_bar` native_host.hpp:1119. `authoritative_bars` native_run_spec.hpp:488 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:795 and `pf_native_gaps_e` native_c_api.h:806 (lane E7). The Pine adapter runs its own plain sites through these same subscriptions, ruled **adapter-hook** in ADR-0001's `subscriptions` row. | -| `request.security_lower_tf()` | `NativeAuxiliaryFeed` native_run_spec.hpp:515 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:483 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3182 | `native_auxiliary_feed_strategy.cpp` | Not the same shape: Pine returns an intrabar *array* per bar, the kernel gives you a finer *series* routed by time. A host that wants the raw sub-bars puts them in `IntrabarPath` native_run_spec.hpp:378 and reads `on_native_sub_bar` native_host.hpp:928 instead. | -| `barmerge.gaps_off` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:490 set false | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_HOLD` native_c_api.h:807 | `native_htf_strategy.cpp` | The delivered bucket stands until the next delivery replaces it. | -| `barmerge.gaps_on` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:490 set true | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_CLEAR` native_c_api.h:809 | `native_htf_strategy.cpp` | The series is cleared on every input bar it delivers nothing on, so the pull answers empty — the native spelling of `na`. | -| `barmerge.lookahead_off` | `NativeTimeframeSubscription::lookahead` native_run_spec.hpp:489 set false | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_COMPLETION` native_c_api.h:796 | `native_htf_strategy.cpp` | The default and the honest one: a bucket is delivered when it completes. | -| `barmerge.lookahead_on` | `NativeTimeframeSubscription::lookahead` native_run_spec.hpp:489 set true | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_FIRST_INPUT` native_c_api.h:798 | `tests/test_native_htf_subscriptions.cpp` | Delivers a bucket's final values on the input that held its first bar. It is lookahead: use it to reproduce a chart, never to trade. | +| `request.security()` | `NativeTimeframeSubscription` native_run_spec.hpp:486 in `subscriptions` native_run_spec.hpp:633, or `declare_timeframe_subscriptions` native_host.hpp:1136 inside `on_native_run_begin` native_host.hpp:872 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2898 | `native_htf_strategy.cpp` | A subscription is a series instance: several may share one timeframe. Completed buckets arrive at `on_native_timeframe_bar` native_host.hpp:885 and the latest is pulled with `native_series_bar` native_host.hpp:1119. `authoritative_bars` native_run_spec.hpp:488 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:799 and `pf_native_gaps_e` native_c_api.h:810 (lane E7). The Pine adapter runs its own plain sites through these same subscriptions, ruled **adapter-hook** in ADR-0001's `subscriptions` row. | +| `request.security_lower_tf()` | `NativeAuxiliaryFeed` native_run_spec.hpp:515 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:483 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3175 | `native_auxiliary_feed_strategy.cpp` | Not the same shape: Pine returns an intrabar *array* per bar, the kernel gives you a finer *series* routed by time. A host that wants the raw sub-bars puts them in `IntrabarPath` native_run_spec.hpp:378 and reads `on_native_sub_bar` native_host.hpp:928 instead. | +| `barmerge.gaps_off` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:490 set false | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_HOLD` native_c_api.h:811 | `native_htf_strategy.cpp` | The delivered bucket stands until the next delivery replaces it. | +| `barmerge.gaps_on` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:490 set true | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_CLEAR` native_c_api.h:813 | `native_htf_strategy.cpp` | The series is cleared on every input bar it delivers nothing on, so the pull answers empty — the native spelling of `na`. | +| `barmerge.lookahead_off` | `NativeTimeframeSubscription::lookahead` native_run_spec.hpp:489 set false | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_COMPLETION` native_c_api.h:800 | `native_htf_strategy.cpp` | The default and the honest one: a bucket is delivered when it completes. | +| `barmerge.lookahead_on` | `NativeTimeframeSubscription::lookahead` native_run_spec.hpp:489 set true | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_FIRST_INPUT` native_c_api.h:802 | `tests/test_native_htf_subscriptions.cpp` | Delivers a bucket's final values on the input that held its first bar. It is lookahead: use it to reproduce a chart, never to trade. | | `request.currency_rate()` | none — no FX data feed | `strategy_configure_native_fx_curve_v1` pineforge.h:584 | `native_fx_roll_strategy.cpp` / `tests/test_native_fx_curve.cpp` | The kernel converts with the curve you stage; it fetches nothing. | | `request.dividends()` | none — no fundamentals feed | — | — | Rejected at transpile for a Pine script; a native host fetches and models it itself. | | `request.earnings()` | none — no fundamentals feed | — | — | Same. | @@ -744,21 +744,21 @@ static int on_bar(void* user, const pf_bar_t* bar, const pf_native_decision_v1* } ``` -The host is a `pf_native_callbacks_v1` native_c_api.h:2463 table handed to -`strategy_native_host_create_v1` native_c_api.h:2646; the run is +The host is a `pf_native_callbacks_v1` native_c_api.h:2456 table handed to +`strategy_native_host_create_v1` native_c_api.h:2639; the run is `pf_native_run_spec_v1` pineforge.h:548 plus `pf_native_run_spec_ext_v1` -native_c_api.h:2274 through `strategy_configure_native_ext_v1` -native_c_api.h:3120, and `strategy_native_run_v1` native_c_api.h:2659 drives +native_c_api.h:2267 through `strategy_configure_native_ext_v1` +native_c_api.h:3113, and `strategy_native_run_v1` native_c_api.h:2652 drives the bars into a `pf_report_t` pineforge.h:469. Three rules govern the C door: 1. **Every struct is tagged and size-prefixed.** Set `struct_size` - native_c_api.h:2103 and `version` native_c_api.h:2104; a mismatch is refused + native_c_api.h:2096 and `version` native_c_api.h:2097; a mismatch is refused with a documented negative status and mutates nothing. 2. **A callback must not unwind, and must return 0.** A non-zero return latches `NativeFailureCode::CallbackException` native_host.hpp:69 and ends the run - `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2886. + `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2879. 3. **Installing `on_recalculate` replaces `on_bar`.** A C host that installs only the recalculate hook and still expects the bar hook silently trades nothing. @@ -978,7 +978,7 @@ closes the stop at `101 - 20 × 0.25 = 96`. *accepted* on bar *k* and *filled* at the next eligible point — the open of bar *k+1* under the default `NativeCloseExecution::NextEligiblePoint` native_run_spec.hpp:34. Acceptance and fill are separate rows in - `native_events` native_host.hpp:1329. + `native_events` native_host.hpp:1317. 3. **`strategy.entry` reverses, `Transact` does not.** If the Pine strategy relies on an entry flipping a short into a long, spell it `ReverseTo` native_order.hpp:60. @@ -1068,8 +1068,8 @@ the twin harness of `native-feature-parity.md:331`: number shifts the moment one row is added or dropped, which turns a one-trade difference into a whole-list difference. 3. **Then diff the detail.** Event order and kind from `native_events` - native_host.hpp:1329, ownership (which opening a close consumed), and the - continuation hash `native_continuation_hash` native_host.hpp:1370. + native_host.hpp:1317, ownership (which opening a close consumed), and the + continuation hash `native_continuation_hash` native_host.hpp:1358. 4. **Explain every difference against a named row.** §1 of `native-feature-parity.md:42` marks each TradingView-only rule — money rounding, half-tick trigger thresholds, trail conventions, margin-call From 9f169bd5a9eac59909480b7ea75ffb23ac298a67 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:10:25 +0800 Subject: [PATCH 73/83] Floors: release 678 -> 686 and the excluded registrations +8 for the H-MEASURE rows R5 lane H-THIN carries The eight source-bound rows of lane H-MEASURE's three measurement commits (picked onto INT25) register in release, debug, sanitizers and native; counted with ctest -N on the lane's tree against its base 0d76a099 (release 659 -> 667 in the lane's own configuration, debug 659 -> 667, native +8). No kernel-profile row is added (270 on both trees), so KERNEL_MIN_TESTS stays 277. The two pages that cite RELEASE_MIN_TESTS by line follow it, and test_ci_verify's pin of the excluded floors is corrected with them. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 09f0bbdad145bf23a83732ebdcdbb0ef535051af) INT26 pick note: empty here. The eight rows this commit counts are H-MEASURE's, which H-THIN had cherry-picked (a27c6a2c, 6a89ddfc, cf908f75, skipped here) and which the floors already count since pick 1; H-THIN adds no CTest row of its own. ctest -N on this tree under ci_verify's own argv per profile (INT26-scratch/ctestN/hthin-*.txt) gives release 701, kernel 287 registered / 286 run, debug and sanitizers 682, native 691 -- the floors as they stand -- so this branch's floors, their citations and test_ci_verify's pin are unchanged. The commit is kept, empty, so the pick history lists every commit the late-picks list names. From cb047926e81c3a71b7cfdb9195092c8793eb34a0 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 13:35:24 +0800 Subject: [PATCH 74/83] INT26 merge: H-THIN's C++-only kernel additions in the 1.0 C boundary table; the host writes no excursion H-THIN was written on main 0d76a099, before H-DOCGATES' "1.0 C boundary" table and its kernel-member inventory; the late-picks list asks for its kernel additions to be recorded there. - docs/pages/native-engine.md, the 1.0 C boundary table: a row for `NativeStrategyHost::native_aggregates_input_bars()` (C++ only; its reason is the one the header's COVERAGE `[--]` row gives, which is its checker row), and the library row names the civil-date pair `native_civil_date` / `native_civil_days` and `pairing_aggregates` among `native_calendar.hpp`'s utilities. - scripts/check_native_c_api_surface.py: `C_V1_EXCLUSIONS["library_facilities"]` also refuses a C declaration spelled after those three, so the row fails when a C spelling appears, as the table says every such row does. - docs/adr/0001-kernel-adapter-boundary.md, the row "fields of the kernel's rows the Pine host amends": since H-THIN's E19 the Pine host writes no excursion (`sample_open_trade_extremes` and `replace_masked_entry_bar_extremes` are gone and the kernel's sampler books every Pine lot's), so `PyramidEntry::max_runup` / `max_drawdown` leave its list and the sentence that said the host samples them is replaced by what the code does. That sentence was H-DOCGATES' (c5c611a3, picked from its seam-row commit). check_native_c_api_surface.py passes (21 boundary rows, each pinned and in the table) and its self-test 29/29; check_kernel_seam_rows.py 42 members, 0 without a row. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/adr/0001-kernel-adapter-boundary.md | 2 +- docs/pages/native-engine.md | 3 ++- scripts/check_native_c_api_surface.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index dc630377..05ba65ac 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -582,7 +582,7 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | `broker_fill_event_seq_` (`include/pineforge/engine.hpp`, in the hashed region) | the Pine host alone, one step per applied broker instruction (`broker_fill_event_seq_` `pine_strategy_host.cpp:595`, in `on_native_applied`); the kernel only resets and folds it | **retained, hashed: the adapter's counter on the kernel's class**. Its placement facts (`signal_close_mc_fill_seq`) compare against it in fill-time gates that cross a bar; the execution authority is the kernel's own ordinal. It is folded into the broker-state hash (`src/engine_state_hash.cpp`), so moving it to the source layer's hash extension moves every run's hash: an epoch decision, not a docs change | | `broker_state_hashes_` (`include/pineforge/engine.hpp`) | the kernel under `KernelRecorded` (`src/native_execution_consumer.cpp`), and the Pine host at each script point it marks while recording is on (`scheduler_record_broker_hash` `pine_strategy_host.cpp:1961`) | **retained, per driving mode** (design Q4): the report's per-bar hash series, waived as append-only report output that execution never consults | | the position book's mirror -- `position_qty_`, `position_entry_price_`, `position_entry_count_`, `pyramid_entries_` (`include/pineforge/engine.hpp`, all hashed) | the kernel books and settles them. The Pine host writes them in two places. After every applied event it erases each lot at or below `internal::kQtyEpsilon` and recomputes the position's size, average price and count (`pyramid_entries_` `pine_strategy_host.cpp:600-626`, in `on_native_applied`, restating `ab9714be`'s partial-exit settlement). Before every script body it overwrites `position_entry_count_` with its own entry-slot count (`source_entry_slot_count` `pine_strategy_host.cpp:1886`) | **OPEN, two rulings nobody has made**. The dust sweep erases a lot inside a live book, which the fourth audit found with neither a row nor a tape (its item 32): lane H-THIN's A4-DUST. The count overwrite gives one hashed member two writers with two meanings: the kernel's lots in the current direction, Pine's opened entry slots. It is recorded here first and has no lane yet | -| fields of the kernel's rows the Pine host amends -- `PyramidEntry::time`, `PyramidEntry::max_runup`, `PyramidEntry::max_drawdown`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it dates their fills at the chart bar's open (`on_native_applied`; design row AG2); their bar indices are the kernel's own script-bar indices, which it books for every host (design row AG1, R5 lane K-IDX), so the host writes no index. It samples its lots' excursions itself (`sample_open_trade_extremes` in `src/source/pine_adapter.cpp`, `replace_masked_entry_bar_extremes`, the slippage mask in `on_native_bar`). It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:1277`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | +| fields of the kernel's rows the Pine host amends -- `PyramidEntry::time`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it dates their fills at the chart bar's open (`on_native_applied`; design row AG2); their bar indices are the kernel's own script-bar indices, which it books for every host (design row AG1, R5 lane K-IDX), so the host writes no index. It writes no excursion: since R5 lane H-THIN the kernel's sampler books every Pine lot's run-up and drawdown, as it does for any host that keeps the `owns_lot_excursions()` default. It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:1277`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | | the range-end report -- `range_end_trades_`, the extremes `max_equity_`, `min_equity_`, `max_drawdown_`, `max_runup_`, and `stream_warmup_mode_` (`include/pineforge/engine.hpp`) | the Pine host. `scheduler_record_range_end` clears the report rows, has the kernel's producer append them at the curve's last point and re-folds the four extremes from the re-marked curve (`range_end_trades_` `pine_strategy_host.cpp:1714`). `PineScheduler` raises `stream_warmup_mode_` for a stream's warmup, and the host clears it at the first realtime input or print | **adapter-policy, the `report_open_position_at_end` row above** (design RP5 and §3.7 part 2): report shape, not a mark-to-market row, and the kernel's run-end producer is gated out of every Pine run. The extremes are hashed; the report rows are waived as report-only | | `host_mutation_guard_inert_` (`include/pineforge/engine.hpp`) | the Pine host, once, in its constructor (`host_mutation_guard_inert_ = true` `pine_strategy_host.cpp:168`) | **retained, the host's declared choice**: an in-run call of a frozen C setter is a no-op on a Pine handle, as `ab9714be`'s legacy consumer refused it, and throws on a native host. Outside the hashed region | | `fold_exit_trail_peak_` (`include/pineforge/engine.hpp`, hashed) | nobody but the per-run reset to NaN (`reset_run_state`, `src/engine_run.cpp`) | **dead, kept by the hash** (design §3.7, "A dead branch found and reported"). Its readers test `isnan` and never take the branch, and the member's own comment says so. The broker-state hash folds it (`src/engine_state_hash.cpp`), so deleting it moves every run's hash and `BacktestEngine`'s layout: an epoch item | diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index d57d4260..2534109a 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -4163,6 +4163,7 @@ fails when this table cites a row once too few or too many. | `inspect_current_execution` | Scheduled for 1.1.0. The preview (`NativeCurrentExecutionPreview`) carries the refusal, the settlement readiness, the account projection, the closed rows' P&L and the terms outcome, and moves nothing. C has no non-mutating call: `strategy_native_execute_current_v1` applies the command, so it is not a preview. The facts are representable (`pf_native_precommit_view_v1` flattens them for `on_precommit`); the call is what is missing. | COVERAGE `[--] inspect_current_execution` | | `submit_market`, `replace_market` | C++ conveniences that refuse a non-market trigger, capacity, owner, group, anchor or late-sized intent instead of dropping it. C has no market-only guard: the same request is `strategy_native_submit_v1` / `_replace_v1` with `PF_NATIVE_TRIGGER_MARKET` and a zero-filled struct. | COVERAGE `[--] submit_market`, `[--] replace_market` | | `native_closed_rows_amended` | A C++ host that writes the kernel's closed rows names the first one it changed here. C has no write access to the rows, so it has nothing to name. | COVERAGE `[--] native_closed_rows_amended` | +| `native_aggregates_input_bars()` | Whether the configured run's script bars are buckets the kernel gathers from its input bars (`native_calendar::pairing_aggregates` of the pairing it resolved), since R5 lane H-THIN. Its one reader is the Pine host, which re-dates a closed row onto the aggregated script bar TradingView reports; a C host reads the rows the kernel books as they are, and names both timeframes in its own spec. | COVERAGE `[--] native_aggregates_input_bars` | | the deprecated `hash_source_extension` | Kept so an existing C++ subclass compiles; C has only ever had the current spelling, `on_hash_extension`. | BASE-CLASS SEAMS `[--] hash_source_extension` | | report points at the host's own cadence (`NativeReportPolicy::KernelRecordedAtHostMarks`) | Under it the host marks each report point from inside its own callbacks; a C++ host marks through `NativeStrategyHost::mark_native_report_point`, which returns `false` outside a running `KernelRecordedAtHostMarks` run. C has no call that marks one: `pf_native_report_policy_e` leaves value 2 unnamed and the C host refuses it (`PF_NATIVE_E_TAG`), because a C host could only declare a series nobody records. A C host's recorded series is `PF_NATIVE_REPORT_KERNEL_RECORDED`. | COVERAGE `[--] mark_native_report_point`, `ENUM_TWINS["pf_native_report_policy_e"]`, the `KernelRecordedAtHostMarks` exclusion | | `ReplaceOptions` (`retain_trigger_state`, `keep_handle`, `keep_binding`) | Scheduled for 1.1.0. The C replace calls take no options word: a plain replace is available, and carrying the trigger state, the handle or a book binding across it needs C++. | `C_V1_EXCLUSIONS["replace_options"]` | @@ -4184,7 +4185,7 @@ fails when this table cites a row once too few or too many. | the command results (`SubmitResult`, `ReplaceResult`, `CancelResult`, `NativeCurrentExecutionResult`) | C returns the handle or the refusal, or a status, without the event ordinal; `strategy_native_execute_current_v1` returns the outcome's code, not the event. The event history (`strategy_native_events_v1`) is the route. | `C_V1_EXCLUSIONS["command_results"]` | | `native_state()`'s staged spec and failure context (`NativeStateView::spec`, `NativeFailure::context`) | `pf_native_state_v1` carries the lifecycle, the failure's code, operation, discriminator and ordinal, and the phase facts, but not the spec or the failure's cause ordinal, recipient and cursor. | `C_V1_EXCLUSIONS["state_view"]` | | two minor ones: `trace(name, value)`, and re-declaring the optional hooks after creation | A C++ host writes report trace rows; C can turn tracing on (`strategy_set_trace_enabled`) and read the rows a report carries, not write one. A C++ declaration of `on_bar_open` or `on_precommit` stands until the host makes another; a C table fixes both once, at `strategy_native_host_create_v1`, which moves no value. | `C_V1_EXCLUSIONS["host_minor"]` | -| the C++ library around the host | Spec and curve validation and digests (`validate_native_run_spec`, `normalize_native_run_spec`, the subscription and auxiliary-feed validators, `native_run_spec_digest` and the other digests, `validate_native_fx_curve`, `native_fx_curve_digest`); the input preflight (`preflight_native_inputs`, `native_bar_structurally_valid`); the calendar utilities of `native_calendar.hpp`; the request helpers (`quantity_on_grid`, `market_request`); the module export macro of `native_module.hpp`; the run identity a `RequestHandle` carries. A C host validates by configuring (`strategy_configure_native_ext_result_v1` leaves the handle unconfigured on a refusal) and has no digest. The kernel's own machinery (`WorkingRequestCore`, `INativeDriverSink`) is no host surface in either language. | `C_V1_EXCLUSIONS["library_facilities"]` | +| the C++ library around the host | Spec and curve validation and digests (`validate_native_run_spec`, `normalize_native_run_spec`, the subscription and auxiliary-feed validators, `native_run_spec_digest` and the other digests, `validate_native_fx_curve`, `native_fx_curve_digest`); the input preflight (`preflight_native_inputs`, `native_bar_structurally_valid`); the calendar utilities of `native_calendar.hpp` (the civil-date pair `native_civil_date` / `native_civil_days` and `pairing_aggregates` among them, since R5 lane H-THIN); the request helpers (`quantity_on_grid`, `market_request`); the module export macro of `native_module.hpp`; the run identity a `RequestHandle` carries. A C host validates by configuring (`strategy_configure_native_ext_result_v1` leaves the handle unconfigured on a refusal) and has no digest. The kernel's own machinery (`WorkingRequestCore`, `INativeDriverSink`) is no host surface in either language. | `C_V1_EXCLUSIONS["library_facilities"]` | | `native_toolkit.hpp` | Header-only C++ over the host's own commands: `submit_bracket` and its receipt, and the `OrderBook` id book. A C host writes the same commands itself: each bracket leg is a `strategy_native_submit_v1` with `PF_NATIVE_OWNER_WAIT_FOR_APPLIED` and a `PF_NATIVE_GROUP_MEMBER` group, answering its own refusal, and an id book is its own key-to-handle map over `_submit_v1` / `_replace_v1` / `_cancel_v1`. | `C_V1_EXCLUSIONS["toolkit"]` | Every policy hook has a C callback since R5 lane F4: diff --git a/scripts/check_native_c_api_surface.py b/scripts/check_native_c_api_surface.py index cd3d6e3a..807d0626 100644 --- a/scripts/check_native_c_api_surface.py +++ b/scripts/check_native_c_api_surface.py @@ -238,7 +238,8 @@ ("@declarations", r"\bstrategy_(?:native_)?(?:trace|declare_\w*hook)\w*"),)), "library_facilities": ("native_run_spec.hpp", "std::uint64_t native_run_spec_digest(", ( ("@declarations", r"\b\w*(?:spec_digest|fx_curve_digest|validate_native|preflight_native" - r"|parse_timeframe|parse_session|quantity_on_grid)\w*"),)), + r"|parse_timeframe|parse_session|quantity_on_grid|civil_date|civil_days" + r"|pairing_aggregates)\w*"),)), "toolkit": ("native_toolkit.hpp", "inline BracketReceipt submit_bracket", ( ("@declarations", r"\b\w*(?:bracket|order_book)\w*"),)), } From 42e8975849c79e9246066324da22044be754300a Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 12:28:31 +0800 Subject: [PATCH 75/83] v19 hash step (PAR-CASHFEE): a percent-of-equity default quantity under a cash commission sizes from strategy.equity and leaves out its own order's fee Root cause. PineExecutionAdapter::default_sizing_cash took the percentage of percent_commission_live_equity(), which adds the adapter's open_entry_fees_ ledger (the open entries' cash-per-order / cash-per-contract fees) back onto the kernel's marked equity; the only fee the kernel reserves out of the money is a PERCENT commission (native_order::Sized::reserve_percent_fee). So under a cash commission every default percent-of-equity quantity was sized from an equity with the open cash fees restored and with no fee kept for its own order. TradingView's rule, on thirteen `lab tv` tapes (tests/fixtures/cash_fee_sizing, ws-report-v1, rangeProof covered; twelve new -- 50, 100 and 200 %, long and short, flat, pyramided, after a partial close, reversing, limit, stop and strategy.order, BINANCE:ETHUSDT.P and CME_MINI:ES1! at point value 50, margin 25 and 100 -- plus lane H-THIN's FIFO fee-basis tape): cash per order units = floor((pct * E - fee) / (price * pointvalue)) cash per contract units = floor(pct * E / (price * pointvalue + fee)) with E = strategy.equity at the sizing mark (every open entry's pro-rata fee charged) and price = the signal close (market entry, strategy.order) or the level (limit, stop); a reversal keeps one fee for its one order. The rule fits 60 of 60 sized entries per order, 54 of 54 per contract, and all 366 trades of corpus order-percent-equity-cash-commission-01; the restored basis fits only the flat entries, no reserve none. The change (adapter policy): PineSizingSnapshot::strategy_equity records strategy.equity at the mark (mark_sizing_equity, only when default_sizing_reserves_cash_fee(): a percent-of-equity default quantity, a cash-per-order or cash-per-contract commission, a positive value), and default_sizing_cash hands the kernel's CashValue the percentage of it less the per-order fee, or scaled by unit / (unit + fee) per contract. The money gates and the margin money keep the restored equity; a per-call qty_type (PineForge-only, TradingView refuses it) keeps its conversion. The new field folds into pineforge-source-adapter/v4 only where it is recorded. Tests. New row test_cash_fee_sizing_tapes replays every tape's script through the Pine adapter on its bars: fail-before on base 09f0bbda (its headers and library) 0 of 111 trades booked as TradingView books them, 134 of 174 checks failing, first "pcf-order-p50-m25 #1 A1 26.5953 vs the tape's 26.0634"; after, 111 of 111 (ids, sides, times, prices, quantities exact, commission to 1e-9, net profit to 1e-7 of |profit| + |commission|). The two margin-100 tapes are a recorded divergence: TradingView margin-calls a default 100 % entry on its entry bar once the order's cash fee takes the equity under the position, the adapter's margin equity restores that fee; the quantities sized agree, the adapter's rows are pinned. test_l4b_policy_regressions pinned the old rule: expectation corrected 20.0 -> 19.79 (9.9 + 9.89), row renamed. Hash step -- every value that moves, harvested with each TU's own switch on this tree; the same harvest on base 09f0bbda reproduces every old value: tests/test_adapter_quiet_bar_pinned.inc: 80 values over 24 rows (boundary 24, rows 12, trades 22, final_hash 22; no count), the battery's six random scenarios with a percent-of-equity default quantity and a cash-per-order fee (Random05, 06, 20, 35, 38, 46). tests/test_publication_witness_pinned.inc: 34 of 156 digests -- exactly the Config runs whose configuration sweep passes through a percent-of-equity default quantity with a positive cash fee (census by an instrumented copy of the TU) -- six of them closing a different number of trades (Config03 50 -> 48, Config03M 48 -> 44, ConfigFlags103 65 -> 63, ConfigFlags103M 66 -> 65, ConfigFlags104 and ConfigFlags104M 12 -> 13); folds and recorded rows unchanged. scripts/corpus_parity_baseline.txt: order-percent-equity-cash-commission-01 7c8f19ad -> 44ad17ec, toward TradingView (quantities inside its 0.0001 cell 0 -> 355 of 366, the rest 1e-7 to 3e-6 under it through the corpus harness's gridless equity; largest |dqty| 0.00106 -> 0.0000985; 90th percentile |d net profit| 0.029 -> 0.0023; 366 of 366 quantities and net profits closer). It is the corpus's only probe with a percent-of-equity default quantity and a cash commission. The other witness tables (report_relower, recording_hash_witness, continuation_view, receipts_in_place, command_allocations, lookup_index_witness, host_view_memo, live_state_equivalence) pass unchanged on this tree. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 214c082c8d6ceaf36e26f5377b0061ed3842d45f) INT26 pick note (late pick, accepted 2026-09-26 14:10, after H-THIN as the list says): every source, header and test-code hunk applied unchanged. Its hash values fold into the single INT26 v19 hash re-pin, so the quiet-bar and publication tables keep this branch's hash values here; the behaviour it moves is pinned here, harvested on this tree with each TU's own switch: the trade digests of quiet-bar Random05/0/*, Random06, Random20, Random35, Random38 and Random46 (the scenarios with a percent-of-equity quantity under a cash fee; Random05/0/* had been moved by an earlier pick too, so it takes this tree's own value) and the closed trades of five publication runs (ConfigFlags103 65 -> 63, Config03M 48 -> 44, ConfigFlags103M 66 -> 65, ConfigFlags104 and ConfigFlags104M 12 -> 13), each equal to the lane's; the lane's Config03 50 -> 48 was already pinned with PAR-MARGIN-2. No other pinned behaviour moves. The publication test's provenance keeps both lanes' paragraphs. The corpus line for order-percent-equity-cash-commission-01 is the lane's, re-derived once after every pick (CORPUS4, applied after this pick, stays last). --- include/pineforge/source/pine_adapter.hpp | 17 + scripts/corpus_parity_baseline.txt | 2 +- src/source/pine_adapter.cpp | 97 +++- src/source/pine_state_hash.cpp | 8 +- tests/CMakeLists.txt | 11 + tests/fixtures/cash_fee_sizing/README.md | 68 +++ tests/fixtures/cash_fee_sizing/bars.inc | 273 +++++++++ .../hthin-x15b-fifo-fee-basis/meta.json | 16 + .../hthin-x15b-fifo-fee-basis/metrics.json | 40 ++ .../hthin-x15b-fifo-fee-basis/strategy.pine | 49 ++ .../hthin-x15b-fifo-fee-basis/tv_trades.csv | 23 + .../pcf-contract-p100-m100/meta.json | 17 + .../pcf-contract-p100-m100/metrics.json | 40 ++ .../pcf-contract-p100-m100/strategy.pine | 43 ++ .../pcf-contract-p100-m100/tv_trades.csv | 19 + .../pcf-contract-p100-m25/meta.json | 17 + .../pcf-contract-p100-m25/metrics.json | 40 ++ .../pcf-contract-p100-m25/strategy.pine | 69 +++ .../pcf-contract-p100-m25/tv_trades.csv | 31 ++ .../pcf-contract-p200-m25/meta.json | 17 + .../pcf-contract-p200-m25/metrics.json | 40 ++ .../pcf-contract-p200-m25/strategy.pine | 39 ++ .../pcf-contract-p200-m25/tv_trades.csv | 13 + .../pcf-contract-p50-m25/meta.json | 17 + .../pcf-contract-p50-m25/metrics.json | 40 ++ .../pcf-contract-p50-m25/strategy.pine | 69 +++ .../pcf-contract-p50-m25/tv_trades.csv | 31 ++ .../pcf-contract-priced-m25/meta.json | 17 + .../pcf-contract-priced-m25/metrics.json | 40 ++ .../pcf-contract-priced-m25/strategy.pine | 50 ++ .../pcf-contract-priced-m25/tv_trades.csv | 15 + .../pcf-es-contract-p50-m25/meta.json | 17 + .../pcf-es-contract-p50-m25/metrics.json | 40 ++ .../pcf-es-contract-p50-m25/strategy.pine | 41 ++ .../pcf-es-contract-p50-m25/tv_trades.csv | 15 + .../pcf-es-order-p50-m25/meta.json | 17 + .../pcf-es-order-p50-m25/metrics.json | 40 ++ .../pcf-es-order-p50-m25/strategy.pine | 41 ++ .../pcf-es-order-p50-m25/tv_trades.csv | 15 + .../pcf-order-p100-m100/meta.json | 17 + .../pcf-order-p100-m100/metrics.json | 40 ++ .../pcf-order-p100-m100/strategy.pine | 43 ++ .../pcf-order-p100-m100/tv_trades.csv | 23 + .../pcf-order-p100-m25/meta.json | 17 + .../pcf-order-p100-m25/metrics.json | 40 ++ .../pcf-order-p100-m25/strategy.pine | 69 +++ .../pcf-order-p100-m25/tv_trades.csv | 31 ++ .../pcf-order-p200-m25/meta.json | 17 + .../pcf-order-p200-m25/metrics.json | 40 ++ .../pcf-order-p200-m25/strategy.pine | 39 ++ .../pcf-order-p200-m25/tv_trades.csv | 13 + .../pcf-order-p50-m25/meta.json | 17 + .../pcf-order-p50-m25/metrics.json | 40 ++ .../pcf-order-p50-m25/strategy.pine | 69 +++ .../pcf-order-p50-m25/tv_trades.csv | 31 ++ .../pcf-order-priced-m25/meta.json | 17 + .../pcf-order-priced-m25/metrics.json | 40 ++ .../pcf-order-priced-m25/strategy.pine | 50 ++ .../pcf-order-priced-m25/tv_trades.csv | 15 + tests/test_adapter_quiet_bar_pinned.inc | 72 ++- tests/test_cash_fee_sizing_tapes.cpp | 516 ++++++++++++++++++ tests/test_l4b_policy_regressions.cpp | 13 +- tests/test_publication_witness.cpp | 14 + tests/test_publication_witness_pinned.inc | 19 +- 64 files changed, 2741 insertions(+), 55 deletions(-) create mode 100644 tests/fixtures/cash_fee_sizing/README.md create mode 100644 tests/fixtures/cash_fee_sizing/bars.inc create mode 100644 tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/tv_trades.csv create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/tv_trades.csv create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/tv_trades.csv create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/tv_trades.csv create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/tv_trades.csv create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/tv_trades.csv create mode 100644 tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/tv_trades.csv create mode 100644 tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/tv_trades.csv create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/tv_trades.csv create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/tv_trades.csv create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/tv_trades.csv create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/tv_trades.csv create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/meta.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/metrics.json create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/strategy.pine create mode 100644 tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/tv_trades.csv create mode 100644 tests/test_cash_fee_sizing_tapes.cpp diff --git a/include/pineforge/source/pine_adapter.hpp b/include/pineforge/source/pine_adapter.hpp index 571725ec..476b43a6 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -151,6 +151,11 @@ struct PineSizingSnapshot { double mark = std::numeric_limits::quiet_NaN(); double frozen_units = std::numeric_limits::quiet_NaN(); bool at_fill = false; + // strategy.equity at `mark`: every open entry fee charged, the cash ones + // included, which `equity` restores for the money gates. Recorded only + // where a percent-of-equity default quantity converts it -- a cash + // commission (default_sizing_reserves_cash_fee); NaN everywhere else. + double strategy_equity = std::numeric_limits::quiet_NaN(); }; // Source placement evidence keyed by the native request handle, read by the @@ -1695,6 +1700,14 @@ class PineExecutionAdapter { // execution converts at, which its terms facts carry // (NativeExecutionTermsFacts::active_fx); none, the presented clock's. double percent_commission_live_equity(double, std::optional fx) const noexcept; + // The kernel's marked equity at `mark` (every open entry fee charged), + // at `fx` when given: strategy.equity under a cash commission. + double strategy_equity_at(double mark, std::optional fx) const noexcept; + // Records both equities a sizing snapshot carries at `mark`: `equity` + // (percent_commission_live_equity) and, for a cash-fee percent-of-equity + // default quantity, `strategy_equity`. + void mark_sizing_equity(PineSizingSnapshot&, double mark, + std::optional fx = std::nullopt) const noexcept; double quantize_close_units(double basis, double percent) const noexcept; double quantize_percent_exit_units(double requested, double available) const noexcept; @@ -1848,6 +1861,10 @@ class PineExecutionAdapter { // reserved out of it, and the lot floor applied to the core's quotient. double default_sizing_cash(const PineSizingSnapshot&) const noexcept; bool default_sizing_reserves_percent_fee() const noexcept; + // A percent-of-equity default quantity under a cash commission (per + // order or per contract) takes its percentage of strategy.equity and + // leaves out the fee its own order pays (R5 lane PAR-CASHFEE). + bool default_sizing_reserves_cash_fee() const noexcept; double default_sizing_lot_floor(double units) const noexcept; // A typed quantity (qty_type cash / percent_of_equity) names money, not // units: `money` converted at `price` by the core (native_sized_units: diff --git a/scripts/corpus_parity_baseline.txt b/scripts/corpus_parity_baseline.txt index f4a80b7d..8c2e6829 100644 --- a/scripts/corpus_parity_baseline.txt +++ b/scripts/corpus_parity_baseline.txt @@ -175,7 +175,7 @@ a9b3c604af9abc026139582f4a03c022ccf8837d179cb3f8755545acd56dc8f4 validation/ord 51713bad5e6e39df38618d2f19aed298564bb21d5f494b39db8fb9e10e08cbb9 validation/order-market-close-fill-basis-01/engine_trades.csv c87ea211867e2348597e9bfa8f4d5fa8bbe7042f61f209d8b565c7a2f1f0672e validation/order-one-side-four-bar-far-opposite-01/engine_trades.csv 75929843feb30167929c8d3d6741797813897a6b7d1d40705accca392db2058f validation/order-opposite-entry-close-same-pass-01/engine_trades.csv -7c8f19ade2b6fb3e505fd4a553456e666ddde57bfbfa20c7dda34985ba03152b validation/order-percent-equity-cash-commission-01/engine_trades.csv +44ad17ecbbdca31a9e96502d7f809e575b155b1741a8266bebe97baf770a9b4d validation/order-percent-equity-cash-commission-01/engine_trades.csv a417e420e06d9d901da04455678cd447c267ff952435d5eb37d6e1990727b896 validation/order-process-on-close-false-01/engine_trades.csv 0d4fd87450627c8c857d41b85b4f650feb86daf8e2af2f8d4b7157ef38dcdb30 validation/order-process-on-close-true-01/engine_trades.csv 24f93efaabd752be076564c8a2f16f233144ae91bbc5b2f852ace6f91a549531 validation/order-range-expansion-pending-stop-01/engine_trades.csv diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 956243b1..471636f7 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -2353,7 +2353,7 @@ PineSizingSnapshot PineExecutionAdapter::sizing_snapshot() const { // for frozen sizing, money-band admission and the paired FX fact. snapshot.mark = nearest_tick(point->price, staged_.syminfo.mintick); snapshot.price = snapshot.mark; - snapshot.equity = percent_commission_live_equity(snapshot.mark); + mark_sizing_equity(snapshot, snapshot.mark); } snapshot.fx = staged_.account_fx; if (const auto point = detail::callback_point(host)) @@ -2369,15 +2369,37 @@ PineSizingSnapshot PineExecutionAdapter::sizing_snapshot() const { // generic EquityFraction basis deliberately does not model, so the source // hands the core the money and the core converts it. Any other declaration // has no money of its own and returns NaN. +// +// Under a cash commission the percentage is taken of strategy.equity -- every +// open entry fee charged, the cash fees percent_commission_live_equity +// restores included -- and the fee the order itself pays is left out of the +// money: the value per order, or the value per unit, so that the units' notional +// and their fee together spend the money (units = pct * E / (price * pointvalue +// + fee)). R5 lane PAR-CASHFEE, `lab tv` tapes tests/fixtures/cash_fee_sizing: +// every sized entry of the thirteen tapes there (percentages 50, 100 and 200, +// long and short, flat, pyramided, after a partial close, reversing, priced +// entries and strategy.order, point values 1 and 50) and all 366 trades of +// corpus order-percent-equity-cash-commission-01. double PineExecutionAdapter::default_sizing_cash( const PineSizingSnapshot& sizing) const noexcept { if (config_.default_qty_type == static_cast(QtyType::CASH)) { return config_.default_qty_value; } - if (config_.default_qty_type != static_cast(QtyType::PERCENT_OF_EQUITY) - || !finite_positive(sizing.equity)) { - return kNaN; - } + if (config_.default_qty_type != static_cast(QtyType::PERCENT_OF_EQUITY)) return kNaN; + if (default_sizing_reserves_cash_fee()) { + if (!finite_positive(sizing.strategy_equity)) return kNaN; + const double equity = staged_.quantity_grid ? source_money_round(sizing.strategy_equity) + : sizing.strategy_equity; + const double money = config_.default_qty_value / 100.0 * equity; + if (config_.commission_type == static_cast(CommissionType::CASH_PER_ORDER)) + return money - config_.commission_value; + // The core converts cash / unit_cost; this cash leaves + // units * (unit_cost + fee) == money. + const double unit_cost = sizing.price * staged_.syminfo.pointvalue * sizing.fx; + if (!finite_positive(unit_cost)) return kNaN; + return money * unit_cost / (unit_cost + config_.commission_value); + } + if (!finite_positive(sizing.equity)) return kNaN; const double equity = staged_.quantity_grid ? source_money_round(sizing.equity) : sizing.equity; return config_.default_qty_value / 100.0 * equity; } @@ -2392,6 +2414,13 @@ bool PineExecutionAdapter::default_sizing_reserves_percent_fee() const noexcept && config_.commission_value > 0.0; } +bool PineExecutionAdapter::default_sizing_reserves_cash_fee() const noexcept { + return config_.default_qty_type == static_cast(QtyType::PERCENT_OF_EQUITY) + && (config_.commission_type == static_cast(CommissionType::CASH_PER_ORDER) + || config_.commission_type == static_cast(CommissionType::CASH_PER_CONTRACT)) + && config_.commission_value > 0.0; +} + // The source lot floor. The two declarations do not share one: a percentage // quantity uses the money floor with its cent-grid case, a cash quantity the // ordinary grid helper with its proportional epsilon. Neither is the core's @@ -2505,7 +2534,7 @@ bool PineExecutionAdapter::core_sizes_default_opening(bool is_long) const { PineSizingSnapshot sizing = sizing_snapshot(); if (!finite_positive(sizing.mark)) return false; sizing.price = default_market_sizing_price(sizing.mark, is_long); - sizing.equity = percent_commission_live_equity(sizing.mark); + mark_sizing_equity(sizing, sizing.mark); return finite_positive(default_sizing_units(sizing)) && default_sizing_intent(sizing, is_long).has_value() && core_sizing_price_matches(sizing, is_long); @@ -4184,14 +4213,15 @@ double PineExecutionAdapter::percent_commission_live_equity( return (pine->current_equity() + open) - paid_open_commission; } } - const auto* pine = pine_view_of(host_); - const double marked = fx && pine ? pine->marked_equity_at(mark, *fx) - : host_->native_marked_equity(mark); + const double marked = strategy_equity_at(mark, fx); if (!std::isfinite(marked)) return marked; - // `marked_equity()` accounts for every open entry fee. Pine's sizing - // basis subtracts only surviving PERCENT entry commissions, so restore the - // adapter-recorded cash-per-order/contract fees without changing generic - // accounting or marked equity itself. + // `marked_equity()` accounts for every open entry fee. The money gates + // and the margin money read an equity that subtracts only surviving + // PERCENT entry commissions, so restore the adapter-recorded + // cash-per-order/contract fees without changing generic accounting or + // marked equity itself. A percent-of-equity default QUANTITY does not + // read this: TradingView sizes it from strategy.equity, cash fees charged + // (default_sizing_cash, R5 lane PAR-CASHFEE). double restored = 0.0; for (const auto& fact : open_entry_fees_) { if (!std::isfinite(fact.nonpercent_fee)) @@ -4202,6 +4232,20 @@ double PineExecutionAdapter::percent_commission_live_equity( : std::numeric_limits::quiet_NaN(); } +double PineExecutionAdapter::strategy_equity_at( + double mark, std::optional fx) const noexcept { + if (!host_) return std::numeric_limits::quiet_NaN(); + const auto* pine = pine_view_of(host_); + return fx && pine ? pine->marked_equity_at(mark, *fx) : host_->native_marked_equity(mark); +} + +void PineExecutionAdapter::mark_sizing_equity( + PineSizingSnapshot& sizing, double mark, std::optional fx) const noexcept { + sizing.equity = percent_commission_live_equity(mark, fx); + sizing.strategy_equity = default_sizing_reserves_cash_fee() + ? strategy_equity_at(mark, fx) : kNaN; +} + void PineExecutionAdapter::record_opening_fee( const PlacementSnapshot& source, const native_order::ExecutionAppliedEvent& event) { const double opened = std::abs(event.opened_units); @@ -6498,7 +6542,7 @@ void PineExecutionAdapter::entry(const SourceId& id, bool is_long, double limit_ } if (default_sized && !priced && finite_positive(snapshot.sizing.mark)) { snapshot.sizing.price = default_market_sizing_price(snapshot.sizing.mark, is_long); - snapshot.sizing.equity = percent_commission_live_equity(snapshot.sizing.mark); + mark_sizing_equity(snapshot.sizing, snapshot.sizing.mark); } const auto predecessor = live_by_source_key_.find(key_for(id)); snapshot.replaced_opening = predecessor != live_by_source_key_.end(); @@ -12539,16 +12583,18 @@ native_order::ExecutionTerms PineExecutionAdapter::resolve_terms( } else if (config_.default_qty_type == static_cast(QtyType::FIXED)) { result.units = config_.default_qty_value; } else { - const double equity = source.sizing.at_fill - ? percent_commission_live_equity(result.resolved_price, facts.active_fx) - : source.sizing.equity; const double price = source.sizing.at_fill ? result.resolved_price : source.sizing.price; const double fx = source.sizing.at_fill ? facts.active_fx : source.sizing.fx; PineSizingSnapshot sizing; sizing.price = price; sizing.fx = fx; sizing.mark = price; - sizing.equity = equity; + if (source.sizing.at_fill) { + mark_sizing_equity(sizing, result.resolved_price, facts.active_fx); + } else { + sizing.equity = source.sizing.equity; + sizing.strategy_equity = source.sizing.strategy_equity; + } // ab9714be pine_policy_members.cpp:282: default sizing units divides equity by price, pointvalue, and currency fx result.units = default_sizing_units(sizing); } @@ -15056,8 +15102,8 @@ void PineExecutionAdapter::refresh_pending_sizing_after_margin( const NativeDecisionContext& context) { const double mark = policy_script_bar_valid_ ? policy_script_bar_.close : event.resolved_price; - const double marked_equity = percent_commission_live_equity( - nearest_tick(mark, staged_.syminfo.mintick)); + PineSizingSnapshot marked; + mark_sizing_equity(marked, nearest_tick(mark, staged_.syminfo.mintick)); const double active_fx = active_staged_fx(context.sub_bar_open_ms); const std::uint64_t cause_fill = static_cast(require_host()).broker_fill_event_seq_; @@ -15080,7 +15126,8 @@ void PineExecutionAdapter::refresh_pending_sizing_after_margin( && !finite_positive(snapshot.exit_levels.limit) && !finite_positive(snapshot.exit_levels.stop); if (market_entry && std::isfinite(snapshot.sizing.frozen_units)) { - snapshot.sizing.equity = marked_equity; + snapshot.sizing.equity = marked.equity; + snapshot.sizing.strategy_equity = marked.strategy_equity; snapshot.sizing.fx = active_fx; snapshot.sizing.frozen_units = default_sizing_units(snapshot.sizing); revised = true; @@ -18668,8 +18715,12 @@ int PendingIntentView::probe_fill_qty(int index, double fill_price, double* qty, PineSizingSnapshot sizing = owner_->sizing_snapshot(); sizing.price = sized_price; sizing.mark = sized_price; - sizing.equity = finite_positive(sized_price) - ? owner_->percent_commission_live_equity(sized_price) : kNaN; + if (finite_positive(sized_price)) { + owner_->mark_sizing_equity(sizing, sized_price); + } else { + sizing.equity = kNaN; + sizing.strategy_equity = kNaN; + } *qty = owner_->default_sizing_units(sizing); } else { *qty = floor_quantity_grid(owner_->config_.default_qty_value, diff --git a/src/source/pine_state_hash.cpp b/src/source/pine_state_hash.cpp index 74173b06..f1874d9b 100644 --- a/src/source/pine_state_hash.cpp +++ b/src/source/pine_state_hash.cpp @@ -4,6 +4,7 @@ #include "../broker_state_hash_internal.hpp" #include +#include #include #include @@ -107,7 +108,12 @@ void hash_placement(BrokerStateHashSink& f, const source::PlacementSnapshot& val f.d(value.projection_affordability_held_qty); f.d(value.sizing.equity); f.d(value.sizing.price); f.d(value.sizing.fx); f.d(value.sizing.mark); f.d(value.sizing.frozen_units); - f.b(value.sizing.at_fill); f.d(value.exit_levels.limit); f.d(value.exit_levels.stop); + f.b(value.sizing.at_fill); + // Recorded only under a cash commission with a percent-of-equity default + // quantity (the folded run configuration decides), so every other run + // keeps its digest (R5 lane PAR-CASHFEE). + if (!std::isnan(value.sizing.strategy_equity)) f.d(value.sizing.strategy_equity); + f.d(value.exit_levels.limit); f.d(value.exit_levels.stop); f.d(value.exit_levels.trail_points); f.d(value.exit_levels.trail_offset); f.d(value.exit_levels.trail_price); f.d(value.exit_levels.profit_ticks); f.d(value.exit_levels.loss_ticks); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e8345357..5a5e37bd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1390,6 +1390,11 @@ list(APPEND TEST_SOURCES test_flat_coof_exit_tapes) list(APPEND TEST_SOURCES test_second_extreme_order_tapes) # A process_orders_on_close stop entry placed while the book is still open. list(APPEND TEST_SOURCES test_pooc_reversing_stop_tapes) +# R5 lane PAR-CASHFEE: a percent-of-equity default quantity under a cash +# commission (per order, per contract) against TradingView's own trades -- the +# `lab tv` tapes under tests/fixtures/cash_fee_sizing (README.md there names +# them) replayed on their bars through the Pine adapter. Source-bound. +list(APPEND TEST_SOURCES test_cash_fee_sizing_tapes) # R4-D L4b-fix restores every removed ShortSeed, affordability, sizing and # deferred-close oracle as an individually compiled native-route twin. Keep @@ -2146,6 +2151,12 @@ foreach(_pf_hm_row IN ITEMS endif() endforeach() +# R5 lane PAR-CASHFEE reads its TradingView tapes from the fixture tree too. +if(TARGET test_cash_fee_sizing_tapes) + target_compile_definitions(test_cash_fee_sizing_tapes PRIVATE + PINEFORGE_CASH_FEE_FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/fixtures/cash_fee_sizing") +endif() + # R5 E21 reads the kernel's own sources to prove BacktestEngine::marked_equity_at # is the one implementation of the marked equity. Kernel files only, so the # kernel-only profile runs the row too. diff --git a/tests/fixtures/cash_fee_sizing/README.md b/tests/fixtures/cash_fee_sizing/README.md new file mode 100644 index 00000000..f6af8712 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/README.md @@ -0,0 +1,68 @@ +# Percent-of-equity sizing under a cash commission (R5 lane PAR-CASHFEE) + +TradingView's own trades for one question: what quantity does a +`default_qty_type = strategy.percent_of_equity` order get when the strategy +pays a cash commission (`strategy.commission.cash_per_order` or +`cash_per_contract`)? Every script opens from flat on a UTC day of its own, +pyramids, closes part, reverses, or places a priced entry or a +`strategy.order`, and ends the day with `strategy.close_all`. Their headers +name each scenario. + +TradingView takes the percentage of `strategy.equity` -- every open entry's +fee charged, the cash fees included -- and leaves out of that money the fee the +order itself pays, then floors onto the quantity grid: + +- cash per order: `floor((pct * E - fee) / (price * pointvalue))`; +- cash per contract: `floor(pct * E / (price * pointvalue + fee))`; + +`price` being the signal close for a market `strategy.entry` and for +`strategy.order`, and the level for a limit or stop entry. A reversal sizes its +new position the same way from the equity that still carries the reversed +position, and its one order pays one fee. Reading `E` off each tape (initial +capital, net profit closed before the signal, open pieces marked at the signal +close less their pro-rata entry fee), the rule fits all 60 sized entries of the +seven per-order tapes and all 54 of the six per-contract tapes. + +`tests/test_cash_fee_sizing_tapes.cpp` replays every script through the Pine +adapter on `bars.inc` and compares every trade with its tape: the eleven tapes +without an entry-bar margin call trade for trade (111 trades), the two +`*-p100-m100` tapes up to the margin call TradingView books on the second +scenario's entry bar and the adapter does not (a recorded divergence; the +adapter's rows are pinned). `docs/design/native-feature-parity.md` §3.10 has +the measurement. + +Each directory is one `lab tv` export (pineforge-workflow, channel +`ws-report-v1`, `rangeProof: covered`, `--no-note`), byte-identical: +`strategy.pine`, `tv_trades.csv` (times UTC+8), `meta.json`, `metrics.json`. +`hthin-x15b-fifo-fee-basis` is lane H-THIN's tape (its X15 b measurement: a +FIFO `strategy.exit(from_entry)` closes an older lot of another id before the +next default entry sizes), copied from that lane's scratch. + +| tape | chart | window | trades | tv_trades.csv sha256 | +|---|---|---|---|---| +| `pcf-order-p50-m25` | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2025-05-01 | 15 | `5b858a622a4d8afdb8e3297fbe74e83fdeb6ba2701be8fd8a4bc13c8dac55849` | +| `pcf-order-p100-m25` | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2025-05-01 | 15 | `93cd6031e84fb1b303e9d22cc8a8cc3f5af7dfe48b640c9b31cc819656519602` | +| `pcf-order-p200-m25` | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2025-05-01 | 6 | `8e105f593fe1da7900b19eaba74a28b9ce61c20d0178f18d88e09bb673aa9b85` | +| `pcf-order-priced-m25` | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2025-05-01 | 7 | `3210a82cc426fb6cd5d7a1a09027a7aeab8eccc2b512cd88b893c42efee6da57` | +| `pcf-order-p100-m100` | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2025-05-01 | 11 | `5fe7fb71f114a7ecc08a295cd373d4cfd93d1f6f8be9545875d32e876620bab4` | +| `pcf-es-order-p50-m25` | CME_MINI:ES1! 15 | 2025-06-01 .. 2025-07-01 | 7 | `05882d7a78e3a8ca6a3e8a49249ff66169b30cbe23982c547f977e2fae387f44` | +| `pcf-contract-p50-m25` | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2025-05-01 | 15 | `4c357f02aa06ceb6577a8ba6ba601f73dddb771536401989765cd5a208256852` | +| `pcf-contract-p100-m25` | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2025-05-01 | 15 | `e713dda381f35e43a88506821004aa82d2afccd2e39a69dd2746100083accba3` | +| `pcf-contract-p200-m25` | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2025-05-01 | 6 | `fcf3e66efb18a028b94333fadcb89290888216c56b507b403b12423f7c2f561e` | +| `pcf-contract-priced-m25` | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2025-05-01 | 7 | `c9b9c711279b8e20a3abfa34b6a9ef968e8c8c6ff776eb03db3e30b99f91d9ec` | +| `pcf-contract-p100-m100` | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2025-05-01 | 9 | `38cdc2cee4d123b0f540e2d4514249bae4a5f46be2247d45714652ba8dfe2077` | +| `pcf-es-contract-p50-m25` | CME_MINI:ES1! 15 | 2025-06-01 .. 2025-07-01 | 7 | `953754c5e1e3ed239f9375b3834f3d20c89026b53d69edb9cbc911ff13a31a5e` | +| `hthin-x15b-fifo-fee-basis` | BINANCE:ETHUSDT.P 15 | 2025-04-01 .. 2025-05-01 | 11 | `f170f906d10112fbb6524fc51747c3b7e64e063651114449c7b8364b4ce9a075` | + +`bars.inc`: the windows of the two feeds the scripts trade on -- the corpus +feed's BINANCE:ETHUSDT.P 15m bars (`scripts/derive_corpus_feeds.py` at corpus +gitlink f989e36) from each scenario day's 10:00 UTC signal bar through its +closing fill, and the registry feed `lab bars CME_MINI:ES1! 15` (evidence +sha256 in the file) for the ES days. An account flat between windows books +nothing on the bars left out: on the ETH tapes the adapter's trades over the +whole of 2025-04 and over the windows are the same, row for row. + +TradingView refuses a per-call `qty_type` on `strategy.entry` (pine-facade: +`The "{signature}" function does not have an argument with the name "{name}"`, +at the `qty_type` argument), so the typed quantity the adapter also accepts has +no tape here. diff --git a/tests/fixtures/cash_fee_sizing/bars.inc b/tests/fixtures/cash_fee_sizing/bars.inc new file mode 100644 index 00000000..121faacf --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/bars.inc @@ -0,0 +1,273 @@ +// R5 lane PAR-CASHFEE: the bars every tape in this directory trades on, the +// windows only -- each scenario day from its 10:00 UTC signal bar through the +// fill of its closing order (an account flat between windows books nothing on +// the bars left out, so the run is the one TradingView made over the month). +// {timestamp_ms, open, high, low, close} +// +// BINANCE:ETHUSDT.P 15: corpus/data/derived/ohlcv_ETH-USDT-USDT_15m.csv, which +// scripts/derive_corpus_feeds.py materializes from the corpus feed at gitlink +// f989e36; 2025-04 days 2, 3, 4, 8, 10, 12, 14, 15, 17, 22, 23, 24, 25, 28, 29, +// 30, 10:00 .. 12:45 UTC (12 bars each). +constexpr FeedBar kEthBars[] = { + {1743588000000LL, 1878.43, 1883.8, 1876.15, 1880.03}, + {1743588900000LL, 1880.02, 1886.7, 1878.19, 1878.91}, + {1743589800000LL, 1878.9, 1878.98, 1871.85, 1874.48}, + {1743590700000LL, 1874.47, 1876.52, 1870, 1871.92}, + {1743591600000LL, 1871.93, 1877.62, 1871, 1874.59}, + {1743592500000LL, 1874.59, 1877.53, 1872.5, 1872.6}, + {1743593400000LL, 1872.6, 1873.99, 1868.5, 1871.4}, + {1743594300000LL, 1871.39, 1872.64, 1867.32, 1868.05}, + {1743595200000LL, 1868.08, 1869.97, 1859.53, 1865.27}, + {1743596100000LL, 1865.26, 1865.99, 1857.06, 1857.87}, + {1743597000000LL, 1857.87, 1867.48, 1857.54, 1866.86}, + {1743597900000LL, 1866.85, 1872.55, 1863.5, 1871.16}, + {1743674400000LL, 1810.48, 1816.74, 1808.24, 1811.57}, + {1743675300000LL, 1811.58, 1813.4, 1797.15, 1799.49}, + {1743676200000LL, 1799.4, 1802.86, 1789.09, 1798.19}, + {1743677100000LL, 1798.17, 1802.09, 1790.01, 1792.39}, + {1743678000000LL, 1792.39, 1801.81, 1786.14, 1800.91}, + {1743678900000LL, 1800.91, 1801.68, 1792.06, 1795.68}, + {1743679800000LL, 1795.69, 1799.1, 1791.4, 1797.93}, + {1743680700000LL, 1797.93, 1798.6, 1792.12, 1794.39}, + {1743681600000LL, 1794.39, 1799.82, 1789.77, 1797.79}, + {1743682500000LL, 1797.78, 1797.78, 1785.03, 1787.76}, + {1743683400000LL, 1787.77, 1794.92, 1759.81, 1766.99}, + {1743684300000LL, 1767, 1775.87, 1747.47, 1758.57}, + {1743760800000LL, 1828.5, 1831.3, 1808.89, 1812.7}, + {1743761700000LL, 1812.69, 1814.76, 1790.5, 1792.93}, + {1743762600000LL, 1792.93, 1800.42, 1787.5, 1788}, + {1743763500000LL, 1788, 1792.62, 1779.77, 1782.8}, + {1743764400000LL, 1782.8, 1786.96, 1775.89, 1778.99}, + {1743765300000LL, 1779, 1784.86, 1763.27, 1766.4}, + {1743766200000LL, 1766.4, 1777.56, 1757.52, 1766.38}, + {1743767100000LL, 1766.37, 1783.97, 1766.16, 1783.16}, + {1743768000000LL, 1783.15, 1783.15, 1775.88, 1780.12}, + {1743768900000LL, 1780.11, 1784, 1772.81, 1776.92}, + {1743769800000LL, 1776.92, 1802, 1768.57, 1780.31}, + {1743770700000LL, 1780.31, 1788.1, 1776, 1788.1}, + {1744106400000LL, 1567.47, 1571.64, 1565, 1567.2}, + {1744107300000LL, 1567.2, 1571.1, 1566.39, 1568.96}, + {1744108200000LL, 1568.96, 1573.75, 1567.28, 1569.01}, + {1744109100000LL, 1569, 1571.27, 1564.38, 1565.57}, + {1744110000000LL, 1565.57, 1572.88, 1564.16, 1567.12}, + {1744110900000LL, 1567.13, 1574.81, 1564.5, 1571.24}, + {1744111800000LL, 1571.24, 1585.66, 1570.84, 1581.32}, + {1744112700000LL, 1581.33, 1589.48, 1580.5, 1587.56}, + {1744113600000LL, 1587.57, 1592.79, 1581.71, 1584.28}, + {1744114500000LL, 1584.27, 1584.66, 1573.44, 1577.53}, + {1744115400000LL, 1577.52, 1579.94, 1567, 1568.63}, + {1744116300000LL, 1568.62, 1573.05, 1566.76, 1570.71}, + {1744279200000LL, 1603.23, 1605.23, 1600.18, 1601.17}, + {1744280100000LL, 1601.17, 1602.99, 1595, 1596.29}, + {1744281000000LL, 1596.29, 1597.72, 1589.01, 1590.36}, + {1744281900000LL, 1590.36, 1594.81, 1587, 1594.01}, + {1744282800000LL, 1594, 1596.12, 1585.69, 1585.87}, + {1744283700000LL, 1585.88, 1589.75, 1580.5, 1586.04}, + {1744284600000LL, 1586.04, 1593.47, 1585.35, 1590.06}, + {1744285500000LL, 1590.05, 1595.65, 1588, 1593.92}, + {1744286400000LL, 1593.91, 1621.14, 1586.1, 1595.9}, + {1744287300000LL, 1595.9, 1627.61, 1593.11, 1612.71}, + {1744288200000LL, 1612.7, 1626.93, 1593.5, 1598.24}, + {1744289100000LL, 1598.24, 1598.63, 1581.12, 1592.63}, + {1744452000000LL, 1596.71, 1596.91, 1591.36, 1592.65}, + {1744452900000LL, 1592.65, 1595.88, 1590.67, 1593.56}, + {1744453800000LL, 1593.55, 1593.97, 1588.17, 1588.18}, + {1744454700000LL, 1588.18, 1591.67, 1586.8, 1591.21}, + {1744455600000LL, 1591.21, 1596.64, 1591.21, 1596.49}, + {1744456500000LL, 1596.48, 1597.88, 1594.93, 1596.98}, + {1744457400000LL, 1596.97, 1596.98, 1592.51, 1593.72}, + {1744458300000LL, 1593.71, 1595, 1592.67, 1593.21}, + {1744459200000LL, 1593.22, 1596.14, 1589.79, 1595.99}, + {1744460100000LL, 1596, 1598.43, 1593.89, 1596.42}, + {1744461000000LL, 1596.42, 1616, 1592.55, 1602.46}, + {1744461900000LL, 1602.47, 1608.9, 1598.27, 1606.37}, + {1744624800000LL, 1640.56, 1654.99, 1638.44, 1646.28}, + {1744625700000LL, 1646.3, 1669.07, 1646.25, 1662.05}, + {1744626600000LL, 1662.06, 1680, 1662.04, 1678.4}, + {1744627500000LL, 1678.4, 1684.47, 1673.22, 1678.4}, + {1744628400000LL, 1678.4, 1688.62, 1671.82, 1685.55}, + {1744629300000LL, 1685.54, 1691.57, 1672.92, 1675.4}, + {1744630200000LL, 1675.4, 1679.56, 1670.86, 1672.02}, + {1744631100000LL, 1672.01, 1677.2, 1671.68, 1674.63}, + {1744632000000LL, 1674.63, 1677.58, 1665.18, 1666.11}, + {1744632900000LL, 1666.11, 1674.19, 1665.14, 1672.41}, + {1744633800000LL, 1672.42, 1676.08, 1670.75, 1671.77}, + {1744634700000LL, 1671.77, 1677.14, 1669.87, 1677.14}, + {1744711200000LL, 1647.7, 1650.8, 1642.07, 1642.53}, + {1744712100000LL, 1642.52, 1643.01, 1631.72, 1634.61}, + {1744713000000LL, 1634.6, 1635.86, 1625.25, 1628.63}, + {1744713900000LL, 1628.64, 1632.37, 1625.57, 1632.31}, + {1744714800000LL, 1632.3, 1632.62, 1627, 1628.15}, + {1744715700000LL, 1628.14, 1628.8, 1621.32, 1628.24}, + {1744716600000LL, 1628.23, 1631.42, 1625.59, 1630.98}, + {1744717500000LL, 1630.98, 1632, 1627.53, 1631.18}, + {1744718400000LL, 1631.17, 1637.55, 1629.66, 1635.18}, + {1744719300000LL, 1635.17, 1636.4, 1629.22, 1630.35}, + {1744720200000LL, 1630.32, 1634.54, 1627.81, 1633.62}, + {1744721100000LL, 1633.62, 1642.33, 1631.52, 1641.09}, + {1744884000000LL, 1594.54, 1596.28, 1588.57, 1588.97}, + {1744884900000LL, 1588.98, 1593.89, 1587.69, 1593.38}, + {1744885800000LL, 1593.38, 1594.49, 1590.52, 1591.53}, + {1744886700000LL, 1591.54, 1593.52, 1590.37, 1592.91}, + {1744887600000LL, 1592.92, 1596.49, 1588.15, 1589.84}, + {1744888500000LL, 1589.83, 1593.83, 1589.5, 1592.79}, + {1744889400000LL, 1592.8, 1596.09, 1590.05, 1594.79}, + {1744890300000LL, 1594.79, 1598.02, 1592.3, 1592.5}, + {1744891200000LL, 1592.51, 1598.47, 1592.16, 1594.77}, + {1744892100000LL, 1594.77, 1602.76, 1594.2, 1600.07}, + {1744893000000LL, 1600.06, 1601, 1596.37, 1598.11}, + {1744893900000LL, 1598.12, 1601.24, 1597.14, 1600.93}, + {1745316000000LL, 1624.65, 1624.86, 1620.16, 1623.42}, + {1745316900000LL, 1623.42, 1623.78, 1612.41, 1615.91}, + {1745317800000LL, 1615.9, 1629, 1615.5, 1627.01}, + {1745318700000LL, 1627, 1629.37, 1625.64, 1627.65}, + {1745319600000LL, 1627.65, 1629.37, 1624.86, 1627.11}, + {1745320500000LL, 1627.11, 1633.28, 1626.78, 1629.01}, + {1745321400000LL, 1629, 1638, 1626.7, 1628.56}, + {1745322300000LL, 1628.55, 1630.76, 1627.52, 1630.14}, + {1745323200000LL, 1630.14, 1633.12, 1628.46, 1631.46}, + {1745324100000LL, 1631.46, 1634.96, 1630.69, 1633.81}, + {1745325000000LL, 1633.82, 1636.89, 1626.45, 1633.26}, + {1745325900000LL, 1633.25, 1639.65, 1631.08, 1634.13}, + {1745402400000LL, 1785.02, 1789.37, 1778.29, 1783.39}, + {1745403300000LL, 1783.39, 1798.77, 1782.5, 1795.26}, + {1745404200000LL, 1795.25, 1798.88, 1792.02, 1794.6}, + {1745405100000LL, 1794.6, 1809, 1793.5, 1802.96}, + {1745406000000LL, 1802.97, 1806.8, 1798.64, 1801.5}, + {1745406900000LL, 1801.5, 1812.04, 1800.14, 1803}, + {1745407800000LL, 1803, 1806.65, 1796.16, 1796.46}, + {1745408700000LL, 1796.46, 1800, 1791.83, 1796.74}, + {1745409600000LL, 1796.74, 1838, 1794.31, 1817.09}, + {1745410500000LL, 1817.08, 1819.15, 1808.12, 1814.7}, + {1745411400000LL, 1814.69, 1818, 1807.25, 1810.15}, + {1745412300000LL, 1810.15, 1815.97, 1808.37, 1814.3}, + {1745488800000LL, 1742.7, 1744.35, 1739.12, 1742.76}, + {1745489700000LL, 1742.76, 1747.9, 1742.55, 1746.47}, + {1745490600000LL, 1746.47, 1749.28, 1743.03, 1748.51}, + {1745491500000LL, 1748.51, 1750.92, 1745.51, 1749.46}, + {1745492400000LL, 1749.47, 1749.9, 1744.14, 1744.91}, + {1745493300000LL, 1744.91, 1745.81, 1742.13, 1745.2}, + {1745494200000LL, 1745.2, 1746.9, 1740.51, 1745.4}, + {1745495100000LL, 1745.4, 1756.7, 1745.4, 1754.05}, + {1745496000000LL, 1754.04, 1755, 1746.44, 1750.12}, + {1745496900000LL, 1750.11, 1754.34, 1748.86, 1751.05}, + {1745497800000LL, 1751.05, 1756.75, 1749.39, 1754.14}, + {1745498700000LL, 1754.13, 1757.26, 1751.49, 1756.99}, + {1745575200000LL, 1787.43, 1791.22, 1776.83, 1783.41}, + {1745576100000LL, 1783.4, 1789.77, 1779.5, 1780.36}, + {1745577000000LL, 1780.36, 1781.88, 1771.58, 1773.12}, + {1745577900000LL, 1773.13, 1775.44, 1768, 1770.76}, + {1745578800000LL, 1770.77, 1783.72, 1770.66, 1783.34}, + {1745579700000LL, 1783.34, 1787.29, 1781.28, 1784.72}, + {1745580600000LL, 1784.73, 1785.52, 1779.29, 1780.64}, + {1745581500000LL, 1780.65, 1789.56, 1780.09, 1789.47}, + {1745582400000LL, 1789.47, 1802, 1784.12, 1795.64}, + {1745583300000LL, 1795.64, 1797.76, 1781.86, 1785.56}, + {1745584200000LL, 1785.56, 1791.07, 1783.29, 1788.9}, + {1745585100000LL, 1788.9, 1789.11, 1769.45, 1772}, + {1745834400000LL, 1804.84, 1808, 1797.77, 1804.25}, + {1745835300000LL, 1804.24, 1805.31, 1787.1, 1795}, + {1745836200000LL, 1794.99, 1823.59, 1794.91, 1819.33}, + {1745837100000LL, 1819.38, 1823.7, 1812.38, 1814.41}, + {1745838000000LL, 1814.42, 1817.83, 1809.29, 1812.29}, + {1745838900000LL, 1812.29, 1819.93, 1811.64, 1819.72}, + {1745839800000LL, 1819.72, 1826.71, 1817.26, 1820.92}, + {1745840700000LL, 1820.91, 1823.41, 1813.54, 1815}, + {1745841600000LL, 1815, 1816.48, 1806, 1808.09}, + {1745842500000LL, 1808.09, 1811.82, 1806.37, 1811.37}, + {1745843400000LL, 1811.37, 1812.3, 1805.63, 1812.17}, + {1745844300000LL, 1812.17, 1813.96, 1803.13, 1805.7}, + {1745920800000LL, 1831.05, 1842.82, 1830.01, 1835.74}, + {1745921700000LL, 1835.74, 1841.16, 1831.3, 1832.58}, + {1745922600000LL, 1832.59, 1833.09, 1824.39, 1826.99}, + {1745923500000LL, 1826.99, 1829.39, 1821.96, 1824.97}, + {1745924400000LL, 1824.97, 1836.28, 1824.21, 1835.49}, + {1745925300000LL, 1835.5, 1837.65, 1831.8, 1831.95}, + {1745926200000LL, 1831.95, 1834, 1826.78, 1827.27}, + {1745927100000LL, 1827.27, 1829.64, 1826.44, 1828.9}, + {1745928000000LL, 1828.89, 1832.36, 1825.75, 1830.2}, + {1745928900000LL, 1830.2, 1832.58, 1826.01, 1828}, + {1745929800000LL, 1827.99, 1829.25, 1822.25, 1829.25}, + {1745930700000LL, 1829.25, 1830.38, 1819.9, 1820.98}, + {1746007200000LL, 1805.62, 1806.65, 1802.9, 1805.85}, + {1746008100000LL, 1805.86, 1808.56, 1803.46, 1805.22}, + {1746009000000LL, 1805.22, 1807.53, 1802.92, 1807.52}, + {1746009900000LL, 1807.51, 1810, 1805.6, 1806.6}, + {1746010800000LL, 1806.59, 1809.5, 1806.59, 1808.5}, + {1746011700000LL, 1808.5, 1812.18, 1807.09, 1809.24}, + {1746012600000LL, 1809.25, 1816, 1808.03, 1813.54}, + {1746013500000LL, 1813.54, 1815.19, 1811.63, 1814.79}, + {1746014400000LL, 1814.79, 1815.48, 1807.54, 1809.6}, + {1746015300000LL, 1809.6, 1809.82, 1793, 1795.71}, + {1746016200000LL, 1795.71, 1799.35, 1774.3, 1776.52}, + {1746017100000LL, 1776.52, 1782.26, 1766, 1774.28}, +}; +// CME_MINI:ES1! 15 (point value 50): the registry feed +// 766e8149d7e16be49cd030f0993eeb339199e0fd4a97d7603625e6a3fe583471 (`lab bars CME_MINI:ES1! 15`), +// 2025-06 days 2 .. 5, 13:15 .. 17:00 UTC (16 bars each). +constexpr FeedBar kEsBars[] = { + {1748870100000LL, 5894.75, 5900.25, 5893, 5897.5}, + {1748871000000LL, 5897.5, 5917.75, 5897, 5912.75}, + {1748871900000LL, 5913.25, 5914.75, 5886, 5887}, + {1748872800000LL, 5887.25, 5892.5, 5870.5, 5889.5}, + {1748873700000LL, 5889.25, 5906.75, 5887.25, 5906.5}, + {1748874600000LL, 5906.25, 5916, 5901.5, 5906.25}, + {1748875500000LL, 5906.25, 5915.5, 5905.25, 5914}, + {1748876400000LL, 5914, 5916.75, 5905.25, 5913.5}, + {1748877300000LL, 5913.25, 5914.25, 5903.5, 5905.75}, + {1748878200000LL, 5906, 5911.75, 5901.5, 5911}, + {1748879100000LL, 5910.75, 5921.25, 5909.25, 5918.75}, + {1748880000000LL, 5918.75, 5920.25, 5912, 5917}, + {1748880900000LL, 5917, 5918, 5910.25, 5914.5}, + {1748881800000LL, 5914.25, 5918, 5910.75, 5912.25}, + {1748882700000LL, 5912, 5922.5, 5911.75, 5920.5}, + {1748883600000LL, 5920.75, 5927, 5920, 5921.25}, + {1748956500000LL, 5940.75, 5944.5, 5939.75, 5943.5}, + {1748957400000LL, 5943.5, 5952.75, 5940.5, 5950.25}, + {1748958300000LL, 5950.25, 5952.25, 5943.75, 5944.5}, + {1748959200000LL, 5944.5, 5953.5, 5938.25, 5949.5}, + {1748960100000LL, 5949.75, 5959.25, 5948.75, 5959}, + {1748961000000LL, 5959, 5959, 5951.75, 5957.25}, + {1748961900000LL, 5957.25, 5964.5, 5956.75, 5963.75}, + {1748962800000LL, 5964, 5968.75, 5963.75, 5967}, + {1748963700000LL, 5967, 5972, 5965, 5970.25}, + {1748964600000LL, 5970, 5975, 5968.75, 5973.5}, + {1748965500000LL, 5973.5, 5977.5, 5970.5, 5976.75}, + {1748966400000LL, 5977, 5977.5, 5972.75, 5977.25}, + {1748967300000LL, 5977.5, 5983, 5977.25, 5981.25}, + {1748968200000LL, 5981, 5987.25, 5980.5, 5984.75}, + {1748969100000LL, 5984.75, 5986.75, 5983, 5985.25}, + {1748970000000LL, 5985.5, 5988.75, 5982.75, 5988.5}, + {1749042900000LL, 5984.25, 5989.5, 5983, 5989.25}, + {1749043800000LL, 5989.25, 5998.75, 5987.5, 5993}, + {1749044700000LL, 5993, 5998, 5991.25, 5995}, + {1749045600000LL, 5994.75, 5994.75, 5974.25, 5987.5}, + {1749046500000LL, 5987.25, 5996.5, 5985.75, 5995.25}, + {1749047400000LL, 5995, 5997.25, 5987.25, 5989.25}, + {1749048300000LL, 5989.25, 5995, 5986.25, 5994.75}, + {1749049200000LL, 5994.5, 5998.25, 5992.75, 5994.75}, + {1749050100000LL, 5994.75, 5996.5, 5989.25, 5989.25}, + {1749051000000LL, 5989.25, 5993.5, 5983.25, 5985}, + {1749051900000LL, 5985, 5992.5, 5984.25, 5988}, + {1749052800000LL, 5988, 5989, 5977, 5977.75}, + {1749053700000LL, 5978, 5983.75, 5977.5, 5978.25}, + {1749054600000LL, 5978.5, 5984.25, 5978, 5984}, + {1749055500000LL, 5984, 5987.75, 5983.75, 5986.75}, + {1749056400000LL, 5986.75, 5993.25, 5986.75, 5991.75}, + {1749129300000LL, 5995, 5996.5, 5989.25, 5995}, + {1749130200000LL, 5995, 5997.75, 5970.75, 5977.25}, + {1749131100000LL, 5977, 5977.75, 5957.5, 5961.25}, + {1749132000000LL, 5961.5, 5971.25, 5960, 5965.25}, + {1749132900000LL, 5965, 5980.75, 5962.25, 5980}, + {1749133800000LL, 5980.25, 5993.75, 5958, 5984.25}, + {1749134700000LL, 5984, 5994.75, 5978, 5991.5}, + {1749135600000LL, 5991.5, 5995.5, 5989.25, 5994}, + {1749136500000LL, 5994, 6006.25, 5993.5, 6005.25}, + {1749137400000LL, 6005, 6006.5, 6001.75, 6004.75}, + {1749138300000LL, 6004.75, 6009, 6000.25, 6006.5}, + {1749139200000LL, 6006.25, 6008.25, 5998.5, 5999}, + {1749140100000LL, 5999, 6000, 5988.75, 5997.5}, + {1749141000000LL, 5997.5, 5998.5, 5987, 5987.5}, + {1749141900000LL, 5987.5, 5987.5, 5960.75, 5965.75}, + {1749142800000LL, 5966, 5979.5, 5961.25, 5979.5}, +}; diff --git a/tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/meta.json b/tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/meta.json new file mode 100644 index 00000000..bee1d2b1 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/meta.json @@ -0,0 +1,16 @@ +{ + "title": "H-THIN X15b FIFO exit fee basis", + "slug": "hthin-x15b-fifo-fee-basis", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T00:35:31.437Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01" + }, + "pine_sha256": "7e28fa1d2e147b693e535ffe068b9e7c3289147f2a049501fe71999d90ff4033", + "tv_trades_sha256": "f170f906d10112fbb6524fc51747c3b7e64e063651114449c7b8364b4ce9a075", + "tv_trades_rows": 22 +} diff --git a/tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/metrics.json b/tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/metrics.json new file mode 100644 index 00000000..da170fe8 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "hthin-x15b-fifo-fee-basis", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "deepBacktesting": true, + "trades": 11, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "7e28fa1d2e147b693e535ffe068b9e7c3289147f2a049501fe71999d90ff4033", + "tvTradesCsvHash": "f170f906d10112fbb6524fc51747c3b7e64e063651114449c7b8364b4ce9a075", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.75137", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "b3c0ce2fd014b51bf8c9bb3b72dbc7016557f50531ffbb44fba290fef5b39c71", + "decodedReportSha256": "5709e5df5f446ecff5f5be9e1deb5862d1c65484ef23b110494ed4e8c5b9d499", + "closedTrades": 11, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T00:35:31.435Z" +} diff --git a/tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/strategy.pine b/tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/strategy.pine new file mode 100644 index 00000000..82b82aa8 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/strategy.pine @@ -0,0 +1,49 @@ +// R5 lane H-THIN, X15(b): which open-entry fees does TradingView's +// percent-of-equity sizing basis keep after a FIFO strategy.exit(from_entry) +// closes an OLDER lot of another id? Cash per order 1000, 50 % of equity, +// pyramiding 5, FIFO; each scenario is flat at its start and closed by +// strategy.close_all. N* is the entry whose quantity reads the basis. +// A 2025-04-08: L2A qty 1, then LA (50 %), then XA from LA with qty = LA's +// units (position - 1): FIFO closes L2A and all of LA but 1 unit +// B 2025-04-10: the same with XB's default quantity +// C 2025-04-14: control, LC (50 %) is the older lot, then L2C qty 1 +//@version=6 +strategy("H-THIN X15b FIFO exit fee basis", overlay=true, initial_capital=100000, + default_qty_type=strategy.percent_of_equity, default_qty_value=50, pyramiding=5, + commission_type=strategy.commission.cash_per_order, commission_value=1000, + process_orders_on_close=false, margin_long=100, margin_short=100) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 4, 8, 10, 0) + strategy.entry("L2A", strategy.long, qty=1) +if at(2025, 4, 8, 10, 30) + strategy.entry("LA", strategy.long) +if at(2025, 4, 8, 11, 0) + strategy.exit("XA", "LA", qty=strategy.position_size - 1, limit=close * 0.9) +if at(2025, 4, 8, 11, 30) + strategy.entry("NA", strategy.long) +if at(2025, 4, 8, 12, 30) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 10, 10, 0) + strategy.entry("L2B", strategy.long, qty=1) +if at(2025, 4, 10, 10, 30) + strategy.entry("LB", strategy.long) +if at(2025, 4, 10, 11, 0) + strategy.exit("XB", "LB", limit=close * 0.9) +if at(2025, 4, 10, 11, 30) + strategy.entry("NB", strategy.long) +if at(2025, 4, 10, 12, 30) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 14, 10, 0) + strategy.entry("LC", strategy.long) +if at(2025, 4, 14, 10, 30) + strategy.entry("L2C", strategy.long, qty=1) +if at(2025, 4, 14, 11, 0) + strategy.exit("XC", "LC", limit=close * 0.9) +if at(2025, 4, 14, 11, 30) + strategy.entry("NC", strategy.long) +if at(2025, 4, 14, 12, 30) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/tv_trades.csv b/tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/tv_trades.csv new file mode 100644 index 00000000..589c7ce5 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/hthin-x15b-fifo-fee-basis/tv_trades.csv @@ -0,0 +1,23 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-08 19:15,XA,1567.13,1,1567.2,-1032.4202,-40.22,1032.35021044,0,0.00,-1003.04,-39.07,-2032.4202,-2.03,4 +1,Entry long,2025-04-08 18:15,L2A,1567.2,1,1567.2,-1032.4202,-40.22,1032.35021044,0,0.00,-1003.04,-39.07,-2032.4202,-2.03,4 +2,Exit long,2025-04-08 19:15,XA,1567.13,29.9117,46931.4573,-1991.2345,-4.16,1935.2995792,0,0.00,-1112.4224,-2.32,-3056.005,-3.06,2 +2,Entry long,2025-04-08 18:45,LA,1569,29.9117,46931.4573,-1991.2345,-4.16,1935.2995792,0,0.00,-1112.4224,-2.32,-3056.005,-3.06,2 +3,Exit long,2025-04-08 20:45,Close position order,1568.62,1,1569,-64.963005,-4.06,64.5830086,0,0.00,-37.190212,-2.32,-4088.6177,-4.09,8 +3,Entry long,2025-04-08 18:45,LA,1569,1,1569,-64.963005,-4.06,64.5830086,0,0.00,-37.190212,-2.32,-4088.6177,-4.09,8 +4,Exit long,2025-04-08 20:45,Close position order,1568.62,30.0243,47478.326319,-2349.376,-4.85,1967.7672018,0,0.00,-1430.2482,-2.95,-5437.9937,-5.44,4 +4,Entry long,2025-04-08 19:45,NA,1581.33,30.0243,47478.326319,-2349.376,-4.85,1967.7672018,0,0.00,-1430.2482,-2.95,-5437.9937,-5.44,4 +5,Exit long,2025-04-10 19:15,XB,1585.88,1,1601.17,-1050.0326,-40.37,1034.74260938,0,0.00,-1015.48,-39.04,-7488.0264,-7.49,4 +5,Entry long,2025-04-10 18:15,L2B,1601.17,1,1601.17,-1050.0326,-40.37,1034.74260938,0,0.00,-1015.48,-39.04,-7488.0264,-7.49,4 +6,Exit long,2025-04-10 19:15,XB,1585.88,27.7831,44185.130916,-2054.9832,-4.55,1930.5147812,0,0.00,-1095.0045,-2.43,-8577.752,-8.58,2 +6,Entry long,2025-04-10 18:45,LB,1590.36,27.7831,44185.130916,-2054.9832,-4.55,1930.5147812,0,0.00,-1095.0045,-2.43,-8577.752,-8.58,2 +7,Exit long,2025-04-10 20:45,Close position order,1598.24,1,1590.36,-61.20445,-3.77,69.08444804,2.5073907,0.15,-44.602608,-2.74,-9604.214,-9.60,8 +7,Entry long,2025-04-10 18:45,LB,1590.36,1,1590.36,-61.20445,-3.77,69.08444804,2.5073907,0.15,-44.602608,-2.74,-9604.214,-9.60,8 +8,Exit long,2025-04-10 20:45,Close position order,1598.24,28.119,44710.61595,-1735.3635,-3.80,1965.6581613,56.14964,0.12,-1111.0701,-2.43,-10339.577,-10.34,4 +8,Entry long,2025-04-10 19:45,NB,1590.05,28.119,44710.61595,-1735.3635,-3.80,1965.6581613,56.14964,0.12,-1111.0701,-2.43,-10339.577,-10.34,4 +9,Exit long,2025-04-14 19:15,XC,1685.54,26.6237,43830.59731,-955.286,-2.13,2000,126.71498,0.28,-1001.3312,-2.23,-12294.863,-12.29,4 +9,Entry long,2025-04-14 18:15,LC,1646.3,26.6237,43830.59731,-955.286,-2.13,2000,126.71498,0.28,-1001.3312,-2.23,-12294.863,-12.29,4 +10,Exit long,2025-04-14 20:45,Close position order,1671.77,1,1678.4,-1044.1854,-38.99,1037.55544122,0,0.00,-1013.26,-37.83,-13339.049,-13.34,8 +10,Entry long,2025-04-14 18:45,L2C,1678.4,1,1678.4,-1044.1854,-38.99,1037.55544122,0,0.00,-1013.26,-37.83,-13339.049,-13.34,8 +11,Exit long,2025-04-14 20:45,Close position order,1671.77,25.6273,42849.101873,-1968.5951,-4.49,1962.4445588,0,0.00,-1176.0596,-2.68,-14307.644,-14.31,4 +11,Entry long,2025-04-14 19:45,NC,1672.01,25.6273,42849.101873,-1968.5951,-4.49,1962.4445588,0,0.00,-1176.0596,-2.68,-14307.644,-14.31,4 \ No newline at end of file diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/meta.json b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/meta.json new file mode 100644 index 00000000..8353b244 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/meta.json @@ -0,0 +1,17 @@ +{ + "title": "PAR-CASHFEE pcf-contract-p100-m100", + "slug": "pcf-contract-p100-m100", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T03:40:03.378Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "timeoutMs": 120000 + }, + "pine_sha256": "ed5c722617b093eee40e596c55770d18e4f30a1300f53bc4c9bf449945ad63b4", + "tv_trades_sha256": "38cdc2cee4d123b0f540e2d4514249bae4a5f46be2247d45714652ba8dfe2077", + "tv_trades_rows": 18 +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/metrics.json b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/metrics.json new file mode 100644 index 00000000..8d0f9a31 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pcf-contract-p100-m100", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "deepBacktesting": true, + "trades": 9, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "ed5c722617b093eee40e596c55770d18e4f30a1300f53bc4c9bf449945ad63b4", + "tvTradesCsvHash": "38cdc2cee4d123b0f540e2d4514249bae4a5f46be2247d45714652ba8dfe2077", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.81285", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "7d6cfd09c40948e60609211ca953fba4e96d59ffd14f7a6de0b40bf6dbc2accb", + "decodedReportSha256": "47dc2088ce5555232677abdf1e3ce9f9bc8993ecfc581947118f45e58329592b", + "closedTrades": 9, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T03:40:03.377Z" +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/strategy.pine b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/strategy.pine new file mode 100644 index 00000000..d4543f2a --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/strategy.pine @@ -0,0 +1,43 @@ +// R5 lane PAR-CASHFEE: TradingView's percent-of-equity default sizing under +// a cash commission (cash_per_contract 20), default_qty_value 100, margin 100/100, +// pyramiding 1. Each scenario is flat at its start (its own UTC day) and is +// closed by strategy.close_all; orders fill at the next bar's open. +// P 2025-04-02: flat long, next open below the signal close +// Q 2025-04-03: flat long, next open above the signal close +// R 2025-04-04: flat short +// T 2025-04-10: long T1, then a default short T2 reverses it +// U 2025-04-12: flat long, next open equal to the signal close +//@version=6 +strategy("PAR-CASHFEE pcf-contract-p100-m100", overlay=true, initial_capital=100000, + default_qty_type=strategy.percent_of_equity, default_qty_value=100, pyramiding=1, + commission_type=strategy.commission.cash_per_contract, commission_value=20, + process_orders_on_close=false, margin_long=100, margin_short=100) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 4, 2, 10, 0) + strategy.entry("P1", strategy.long) +if at(2025, 4, 2, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 3, 10, 0) + strategy.entry("Q1", strategy.long) +if at(2025, 4, 3, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 4, 10, 0) + strategy.entry("R1", strategy.short) +if at(2025, 4, 4, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 10, 10, 0) + strategy.entry("T1", strategy.long) +if at(2025, 4, 10, 10, 30) + strategy.entry("T2", strategy.short) +if at(2025, 4, 10, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 12, 10, 0) + strategy.entry("U1", strategy.long) +if at(2025, 4, 12, 12, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/tv_trades.csv b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/tv_trades.csv new file mode 100644 index 00000000..c5d4884f --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m100/tv_trades.csv @@ -0,0 +1,19 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-02 20:15,Close position order,1865.26,52.6307,98946.768614,-2882.0571,-2.88,2105.228,0,0.00,-2131.017,-2.13,-2882.0571,-2.88,8 +1,Entry long,2025-04-02 18:15,P1,1880.02,52.6307,98946.768614,-2882.0571,-2.88,2105.228,0,0.00,-2131.017,-2.13,-2882.0571,-2.88,8 +2,Exit long,2025-04-03 18:15,Margin call,1811.58,0.0008,1.449264,-0.032,-2.18,0.032,0,0.00,-0.016,-1.09,-3942.561,-3.94,0 +2,Entry long,2025-04-03 18:15,Q1,1811.58,0.0008,1.449264,-0.032,-2.18,0.032,0,0.00,-0.016,-1.09,-3942.561,-3.94,0 +3,Exit long,2025-04-03 20:15,Close position order,1797.78,53.0236,96056.493288,-2852.6697,-2.94,2120.944,0,0.00,-2409.3923,-2.48,-5734.759,-5.73,8 +3,Entry long,2025-04-03 18:15,Q1,1811.58,53.0236,96056.493288,-2852.6697,-2.94,2120.944,0,0.00,-2409.3923,-2.48,-5734.759,-5.73,8 +4,Exit short,2025-04-04 18:15,Margin call,1814.76,0.4676,847.613844,-19.671932,-2.30,18.704,0,0.00,-10.319932,-1.20,-6773.781,-6.77,0 +4,Entry short,2025-04-04 18:15,R1,1812.69,0.4676,847.613844,-19.671932,-2.30,18.704,0,0.00,-10.319932,-1.20,-6773.781,-6.77,0 +5,Exit short,2025-04-04 20:15,Close position order,1780.11,50.9675,92388.277575,-378.17886,-0.40,2038.7,1792.527,1.92,-1124.8528,-1.20,-6132.6094,-6.13,8 +5,Entry short,2025-04-04 18:15,R1,1812.69,50.9675,92388.277575,-378.17886,-0.40,2038.7,1792.527,1.92,-1124.8528,-1.20,-6132.6094,-6.13,8 +6,Exit long,2025-04-10 18:45,T2,1590.36,57.901,92709.34417,-2941.9497,-3.13,2316.04,0,0.00,-1862.0962,-1.98,-9074.56,-9.07,2 +6,Entry long,2025-04-10 18:15,T1,1601.17,57.901,92709.34417,-2941.9497,-3.13,2316.04,0,0.00,-1862.0962,-1.98,-9074.56,-9.07,2 +7,Exit short,2025-04-10 18:45,Margin call,1590.36,2.9124,4631.764464,-116.496,-2.48,116.496,0,0.00,-58.248,-1.24,-10276.445,-10.28,0 +7,Entry short,2025-04-10 18:45,T2,1590.36,2.9124,4631.764464,-116.496,-2.48,116.496,0,0.00,-58.248,-1.24,-10276.445,-10.28,0 +8,Exit short,2025-04-10 20:15,Close position order,1595.9,54.2695,86308.04202,-2471.433,-2.83,2170.78,0,0.00,-2755.8052,-3.15,-11662.488,-11.66,6 +8,Entry short,2025-04-10 18:45,T2,1590.36,54.2695,86308.04202,-2471.433,-2.83,2170.78,0,0.00,-2755.8052,-3.15,-11662.488,-11.66,6 +9,Exit long,2025-04-12 20:15,Close position order,1596,54.7778,87241.86317,-2007.6063,-2.27,2191.112,0,0.00,-1416.0061,-1.60,-13670.095,-13.67,8 +9,Entry long,2025-04-12 18:15,U1,1592.65,54.7778,87241.86317,-2007.6063,-2.27,2191.112,0,0.00,-1416.0061,-1.60,-13670.095,-13.67,8 \ No newline at end of file diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/meta.json b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/meta.json new file mode 100644 index 00000000..3fd166f5 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/meta.json @@ -0,0 +1,17 @@ +{ + "title": "PAR-CASHFEE pcf-contract-p100-m25", + "slug": "pcf-contract-p100-m25", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T03:39:55.765Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "timeoutMs": 120000 + }, + "pine_sha256": "91438975b38a435937152d1f165cfa547f70fa402e7a65ccc00cee67ac362556", + "tv_trades_sha256": "e713dda381f35e43a88506821004aa82d2afccd2e39a69dd2746100083accba3", + "tv_trades_rows": 30 +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/metrics.json b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/metrics.json new file mode 100644 index 00000000..364bf36d --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pcf-contract-p100-m25", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "deepBacktesting": true, + "trades": 15, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "91438975b38a435937152d1f165cfa547f70fa402e7a65ccc00cee67ac362556", + "tvTradesCsvHash": "e713dda381f35e43a88506821004aa82d2afccd2e39a69dd2746100083accba3", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.81280", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "adacdc71ec9a58d6b5f77491c26a7b04b5b21109ceafb4448d49b5da6605d450", + "decodedReportSha256": "779d52a2556f479e09977b3254cc58035d693ec51db66e8818b0b7a0fb2aa6ee", + "closedTrades": 15, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T03:39:55.764Z" +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/strategy.pine b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/strategy.pine new file mode 100644 index 00000000..06d432e7 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/strategy.pine @@ -0,0 +1,69 @@ +// R5 lane PAR-CASHFEE: TradingView's percent-of-equity default sizing under +// a cash commission (cash_per_contract 20), default_qty_value 100, margin 25/25, +// pyramiding 3. Each scenario is flat at its start (its own UTC day) and is +// closed by strategy.close_all; orders fill at the next bar's open. +// A 2025-04-02: flat long L1, then adds L2 and L3 while the earlier lots are open +// B 2025-04-04: flat short S1, then adds S2 and S3 while the earlier lots are open +// C 2025-04-08: long C1, strategy.close(C1, qty_percent=50), then add C2 on the half lot +// D 2025-04-10: long D1, then a default short D2 reverses it +// E 2025-04-15: short E1, then a default long E2 reverses it +// F 2025-04-17: flat long F1 alone +// G 2025-04-22: flat short G1 alone +//@version=6 +strategy("PAR-CASHFEE pcf-contract-p100-m25", overlay=true, initial_capital=100000, + default_qty_type=strategy.percent_of_equity, default_qty_value=100, pyramiding=3, + commission_type=strategy.commission.cash_per_contract, commission_value=20, + process_orders_on_close=false, margin_long=25, margin_short=25) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 4, 2, 10, 0) + strategy.entry("A1", strategy.long) +if at(2025, 4, 2, 10, 30) + strategy.entry("A2", strategy.long) +if at(2025, 4, 2, 11, 0) + strategy.entry("A3", strategy.long) +if at(2025, 4, 2, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 4, 10, 0) + strategy.entry("B1", strategy.short) +if at(2025, 4, 4, 10, 30) + strategy.entry("B2", strategy.short) +if at(2025, 4, 4, 11, 0) + strategy.entry("B3", strategy.short) +if at(2025, 4, 4, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 8, 10, 0) + strategy.entry("C1", strategy.long) +if at(2025, 4, 8, 10, 30) + strategy.close("C1", qty_percent=50) +if at(2025, 4, 8, 11, 0) + strategy.entry("C2", strategy.long) +if at(2025, 4, 8, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 10, 10, 0) + strategy.entry("D1", strategy.long) +if at(2025, 4, 10, 10, 30) + strategy.entry("D2", strategy.short) +if at(2025, 4, 10, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 15, 10, 0) + strategy.entry("E1", strategy.short) +if at(2025, 4, 15, 10, 30) + strategy.entry("E2", strategy.long) +if at(2025, 4, 15, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 17, 10, 0) + strategy.entry("F1", strategy.long) +if at(2025, 4, 17, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 22, 10, 0) + strategy.entry("G1", strategy.short) +if at(2025, 4, 22, 12, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/tv_trades.csv b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/tv_trades.csv new file mode 100644 index 00000000..9224c1c8 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p100-m25/tv_trades.csv @@ -0,0 +1,31 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-02 20:15,Close position order,1865.26,52.6307,98946.768614,-2882.0571,-2.88,2105.228,0,0.00,-2131.017,-2.13,-4954.145,-4.95,8 +1,Entry long,2025-04-02 18:15,A1,1880.02,52.6307,98946.768614,-2882.0571,-2.88,2105.228,0,0.00,-2131.017,-2.13,-4954.145,-4.95,8 +2,Exit long,2025-04-02 20:15,Close position order,1865.26,52.0754,97613.775038,-2562.6304,-2.60,2083.016,0,0.00,-1819.5145,-1.84,-6475.2676,-6.48,6 +2,Entry long,2025-04-02 18:45,A2,1874.47,52.0754,97613.775038,-2562.6304,-2.60,2083.016,0,0.00,-1819.5145,-1.84,-6475.2676,-6.48,6 +3,Exit long,2025-04-02 20:15,Close position order,1865.26,51.529,96595.74811,-2541.9255,-2.60,2061.16,0,0.00,-1806.6067,-1.85,-7986.6133,-7.99,4 +3,Entry long,2025-04-02 19:15,A3,1874.59,51.529,96595.74811,-2541.9255,-2.60,2061.16,0,0.00,-1806.6067,-1.85,-7986.6133,-7.99,4 +4,Exit short,2025-04-04 20:15,Close position order,1780.11,50.2064,91008.639216,-372.5315,-0.40,2008.256,1765.759,1.92,-1108.0553,-1.20,-10403.95,-10.40,8 +4,Entry short,2025-04-04 18:15,B1,1812.69,50.2064,91008.639216,-372.5315,-0.40,2008.256,1765.759,1.92,-1108.0553,-1.20,-10403.95,-10.40,8 +5,Exit short,2025-04-04 20:15,Close position order,1780.11,51.0225,91228.23,-1638.3325,-1.78,2040.9,534.7158,0.58,-1256.174,-1.36,-11021.833,-11.02,6 +5,Entry short,2025-04-04 18:45,B2,1788,51.0225,91228.23,-1638.3325,-1.78,2040.9,534.7158,0.58,-1256.174,-1.36,-11021.833,-11.02,6 +6,Exit short,2025-04-04 20:15,Close position order,1780.11,51.2178,91116.4662,-2105.5637,-2.29,2048.712,75.802345,0.08,-1324.4923,-1.44,-12103.041,-12.10,4 +6,Entry short,2025-04-04 19:15,B3,1779,51.2178,91116.4662,-2105.5637,-2.29,2048.712,75.802345,0.08,-1324.4923,-1.44,-12103.041,-12.10,4 +7,Exit long,2025-04-08 18:45,Close entry(s) order C1,1569,27.6893,43394.67096,-1057.7312,-2.41,1107.572,0,0.00,-576.21436,-1.31,-13714.559,-13.71,2 +7,Entry long,2025-04-08 18:15,C1,1567.2,27.6893,43394.67096,-1057.7312,-2.41,1107.572,0,0.00,-576.21436,-1.31,-13714.559,-13.71,2 +8,Exit long,2025-04-08 20:15,Close position order,1584.27,27.6893,43394.67096,-634.91565,-1.44,1107.572,154.78319,0.35,-637.9615,-1.45,-14882.9795,-14.88,8 +8,Entry long,2025-04-08 18:15,C1,1567.2,27.6893,43394.67096,-634.91565,-1.44,1107.572,154.78319,0.35,-637.9615,-1.45,-14882.9795,-14.88,8 +9,Exit long,2025-04-08 20:15,Close position order,1584.27,54.3646,85196.395598,-1242.7748,-1.44,2174.584,307.70364,0.36,-1230.2709,-1.43,-15038.463,-15.04,4 +9,Entry long,2025-04-08 19:15,C2,1567.13,54.3646,85196.395598,-1242.7748,-1.44,2174.584,307.70364,0.36,-1230.2709,-1.43,-15038.463,-15.04,4 +10,Exit long,2025-04-10 18:45,D2,1590.36,52.4075,83913.316775,-2662.8252,-3.13,2096.3,0,0.00,-1685.4252,-1.98,-17701.287,-17.70,2 +10,Entry long,2025-04-10 18:15,D1,1601.17,52.4075,83913.316775,-2662.8252,-3.13,2096.3,0,0.00,-1685.4252,-1.98,-17701.287,-17.70,2 +11,Exit short,2025-04-10 20:15,Close position order,1595.9,51.7566,82311.626376,-2356.9956,-2.83,2070.264,0,0.00,-2628.2002,-3.15,-20058.283,-20.06,6 +11,Entry short,2025-04-10 18:45,D2,1590.36,51.7566,82311.626376,-2356.9956,-2.83,2070.264,0,0.00,-2628.2002,-3.15,-20058.283,-20.06,6 +12,Exit short,2025-04-15 18:45,E2,1628.64,48.0843,78979.424436,-1255.9619,-1.57,1923.372,0,0.00,-985.2473,-1.23,-21314.244,-21.31,2 +12,Entry short,2025-04-15 18:15,E1,1642.52,48.0843,78979.424436,-1255.9619,-1.57,1923.372,0,0.00,-985.2473,-1.23,-21314.244,-21.31,2 +13,Exit long,2025-04-15 20:15,Close position order,1635.17,48.3115,78682.04136,-1616.986,-2.03,1932.46,0,0.00,-1319.8702,-1.66,-22931.23,-22.93,6 +13,Entry long,2025-04-15 18:45,E2,1628.64,48.3115,78682.04136,-1616.986,-2.03,1932.46,0,0.00,-1319.8702,-1.66,-22931.23,-22.93,6 +14,Exit long,2025-04-17 20:15,Close position order,1594.77,47.8994,76111.188612,-1638.6384,-2.13,1915.976,0,0.00,-1019.7782,-1.32,-24569.87,-24.57,8 +14,Entry long,2025-04-17 18:15,F1,1588.98,47.8994,76111.188612,-1638.6384,-2.13,1915.976,0,0.00,-1019.7782,-1.32,-24569.87,-24.57,8 +15,Exit short,2025-04-22 20:15,Close position order,1631.46,45.8982,74512.055844,-2204.9495,-2.92,1835.928,0,0.00,-1587.1598,-2.10,-26774.818,-26.77,8 +15,Entry short,2025-04-22 18:15,G1,1623.42,45.8982,74512.055844,-2204.9495,-2.92,1835.928,0,0.00,-1587.1598,-2.10,-26774.818,-26.77,8 \ No newline at end of file diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/meta.json b/tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/meta.json new file mode 100644 index 00000000..31c0b6f4 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/meta.json @@ -0,0 +1,17 @@ +{ + "title": "PAR-CASHFEE pcf-contract-p200-m25", + "slug": "pcf-contract-p200-m25", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T03:46:57.141Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "timeoutMs": 120000 + }, + "pine_sha256": "b5c09b080518a89e68c523cab10f5958774e84f8cf961fcf227c026e16acfb91", + "tv_trades_sha256": "fcf3e66efb18a028b94333fadcb89290888216c56b507b403b12423f7c2f561e", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/metrics.json b/tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/metrics.json new file mode 100644 index 00000000..ee84d322 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pcf-contract-p200-m25", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "b5c09b080518a89e68c523cab10f5958774e84f8cf961fcf227c026e16acfb91", + "tvTradesCsvHash": "fcf3e66efb18a028b94333fadcb89290888216c56b507b403b12423f7c2f561e", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.81532", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "1e1768c3eb16a51ae56d132b6ff1cb1d35494355c25776143740e0565a831290", + "decodedReportSha256": "936ce46a211248586d839b913704793c6a3a73a36aa22f783210a86f8dd88ac4", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T03:46:57.140Z" +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/strategy.pine b/tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/strategy.pine new file mode 100644 index 00000000..a2820600 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/strategy.pine @@ -0,0 +1,39 @@ +// R5 lane PAR-CASHFEE: TradingView's percent-of-equity default sizing under +// a cash commission (cash_per_contract 20), default_qty_value 200, margin 25/25, +// pyramiding 1, initial capital 100000. Each scenario is flat at its start (its own +// UTC day) and is closed by strategy.close_all; orders fill at the next bar's open. +// F 2025-04-17: flat long F1 alone +// G 2025-04-22: flat short G1 alone +// D 2025-04-10: long D1, then a default short D2 reverses it +// E 2025-04-15: short E1, then a default long E2 reverses it +//@version=6 +strategy("PAR-CASHFEE pcf-contract-p200-m25", overlay=true, initial_capital=100000, + default_qty_type=strategy.percent_of_equity, default_qty_value=200, pyramiding=1, + commission_type=strategy.commission.cash_per_contract, commission_value=20, + process_orders_on_close=false, margin_long=25, margin_short=25) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 4, 17, 10, 0) + strategy.entry("F1", strategy.long) +if at(2025, 4, 17, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 22, 10, 0) + strategy.entry("G1", strategy.short) +if at(2025, 4, 22, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 10, 10, 0) + strategy.entry("D1", strategy.long) +if at(2025, 4, 10, 10, 30) + strategy.entry("D2", strategy.short) +if at(2025, 4, 10, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 15, 10, 0) + strategy.entry("E1", strategy.short) +if at(2025, 4, 15, 10, 30) + strategy.entry("E2", strategy.long) +if at(2025, 4, 15, 12, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/tv_trades.csv b/tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/tv_trades.csv new file mode 100644 index 00000000..b3e13a0a --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p200-m25/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-10 18:45,D2,1590.36,123.3676,197532.500092,-6268.3076,-3.13,4934.704,0,0.00,-3967.502,-1.98,-6268.3076,-6.27,2 +1,Entry long,2025-04-10 18:15,D1,1601.17,123.3676,197532.500092,-6268.3076,-3.13,4934.704,0,0.00,-3967.502,-1.98,-6268.3076,-6.27,2 +2,Exit short,2025-04-10 20:15,Close position order,1595.9,119.4752,190008.579072,-5440.9004,-2.83,4779.008,0,0.00,-6066.9507,-3.15,-11709.208,-11.71,6 +2,Entry short,2025-04-10 18:45,D2,1590.36,119.4752,190008.579072,-5440.9004,-2.83,4779.008,0,0.00,-6066.9507,-3.15,-11709.208,-11.71,6 +3,Exit short,2025-04-15 18:45,E2,1628.64,106.2125,174456.1555,-2774.2705,-1.57,4248.5,0,0.00,-2176.2942,-1.23,-14483.479,-14.48,2 +3,Entry short,2025-04-15 18:15,E1,1642.52,106.2125,174456.1555,-2774.2705,-1.57,4248.5,0,0.00,-2176.2942,-1.23,-14483.479,-14.48,2 +4,Exit long,2025-04-15 20:15,Close position order,1635.17,106.3208,173158.307712,-3558.5571,-2.03,4252.832,0,0.00,-2904.6843,-1.66,-18042.035,-18.04,6 +4,Entry long,2025-04-15 18:45,E2,1628.64,106.3208,173158.307712,-3558.5571,-2.03,4252.832,0,0.00,-2904.6843,-1.66,-18042.035,-18.04,6 +5,Exit long,2025-04-17 20:15,Close position order,1594.77,101.8763,161879.403174,-3485.1882,-2.13,4075.052,0,0.00,-2168.9465,-1.32,-21527.225,-21.53,8 +5,Entry long,2025-04-17 18:15,F1,1588.98,101.8763,161879.403174,-3485.1882,-2.13,4075.052,0,0.00,-2168.9465,-1.32,-21527.225,-21.53,8 +6,Exit short,2025-04-22 20:15,Close position order,1631.46,95.4993,155035.473606,-4587.786,-2.92,3819.972,0,0.00,-3302.3657,-2.10,-26115.01,-26.12,8 +6,Entry short,2025-04-22 18:15,G1,1623.42,95.4993,155035.473606,-4587.786,-2.92,3819.972,0,0.00,-3302.3657,-2.10,-26115.01,-26.12,8 \ No newline at end of file diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/meta.json b/tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/meta.json new file mode 100644 index 00000000..eb1e2d3b --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/meta.json @@ -0,0 +1,17 @@ +{ + "title": "PAR-CASHFEE pcf-contract-p50-m25", + "slug": "pcf-contract-p50-m25", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T03:39:52.463Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "timeoutMs": 120000 + }, + "pine_sha256": "f687d6343534356670b6d296e1881e2a1acaf4e30eaf85386cd0e422c5784535", + "tv_trades_sha256": "4c357f02aa06ceb6577a8ba6ba601f73dddb771536401989765cd5a208256852", + "tv_trades_rows": 30 +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/metrics.json b/tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/metrics.json new file mode 100644 index 00000000..f7182592 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pcf-contract-p50-m25", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "deepBacktesting": true, + "trades": 15, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "f687d6343534356670b6d296e1881e2a1acaf4e30eaf85386cd0e422c5784535", + "tvTradesCsvHash": "4c357f02aa06ceb6577a8ba6ba601f73dddb771536401989765cd5a208256852", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.81277", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "17b56b93d08a7bab692fabe8a68301f028cd53a8667c38ec67c5c1252fd42b97", + "decodedReportSha256": "4a7601909151fc66d3f557c84ea5f21f3912c5c89368df29dcbb7c350f692c39", + "closedTrades": 15, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T03:39:52.462Z" +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/strategy.pine b/tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/strategy.pine new file mode 100644 index 00000000..5733badc --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/strategy.pine @@ -0,0 +1,69 @@ +// R5 lane PAR-CASHFEE: TradingView's percent-of-equity default sizing under +// a cash commission (cash_per_contract 20), default_qty_value 50, margin 25/25, +// pyramiding 3. Each scenario is flat at its start (its own UTC day) and is +// closed by strategy.close_all; orders fill at the next bar's open. +// A 2025-04-02: flat long L1, then adds L2 and L3 while the earlier lots are open +// B 2025-04-04: flat short S1, then adds S2 and S3 while the earlier lots are open +// C 2025-04-08: long C1, strategy.close(C1, qty_percent=50), then add C2 on the half lot +// D 2025-04-10: long D1, then a default short D2 reverses it +// E 2025-04-15: short E1, then a default long E2 reverses it +// F 2025-04-17: flat long F1 alone +// G 2025-04-22: flat short G1 alone +//@version=6 +strategy("PAR-CASHFEE pcf-contract-p50-m25", overlay=true, initial_capital=100000, + default_qty_type=strategy.percent_of_equity, default_qty_value=50, pyramiding=3, + commission_type=strategy.commission.cash_per_contract, commission_value=20, + process_orders_on_close=false, margin_long=25, margin_short=25) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 4, 2, 10, 0) + strategy.entry("A1", strategy.long) +if at(2025, 4, 2, 10, 30) + strategy.entry("A2", strategy.long) +if at(2025, 4, 2, 11, 0) + strategy.entry("A3", strategy.long) +if at(2025, 4, 2, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 4, 10, 0) + strategy.entry("B1", strategy.short) +if at(2025, 4, 4, 10, 30) + strategy.entry("B2", strategy.short) +if at(2025, 4, 4, 11, 0) + strategy.entry("B3", strategy.short) +if at(2025, 4, 4, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 8, 10, 0) + strategy.entry("C1", strategy.long) +if at(2025, 4, 8, 10, 30) + strategy.close("C1", qty_percent=50) +if at(2025, 4, 8, 11, 0) + strategy.entry("C2", strategy.long) +if at(2025, 4, 8, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 10, 10, 0) + strategy.entry("D1", strategy.long) +if at(2025, 4, 10, 10, 30) + strategy.entry("D2", strategy.short) +if at(2025, 4, 10, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 15, 10, 0) + strategy.entry("E1", strategy.short) +if at(2025, 4, 15, 10, 30) + strategy.entry("E2", strategy.long) +if at(2025, 4, 15, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 17, 10, 0) + strategy.entry("F1", strategy.long) +if at(2025, 4, 17, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 22, 10, 0) + strategy.entry("G1", strategy.short) +if at(2025, 4, 22, 12, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/tv_trades.csv b/tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/tv_trades.csv new file mode 100644 index 00000000..25789194 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-p50-m25/tv_trades.csv @@ -0,0 +1,31 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-02 20:15,Close position order,1865.26,26.3153,49473.290306,-1441.0259,-2.88,1052.612,0,0.00,-1065.5065,-2.13,-2486.8599,-2.49,8 +1,Entry long,2025-04-02 18:15,A1,1880.02,26.3153,49473.290306,-1441.0259,-2.88,1052.612,0,0.00,-1065.5065,-2.13,-2486.8599,-2.49,8 +2,Exit long,2025-04-02 20:15,Close position order,1865.26,26.215,49139.23105,-1290.0402,-2.60,1048.6,0,0.00,-915.9521,-1.84,-3252.6,-3.25,6 +2,Entry long,2025-04-02 18:45,A2,1874.47,26.215,49139.23105,-1290.0402,-2.60,1048.6,0,0.00,-915.9521,-1.84,-3252.6,-3.25,6 +3,Exit long,2025-04-02 20:15,Close position order,1865.26,26.0767,48883.121053,-1286.3636,-2.60,1043.068,0,0.00,-914.2491,-1.85,-4017.4297,-4.02,4 +3,Entry long,2025-04-02 19:15,A3,1874.59,26.0767,48883.121053,-1286.3636,-2.60,1043.068,0,0.00,-914.2491,-1.85,-4017.4297,-4.02,4 +4,Exit short,2025-04-04 20:15,Close position order,1780.11,26.1861,47467.281609,-194.30086,-0.40,1047.444,920.96515,1.92,-577.92725,-1.20,-5277.1924,-5.28,8 +4,Entry short,2025-04-04 18:15,B1,1812.69,26.1861,47467.281609,-194.30086,-0.40,1047.444,920.96515,1.92,-577.92725,-1.20,-5277.1924,-5.28,8 +5,Exit short,2025-04-04 20:15,Close position order,1780.11,26.5778,47521.1064,-853.41315,-1.78,1063.112,278.53534,0.58,-654.34546,-1.36,-5599.05,-5.60,6 +5,Entry short,2025-04-04 18:45,B2,1788,26.5778,47521.1064,-853.41315,-1.78,1063.112,278.53534,0.58,-654.34546,-1.36,-5599.05,-5.60,6 +6,Exit short,2025-04-04 20:15,Close position order,1780.11,26.6953,47490.9387,-1097.4437,-2.29,1067.812,39.509045,0.08,-690.34045,-1.44,-6162.5874,-6.16,4 +6,Entry short,2025-04-04 19:15,B3,1779,26.6953,47490.9387,-1097.4437,-2.29,1067.812,39.509045,0.08,-690.34045,-1.44,-6162.5874,-6.16,4 +7,Exit long,2025-04-08 18:45,Close entry(s) order C1,1569,14.7803,23163.68616,-564.6075,-2.41,591.212,0,0.00,-307.57803,-1.31,-7022.801,-7.02,2 +7,Entry long,2025-04-08 18:15,C1,1567.2,14.7803,23163.68616,-564.6075,-2.41,591.212,0,0.00,-307.57803,-1.31,-7022.801,-7.02,2 +8,Exit long,2025-04-08 20:15,Close position order,1584.27,14.7803,23163.68616,-338.9123,-1.44,591.212,82.62188,0.35,-340.53812,-1.45,-7651.921,-7.65,8 +8,Entry long,2025-04-08 18:15,C1,1567.2,14.7803,23163.68616,-338.9123,-1.44,591.212,82.62188,0.35,-340.53812,-1.45,-7651.921,-7.65,8 +9,Exit long,2025-04-08 20:15,Close position order,1584.27,29.2907,45902.334691,-669.5854,-1.44,1171.628,165.78535,0.36,-662.8485,-1.43,-7735.6924,-7.74,4 +9,Entry long,2025-04-08 19:15,C2,1567.13,29.2907,45902.334691,-669.5854,-1.44,1171.628,165.78535,0.36,-662.8485,-1.43,-7735.6924,-7.74,4 +10,Exit long,2025-04-10 18:45,D2,1590.36,28.456,45562.89352,-1445.8494,-3.13,1138.24,0,0.00,-915.14496,-1.98,-9181.542,-9.18,2 +10,Entry long,2025-04-10 18:15,D1,1601.17,28.456,45562.89352,-1445.8494,-3.13,1138.24,0,0.00,-915.14496,-1.98,-9181.542,-9.18,2 +11,Exit short,2025-04-10 20:15,Close position order,1595.9,28.3748,45126.146928,-1292.1884,-2.83,1134.992,0,0.00,-1440.8723,-3.15,-10473.73,-10.47,6 +11,Entry short,2025-04-10 18:45,D2,1590.36,28.3748,45126.146928,-1292.1884,-2.83,1134.992,0,0.00,-1440.8723,-3.15,-10473.73,-10.47,6 +12,Exit short,2025-04-15 18:45,E2,1628.64,26.9247,44224.358244,-703.2732,-1.57,1076.988,0,0.00,-551.6871,-1.23,-11177.004,-11.18,2 +12,Entry short,2025-04-15 18:15,E1,1642.52,26.9247,44224.358244,-703.2732,-1.57,1076.988,0,0.00,-551.6871,-1.23,-11177.004,-11.18,2 +13,Exit long,2025-04-15 20:15,Close position order,1635.17,27.1018,44139.075552,-907.0972,-2.03,1084.072,0,0.00,-740.4212,-1.66,-12084.101,-12.08,6 +13,Entry long,2025-04-15 18:45,E2,1628.64,27.1018,44139.075552,-907.0972,-2.03,1084.072,0,0.00,-740.4212,-1.66,-12084.101,-12.08,6 +14,Exit long,2025-04-17 20:15,Close position order,1594.77,27.3205,43411.72809,-934.6343,-2.13,1092.82,0,0.00,-581.65344,-1.32,-13018.735,-13.02,8 +14,Entry long,2025-04-17 18:15,F1,1588.98,27.3205,43411.72809,-934.6343,-2.13,1092.82,0,0.00,-581.65344,-1.32,-13018.735,-13.02,8 +15,Exit short,2025-04-22 20:15,Close position order,1631.46,26.4634,42961.212828,-1271.3018,-2.92,1058.536,0,0.00,-915.1044,-2.10,-14290.037,-14.29,8 +15,Entry short,2025-04-22 18:15,G1,1623.42,26.4634,42961.212828,-1271.3018,-2.92,1058.536,0,0.00,-915.1044,-2.10,-14290.037,-14.29,8 \ No newline at end of file diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/meta.json b/tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/meta.json new file mode 100644 index 00000000..78e8b453 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/meta.json @@ -0,0 +1,17 @@ +{ + "title": "PAR-CASHFEE pcf-contract-priced-m25", + "slug": "pcf-contract-priced-m25", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T04:05:05.561Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "timeoutMs": 120000 + }, + "pine_sha256": "9227d8c491de4c72a31845fd3e736fcecc5f8f408bbdb157975961c39b7ebce8", + "tv_trades_sha256": "c9b9c711279b8e20a3abfa34b6a9ef968e8c8c6ff776eb03db3e30b99f91d9ec", + "tv_trades_rows": 14 +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/metrics.json b/tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/metrics.json new file mode 100644 index 00000000..4a8d6076 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pcf-contract-priced-m25", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "deepBacktesting": true, + "trades": 7, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "9227d8c491de4c72a31845fd3e736fcecc5f8f408bbdb157975961c39b7ebce8", + "tvTradesCsvHash": "c9b9c711279b8e20a3abfa34b6a9ef968e8c8c6ff776eb03db3e30b99f91d9ec", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.82091", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "ee9fc7c0f9475724f69d3e92fba2408c2b6efdcda2aecec9b494843022f33a6d", + "decodedReportSha256": "4762526586880ab57525b43c03aacb3d4110bd1525122cc49230764b89af8629", + "closedTrades": 7, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T04:05:05.560Z" +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/strategy.pine b/tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/strategy.pine new file mode 100644 index 00000000..00d35220 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/strategy.pine @@ -0,0 +1,50 @@ +// R5 lane PAR-CASHFEE: TradingView's percent-of-equity default sizing under +// a cash commission (cash_per_contract 20) for PRICED default entries (limit, stop) and +// for strategy.order; default_qty_value 50, margin 25/25, pyramiding 1. Each +// scenario is flat at its start (its own UTC day) and is closed by +// strategy.close_all. +// O 2025-04-23: strategy.order long O1 from flat, then strategy.order long O2 while O1 is open +// M 2025-04-24: limit short M1 at the signal close + 3 +// N 2025-04-25: stop long N1 at the signal close + 3 +// V 2025-04-28: stop short V1 at the signal close - 3 +// K 2025-04-29: limit long K1 at the signal close - 3 +// W 2025-04-30: strategy.order short W1 from flat +//@version=6 +strategy("PAR-CASHFEE pcf-contract-priced-m25", overlay=true, initial_capital=100000, + default_qty_type=strategy.percent_of_equity, default_qty_value=50, pyramiding=1, + commission_type=strategy.commission.cash_per_contract, commission_value=20, + process_orders_on_close=false, margin_long=25, margin_short=25) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 4, 23, 10, 0) + strategy.order("O1", strategy.long) +if at(2025, 4, 23, 10, 30) + strategy.order("O2", strategy.long) +if at(2025, 4, 23, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 24, 10, 0) + strategy.entry("M1", strategy.short, limit=close + 3) +if at(2025, 4, 24, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 25, 10, 0) + strategy.entry("N1", strategy.long, stop=close + 3) +if at(2025, 4, 25, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 28, 10, 0) + strategy.entry("V1", strategy.short, stop=close - 3) +if at(2025, 4, 28, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 29, 10, 0) + strategy.entry("K1", strategy.long, limit=close - 3) +if at(2025, 4, 29, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 30, 10, 0) + strategy.order("W1", strategy.short) +if at(2025, 4, 30, 12, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/tv_trades.csv b/tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/tv_trades.csv new file mode 100644 index 00000000..9ded4f42 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-contract-priced-m25/tv_trades.csv @@ -0,0 +1,15 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-23 20:15,Close position order,1817.08,27.7255,49445.379445,-174.9479,-0.35,1109.02,959.5795,1.92,-579.18567,-1.16,-724.6899,-0.72,8 +1,Entry long,2025-04-23 18:15,O1,1783.39,27.7255,49445.379445,-174.9479,-0.35,1109.02,959.5795,1.92,-579.18567,-1.16,-724.6899,-0.72,8 +2,Exit long,2025-04-23 20:15,Close position order,1817.08,27.4871,49328.34966,-481.574,-0.97,1099.484,643.1981,1.29,-625.8813,-1.25,-656.5219,-0.66,6 +2,Entry long,2025-04-23 18:45,O2,1794.6,27.4871,49328.34966,-481.574,-0.97,1099.484,643.1981,1.29,-625.8813,-1.25,-656.5219,-0.66,6 +3,Exit short,2025-04-24 20:15,Close position order,1750.11,28.1305,49109.10168,-1247.5876,-2.51,1125.22,0,0.00,-870.35767,-1.75,-1904.1096,-1.90,8 +3,Entry short,2025-04-24 18:15,M1,1745.76,28.1305,49109.10168,-1247.5876,-2.51,1125.22,0,0.00,-870.35767,-1.75,-1904.1096,-1.90,8 +4,Exit long,2025-04-25 20:15,Close position order,1795.64,27.1521,48504.782961,-835.4701,-1.70,1086.084,0,0.00,-1042.9121,-2.13,-2739.5796,-2.74,8 +4,Entry long,2025-04-25 18:15,N1,1786.41,27.1521,48504.782961,-835.4701,-1.70,1086.084,0,0.00,-1042.9121,-2.13,-2739.5796,-2.74,8 +5,Exit short,2025-04-28 20:15,Close position order,1808.09,26.7015,48096.076875,-1250.6982,-2.57,1068.06,0,0.00,-1213.8502,-2.50,-3990.2778,-3.99,8 +5,Entry short,2025-04-28 18:15,V1,1801.25,26.7015,48096.076875,-1250.6982,-2.57,1068.06,0,0.00,-1213.8502,-2.50,-3990.2778,-3.99,8 +6,Exit long,2025-04-29 20:15,Close position order,1830.2,25.9101,47486.476674,-1102.2157,-2.30,1036.404,0,0.00,-797.5129,-1.66,-5092.4937,-5.09,8 +6,Entry long,2025-04-29 18:15,K1,1832.74,25.9101,47486.476674,-1102.2157,-2.30,1036.404,0,0.00,-797.5129,-1.66,-5092.4937,-5.09,8 +7,Exit short,2025-04-30 20:15,Close position order,1809.6,25.9899,46934.120814,-1136.7982,-2.40,1039.596,0,0.00,-783.3356,-1.65,-6229.292,-6.23,8 +7,Entry short,2025-04-30 18:15,W1,1805.86,25.9899,46934.120814,-1136.7982,-2.40,1039.596,0,0.00,-783.3356,-1.65,-6229.292,-6.23,8 \ No newline at end of file diff --git a/tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/meta.json b/tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/meta.json new file mode 100644 index 00000000..262a1f8e --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/meta.json @@ -0,0 +1,17 @@ +{ + "title": "PAR-CASHFEE pcf-es-contract-p50-m25", + "slug": "pcf-es-contract-p50-m25", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T03:47:05.872Z", + "chart": { + "symbol": "CME_MINI:ES1!", + "interval": "15", + "from": "2025-06-01", + "to": "2025-07-01", + "timeoutMs": 120000 + }, + "pine_sha256": "0a6e7ceaad6aa4846bc769ee9d2002857788b08e7ace4dc6cff699a5e297622c", + "tv_trades_sha256": "953754c5e1e3ed239f9375b3834f3d20c89026b53d69edb9cbc911ff13a31a5e", + "tv_trades_rows": 14 +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/metrics.json b/tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/metrics.json new file mode 100644 index 00000000..8d4f7654 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pcf-es-contract-p50-m25", + "symbol": "CME_MINI:ES1!", + "interval": "15", + "from": "2025-06-01", + "to": "2025-07-01", + "deepBacktesting": true, + "trades": 7, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "0a6e7ceaad6aa4846bc769ee9d2002857788b08e7ace4dc6cff699a5e297622c", + "tvTradesCsvHash": "953754c5e1e3ed239f9375b3834f3d20c89026b53d69edb9cbc911ff13a31a5e", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.81539", + "symbol": "CME_MINI:ES1!", + "interval": "15", + "requestedRange": { + "from": 1748736000000, + "to": 1751328000000 + }, + "returnedRange": { + "from": 1748815200000, + "to": 1751328000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "68dbd1eab3280061bffa5f6117539ae8d06077bf5126dbea3d43966f595da748", + "decodedReportSha256": "16fc6786e9493de135c2d03c9f6e145854403e198e12b9a1804474187f9d1234", + "closedTrades": 7, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T03:47:05.872Z" +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/strategy.pine b/tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/strategy.pine new file mode 100644 index 00000000..f45b2707 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/strategy.pine @@ -0,0 +1,41 @@ +// R5 lane PAR-CASHFEE: TradingView's percent-of-equity default sizing under +// a cash commission (cash_per_contract 1000), default_qty_value 50, margin 25/25, +// pyramiding 2, initial capital 1000000000. Each scenario is flat at its start (its own +// UTC day) and is closed by strategy.close_all; orders fill at the next bar's open. +// A 2025-06-02: flat long A1, then add A2 while A1 is open +// B 2025-06-03: flat short B1, then add B2 while B1 is open +// D 2025-06-04: long D1, then a default short D2 reverses it +// F 2025-06-05: flat long F1 alone +//@version=6 +strategy("PAR-CASHFEE pcf-es-contract-p50-m25", overlay=true, initial_capital=1000000000, + default_qty_type=strategy.percent_of_equity, default_qty_value=50, pyramiding=2, + commission_type=strategy.commission.cash_per_contract, commission_value=1000, + process_orders_on_close=false, margin_long=25, margin_short=25) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 6, 2, 14, 0) + strategy.entry("A1", strategy.long) +if at(2025, 6, 2, 14, 30) + strategy.entry("A2", strategy.long) +if at(2025, 6, 2, 16, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 6, 3, 14, 0) + strategy.entry("B1", strategy.short) +if at(2025, 6, 3, 14, 30) + strategy.entry("B2", strategy.short) +if at(2025, 6, 3, 16, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 6, 4, 14, 0) + strategy.entry("D1", strategy.long) +if at(2025, 6, 4, 14, 30) + strategy.entry("D2", strategy.short) +if at(2025, 6, 4, 16, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 6, 5, 14, 0) + strategy.entry("F1", strategy.long) +if at(2025, 6, 5, 16, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/tv_trades.csv b/tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/tv_trades.csv new file mode 100644 index 00000000..4bd19b18 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-es-contract-p50-m25/tv_trades.csv @@ -0,0 +1,15 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-03 00:15,Close position order,5917,1692,498230550,-1036350,-0.21,3384000,1015200,0.20,-1861200,-0.37,-2722350,-0.27,8 +1,Entry long,2025-06-02 22:15,A1,5889.25,1692,498230550,-1036350,-0.21,3384000,1015200,0.20,-1861200,-0.37,-2722350,-0.27,8 +2,Exit long,2025-06-03 00:15,Close position order,5917,1686,497896875,-2465775,-0.49,3372000,0,0.00,-2086425,-0.42,-3502125,-0.35,6 +2,Entry long,2025-06-02 22:45,A2,5906.25,1686,497896875,-2465775,-0.49,3372000,0,0.00,-2086425,-0.42,-3502125,-0.35,6 +3,Exit short,2025-06-04 00:15,Close position order,5977.5,1669,496506637.5,-5653737.5,-1.13,3338000,0,0.00,-3984737.5,-0.80,-10818862,-1.08,8 +3,Entry short,2025-06-03 22:15,B1,5949.75,1669,496506637.5,-5653737.5,-1.13,3338000,0,0.00,-3984737.5,-0.80,-10818862,-1.08,8 +4,Exit short,2025-06-04 00:15,Close position order,5977.5,1663,495345337.5,-5009787.5,-1.01,3326000,0,0.00,-3346787.5,-0.67,-14165650,-1.42,6 +4,Entry short,2025-06-03 22:45,B2,5957.25,1663,495345337.5,-5009787.5,-1.01,3326000,0,0.00,-3346787.5,-0.67,-14165650,-1.42,6 +5,Exit long,2025-06-04 22:45,D2,5989.25,1641,491253862.5,-3117900,-0.63,3282000,0,0.00,-1764075,-0.36,-17283550,-1.73,2 +5,Entry long,2025-06-04 22:15,D1,5987.25,1641,491253862.5,-3117900,-0.63,3282000,0,0.00,-1764075,-0.36,-17283550,-1.73,2 +6,Exit short,2025-06-05 00:15,Close position order,5978,1638,490519575,-2354625,-0.48,3276000,0,0.00,-2375100,-0.48,-19638176,-1.96,6 +6,Entry short,2025-06-04 22:45,D2,5989.25,1638,490519575,-2354625,-0.48,3276000,0,0.00,-2375100,-0.48,-19638176,-1.96,6 +7,Exit long,2025-06-06 00:15,Close position order,5999,1637,488235250,-491100,-0.10,3274000,1964400,0.40,-2209950,-0.45,-20129276,-2.01,8 +7,Entry long,2025-06-05 22:15,F1,5965,1637,488235250,-491100,-0.10,3274000,1964400,0.40,-2209950,-0.45,-20129276,-2.01,8 \ No newline at end of file diff --git a/tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/meta.json b/tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/meta.json new file mode 100644 index 00000000..3bd80841 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/meta.json @@ -0,0 +1,17 @@ +{ + "title": "PAR-CASHFEE pcf-es-order-p50-m25", + "slug": "pcf-es-order-p50-m25", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T03:47:01.381Z", + "chart": { + "symbol": "CME_MINI:ES1!", + "interval": "15", + "from": "2025-06-01", + "to": "2025-07-01", + "timeoutMs": 120000 + }, + "pine_sha256": "14975cf40b8a600753f792130d2c7c1bb2722ae4247eda4af92201f406991c4b", + "tv_trades_sha256": "05882d7a78e3a8ca6a3e8a49249ff66169b30cbe23982c547f977e2fae387f44", + "tv_trades_rows": 14 +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/metrics.json b/tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/metrics.json new file mode 100644 index 00000000..e4ab68f6 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pcf-es-order-p50-m25", + "symbol": "CME_MINI:ES1!", + "interval": "15", + "from": "2025-06-01", + "to": "2025-07-01", + "deepBacktesting": true, + "trades": 7, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "14975cf40b8a600753f792130d2c7c1bb2722ae4247eda4af92201f406991c4b", + "tvTradesCsvHash": "05882d7a78e3a8ca6a3e8a49249ff66169b30cbe23982c547f977e2fae387f44", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.81535", + "symbol": "CME_MINI:ES1!", + "interval": "15", + "requestedRange": { + "from": 1748736000000, + "to": 1751328000000 + }, + "returnedRange": { + "from": 1748815200000, + "to": 1751328000000 + }, + "rangeProof": "covered", + "rawFrameSha256": "3dc7cae5fa1589fbed839860c77c0796b4e1cd66c40706b77172e70883f441c6", + "decodedReportSha256": "8bd2fc3052812f1a9d53df4b169a4b72caf8a5514900599bf04051427177246c", + "closedTrades": 7, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T03:47:01.380Z" +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/strategy.pine b/tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/strategy.pine new file mode 100644 index 00000000..76086cf8 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/strategy.pine @@ -0,0 +1,41 @@ +// R5 lane PAR-CASHFEE: TradingView's percent-of-equity default sizing under +// a cash commission (cash_per_order 1000000), default_qty_value 50, margin 25/25, +// pyramiding 2, initial capital 1000000000. Each scenario is flat at its start (its own +// UTC day) and is closed by strategy.close_all; orders fill at the next bar's open. +// A 2025-06-02: flat long A1, then add A2 while A1 is open +// B 2025-06-03: flat short B1, then add B2 while B1 is open +// D 2025-06-04: long D1, then a default short D2 reverses it +// F 2025-06-05: flat long F1 alone +//@version=6 +strategy("PAR-CASHFEE pcf-es-order-p50-m25", overlay=true, initial_capital=1000000000, + default_qty_type=strategy.percent_of_equity, default_qty_value=50, pyramiding=2, + commission_type=strategy.commission.cash_per_order, commission_value=1000000, + process_orders_on_close=false, margin_long=25, margin_short=25) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 6, 2, 14, 0) + strategy.entry("A1", strategy.long) +if at(2025, 6, 2, 14, 30) + strategy.entry("A2", strategy.long) +if at(2025, 6, 2, 16, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 6, 3, 14, 0) + strategy.entry("B1", strategy.short) +if at(2025, 6, 3, 14, 30) + strategy.entry("B2", strategy.short) +if at(2025, 6, 3, 16, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 6, 4, 14, 0) + strategy.entry("D1", strategy.long) +if at(2025, 6, 4, 14, 30) + strategy.entry("D2", strategy.short) +if at(2025, 6, 4, 16, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 6, 5, 14, 0) + strategy.entry("F1", strategy.long) +if at(2025, 6, 5, 16, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/tv_trades.csv b/tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/tv_trades.csv new file mode 100644 index 00000000..173f9c6b --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-es-order-p50-m25/tv_trades.csv @@ -0,0 +1,15 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-06-03 00:15,Close position order,5917,1694,498819475,849834,0.17,1500591.0165,1710400,0.34,-1169400,-0.23,-150166.02,-0.02,8 +1,Entry long,2025-06-02 22:15,A1,5889.25,1694,498819475,849834,0.17,1500591.0165,1710400,0.34,-1169400,-0.23,-150166.02,-0.02,8 +2,Exit long,2025-06-03 00:15,Close position order,5917,1690,499078125,-591034,-0.12,1499408.9835,267500,0.05,-1401375,-0.28,258800,0.03,6 +2,Entry long,2025-06-02 22:45,A2,5906.25,1690,499078125,-591034,-0.12,1499408.9835,267500,0.05,-1401375,-0.28,258800,0.03,6 +3,Exit short,2025-06-04 00:15,Close position order,5977.5,1677,498886537.5,-3827584,-0.77,1500746.4915,0,0.00,-3326837.5,-0.67,-4568784,-0.46,8 +3,Entry short,2025-06-03 22:15,B1,5949.75,1677,498886537.5,-3827584,-0.77,1500746.4915,0,0.00,-3326837.5,-0.67,-4568784,-0.46,8 +4,Exit short,2025-06-04 00:15,Close position order,5977.5,1672,498026100,-3192153.5,-0.64,1499253.5085,0,0.00,-2692900,-0.54,-6760937.5,-0.68,6 +4,Entry short,2025-06-03 22:45,B2,5957.25,1672,498026100,-3192153.5,-0.64,1499253.5085,0,0.00,-2692900,-0.54,-6760937.5,-0.68,6 +5,Exit long,2025-06-04 22:45,D2,5989.25,1655,495444937.5,-1334802.2,-0.27,1500302.2975,0,0.00,-1124125,-0.23,-8095740,-0.81,2 +5,Entry long,2025-06-04 22:15,D1,5987.25,1655,495444937.5,-1334802.2,-0.27,1500302.2975,0,0.00,-1124125,-0.23,-8095740,-0.81,2 +6,Exit short,2025-06-05 00:15,Close position order,5978,1653,495011512.5,-569885.2,-0.12,1499697.7025,512764.8,0.10,-1243547.8,-0.25,-8665625,-0.87,6 +6,Entry short,2025-06-04 22:45,D2,5989.25,1653,495011512.5,-569885.2,-0.12,1499697.7025,512764.8,0.10,-1243547.8,-0.25,-8665625,-0.87,6 +7,Exit long,2025-06-06 00:15,Close position order,5999,1658,494498500,818600,0.17,2000000,2647600,0.53,-1580300,-0.32,-7847025,-0.78,8 +7,Entry long,2025-06-05 22:15,F1,5965,1658,494498500,818600,0.17,2000000,2647600,0.53,-1580300,-0.32,-7847025,-0.78,8 \ No newline at end of file diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/meta.json b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/meta.json new file mode 100644 index 00000000..565cdcbd --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/meta.json @@ -0,0 +1,17 @@ +{ + "title": "PAR-CASHFEE pcf-order-p100-m100", + "slug": "pcf-order-p100-m100", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T03:39:59.658Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "timeoutMs": 120000 + }, + "pine_sha256": "939f4c83899e13675679082dd7bed3ab4e8c0d4ea6ea1fe03b3e63146b21e652", + "tv_trades_sha256": "5fe7fb71f114a7ecc08a295cd373d4cfd93d1f6f8be9545875d32e876620bab4", + "tv_trades_rows": 22 +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/metrics.json b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/metrics.json new file mode 100644 index 00000000..7979424c --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pcf-order-p100-m100", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "deepBacktesting": true, + "trades": 11, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "939f4c83899e13675679082dd7bed3ab4e8c0d4ea6ea1fe03b3e63146b21e652", + "tvTradesCsvHash": "5fe7fb71f114a7ecc08a295cd373d4cfd93d1f6f8be9545875d32e876620bab4", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.81283", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "20770ae30ff0bac5d63fad8be0b4bf504b6103068bb5dbffca1925e63b32bb54", + "decodedReportSha256": "7e50fedafb55893f42b14f601d54d99d345bea9b4a4d6ae9c5f9492bb3ea6a7d", + "closedTrades": 11, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T03:39:59.658Z" +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/strategy.pine b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/strategy.pine new file mode 100644 index 00000000..19c838b4 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/strategy.pine @@ -0,0 +1,43 @@ +// R5 lane PAR-CASHFEE: TradingView's percent-of-equity default sizing under +// a cash commission (cash_per_order 1000), default_qty_value 100, margin 100/100, +// pyramiding 1. Each scenario is flat at its start (its own UTC day) and is +// closed by strategy.close_all; orders fill at the next bar's open. +// P 2025-04-02: flat long, next open below the signal close +// Q 2025-04-03: flat long, next open above the signal close +// R 2025-04-04: flat short +// T 2025-04-10: long T1, then a default short T2 reverses it +// U 2025-04-12: flat long, next open equal to the signal close +//@version=6 +strategy("PAR-CASHFEE pcf-order-p100-m100", overlay=true, initial_capital=100000, + default_qty_type=strategy.percent_of_equity, default_qty_value=100, pyramiding=1, + commission_type=strategy.commission.cash_per_order, commission_value=1000, + process_orders_on_close=false, margin_long=100, margin_short=100) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 4, 2, 10, 0) + strategy.entry("P1", strategy.long) +if at(2025, 4, 2, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 3, 10, 0) + strategy.entry("Q1", strategy.long) +if at(2025, 4, 3, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 4, 10, 0) + strategy.entry("R1", strategy.short) +if at(2025, 4, 4, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 10, 10, 0) + strategy.entry("T1", strategy.long) +if at(2025, 4, 10, 10, 30) + strategy.entry("T2", strategy.short) +if at(2025, 4, 10, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 12, 10, 0) + strategy.entry("U1", strategy.long) +if at(2025, 4, 12, 12, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/tv_trades.csv b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/tv_trades.csv new file mode 100644 index 00000000..25656609 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m100/tv_trades.csv @@ -0,0 +1,23 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-02 20:15,Close position order,1865.26,52.6587,98999.409174,-2777.2424,-2.78,2000,0,0.00,-2078.9768,-2.08,-2777.2424,-2.78,8 +1,Entry long,2025-04-02 18:15,P1,1880.02,52.6587,98999.409174,-2777.2424,-2.78,2000,0,0.00,-2078.9768,-2.08,-2777.2424,-2.78,8 +2,Exit long,2025-04-03 18:15,Margin call,1811.58,0.0008,1.449264,-1000.0151,-68291.86,1000.01506149,0,0.00,-0.015061488,-1.03,-4777.242,-4.78,0 +2,Entry long,2025-04-03 18:15,Q1,1811.58,0.0008,1.449264,-1000.0151,-68291.86,1000.01506149,0,0.00,-0.015061488,-1.03,-4777.242,-4.78,0 +3,Exit long,2025-04-03 18:15,Margin call,1813.4,2.2032,3991.273056,-1037.4695,-25.73,1041.4793394,0,0.00,-41.47934,-1.03,-5773.2324,-5.77,0 +3,Entry long,2025-04-03 18:15,Q1,1811.58,2.2032,3991.273056,-1037.4695,-25.73,1041.4793394,0,0.00,-41.47934,-1.03,-5773.2324,-5.77,0 +4,Exit long,2025-04-03 20:15,Close position order,1797.78,50.9116,92230.436328,-2661.0857,-2.86,1958.5055991,0,0.00,-2253.6968,-2.42,-7475.8125,-7.48,8 +4,Entry long,2025-04-03 18:15,Q1,1811.58,50.9116,92230.436328,-2661.0857,-2.86,1958.5055991,0,0.00,-2253.6968,-2.42,-7475.8125,-7.48,8 +5,Exit short,2025-04-04 18:15,Margin call,1814.76,0.4592,832.387248,-1010.04535,-120.03,1009.09478021,0,0.00,-10.045324,-1.19,-9476.764,-9.48,0 +5,Entry short,2025-04-04 18:15,R1,1812.69,0.4592,832.387248,-1010.04535,-120.03,1009.09478021,0,0.00,-10.045324,-1.19,-9476.764,-9.48,0 +6,Exit short,2025-04-04 20:15,Close position order,1780.11,50.0313,90691.237197,-360.88547,-0.39,1990.9052198,1769.3217,1.93,-1094.47,-1.19,-8846.743,-8.85,8 +6,Entry short,2025-04-04 18:15,R1,1812.69,50.0313,90691.237197,-360.88547,-0.39,1990.9052198,1769.3217,1.93,-1094.47,-1.19,-8846.743,-8.85,8 +7,Exit long,2025-04-10 18:45,T2,1590.36,56.3046,90153.236382,-2111.4604,-2.32,1502.8076342,0,0.00,-1684.664,-1.85,-10958.204,-10.96,2 +7,Entry long,2025-04-10 18:15,T1,1601.17,56.3046,90153.236382,-2111.4604,-2.32,1502.8076342,0,0.00,-1684.664,-1.85,-10958.204,-10.96,2 +8,Exit short,2025-04-10 18:45,Margin call,1594.81,1.2424,1975.863264,-1016.6235,-51.16,1011.09479873,0,0.00,-16.623478,-0.84,-12460.925,-12.46,0 +8,Entry short,2025-04-10 18:45,T2,1590.36,1.2424,1975.863264,-1016.6235,-51.16,1011.09479873,0,0.00,-16.623478,-0.84,-12460.925,-12.46,0 +9,Exit short,2025-04-10 20:00,Margin call,1621.14,5.8736,9341.138496,-1233.2415,-13.13,1052.45203625,5.46166,0.06,-233.24144,-2.48,-13641.714,-13.64,5 +9,Entry short,2025-04-10 18:45,T2,1590.36,5.8736,9341.138496,-1233.2415,-13.13,1052.45203625,5.46166,0.06,-233.24144,-2.48,-13641.714,-13.64,5 +10,Exit short,2025-04-10 20:15,Close position order,1595.9,48.5598,77227.563528,-1702.6669,-2.19,1433.6455308,45.1541,0.06,-1928.3162,-2.48,-14910.735,-14.91,6 +10,Entry short,2025-04-10 18:45,T2,1590.36,48.5598,77227.563528,-1702.6669,-2.19,1433.6455308,45.1541,0.06,-1928.3162,-2.48,-14910.735,-14.91,6 +11,Exit long,2025-04-12 20:15,Close position order,1596,52.7983,84089.212495,-1823.1257,-2.14,2000,0,0.00,-1308.87,-1.54,-16733.861,-16.73,8 +11,Entry long,2025-04-12 18:15,U1,1592.65,52.7983,84089.212495,-1823.1257,-2.14,2000,0,0.00,-1308.87,-1.54,-16733.861,-16.73,8 \ No newline at end of file diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/meta.json b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/meta.json new file mode 100644 index 00000000..660d5b0c --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/meta.json @@ -0,0 +1,17 @@ +{ + "title": "PAR-CASHFEE pcf-order-p100-m25", + "slug": "pcf-order-p100-m25", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T03:39:48.430Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "timeoutMs": 120000 + }, + "pine_sha256": "cef5da27219ae357e20bae4d0d611cfe063ad0982d0269c9b062e22ce871ac10", + "tv_trades_sha256": "93cd6031e84fb1b303e9d22cc8a8cc3f5af7dfe48b640c9b31cc819656519602", + "tv_trades_rows": 30 +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/metrics.json b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/metrics.json new file mode 100644 index 00000000..f03fb0b8 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pcf-order-p100-m25", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "deepBacktesting": true, + "trades": 15, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "cef5da27219ae357e20bae4d0d611cfe063ad0982d0269c9b062e22ce871ac10", + "tvTradesCsvHash": "93cd6031e84fb1b303e9d22cc8a8cc3f5af7dfe48b640c9b31cc819656519602", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.81275", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "c0c25426b9b739eb9acd707fa1a92efd723d9daf75cb25a3fdad507ba224d313", + "decodedReportSha256": "a25703a523080d83745ba0110c19941f55db4cf118fa98472c7797183cf853df", + "closedTrades": 15, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T03:39:48.430Z" +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/strategy.pine b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/strategy.pine new file mode 100644 index 00000000..d907dfb0 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/strategy.pine @@ -0,0 +1,69 @@ +// R5 lane PAR-CASHFEE: TradingView's percent-of-equity default sizing under +// a cash commission (cash_per_order 1000), default_qty_value 100, margin 25/25, +// pyramiding 3. Each scenario is flat at its start (its own UTC day) and is +// closed by strategy.close_all; orders fill at the next bar's open. +// A 2025-04-02: flat long L1, then adds L2 and L3 while the earlier lots are open +// B 2025-04-04: flat short S1, then adds S2 and S3 while the earlier lots are open +// C 2025-04-08: long C1, strategy.close(C1, qty_percent=50), then add C2 on the half lot +// D 2025-04-10: long D1, then a default short D2 reverses it +// E 2025-04-15: short E1, then a default long E2 reverses it +// F 2025-04-17: flat long F1 alone +// G 2025-04-22: flat short G1 alone +//@version=6 +strategy("PAR-CASHFEE pcf-order-p100-m25", overlay=true, initial_capital=100000, + default_qty_type=strategy.percent_of_equity, default_qty_value=100, pyramiding=3, + commission_type=strategy.commission.cash_per_order, commission_value=1000, + process_orders_on_close=false, margin_long=25, margin_short=25) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 4, 2, 10, 0) + strategy.entry("A1", strategy.long) +if at(2025, 4, 2, 10, 30) + strategy.entry("A2", strategy.long) +if at(2025, 4, 2, 11, 0) + strategy.entry("A3", strategy.long) +if at(2025, 4, 2, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 4, 10, 0) + strategy.entry("B1", strategy.short) +if at(2025, 4, 4, 10, 30) + strategy.entry("B2", strategy.short) +if at(2025, 4, 4, 11, 0) + strategy.entry("B3", strategy.short) +if at(2025, 4, 4, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 8, 10, 0) + strategy.entry("C1", strategy.long) +if at(2025, 4, 8, 10, 30) + strategy.close("C1", qty_percent=50) +if at(2025, 4, 8, 11, 0) + strategy.entry("C2", strategy.long) +if at(2025, 4, 8, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 10, 10, 0) + strategy.entry("D1", strategy.long) +if at(2025, 4, 10, 10, 30) + strategy.entry("D2", strategy.short) +if at(2025, 4, 10, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 15, 10, 0) + strategy.entry("E1", strategy.short) +if at(2025, 4, 15, 10, 30) + strategy.entry("E2", strategy.long) +if at(2025, 4, 15, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 17, 10, 0) + strategy.entry("F1", strategy.long) +if at(2025, 4, 17, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 22, 10, 0) + strategy.entry("G1", strategy.short) +if at(2025, 4, 22, 12, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/tv_trades.csv b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/tv_trades.csv new file mode 100644 index 00000000..4dd76e9c --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p100-m25/tv_trades.csv @@ -0,0 +1,31 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-02 20:15,Close position order,1865.26,52.6587,98999.409174,-2113.9788,-2.11,1336.7363774,0,0.00,-2078.9768,-2.08,-4113.979,-4.11,8 +1,Entry long,2025-04-02 18:15,A1,1880.02,52.6587,98999.409174,-2113.9788,-2.11,1336.7363774,0,0.00,-2078.9768,-2.08,-4113.979,-4.11,8 +2,Exit long,2025-04-02 20:15,Close position order,1865.26,52.1255,97707.685985,-1813.4026,-1.84,1333.3267255,0,0.00,-1778.755,-1.80,-4927.3813,-4.93,6 +2,Entry long,2025-04-02 18:45,A2,1874.47,52.1255,97707.685985,-1813.4026,-1.84,1333.3267255,0,0.00,-1778.755,-1.80,-4927.3813,-4.93,6 +3,Exit long,2025-04-02 20:15,Close position order,1865.26,51.5954,96720.220886,-1811.322,-1.85,1329.9368971,0,0.00,-1777.0267,-1.82,-5738.703,-5.74,4 +3,Entry long,2025-04-02 19:15,A3,1874.59,51.5954,96720.220886,-1811.322,-1.85,1329.9368971,0,0.00,-1777.0267,-1.82,-5738.703,-5.74,4 +4,Exit short,2025-04-04 20:15,Close position order,1780.11,51.4488,93260.725272,347.03177,0.37,1329.1701456,1838.4303,1.95,-1106.499,-1.17,-7391.6714,-7.39,8 +4,Entry short,2025-04-04 18:15,B1,1812.69,51.4488,93260.725272,347.03177,0.37,1329.1701456,1838.4303,1.95,-1106.499,-1.17,-7391.6714,-7.39,8 +5,Exit short,2025-04-04 20:15,Close position order,1780.11,52.3107,93531.5316,-921.9532,-0.98,1334.6845939,594.4301,0.63,-1241.6754,-1.31,-7313.625,-7.31,6 +5,Entry short,2025-04-04 18:45,B2,1788,52.3107,93531.5316,-921.9532,-0.98,1334.6845939,594.4301,0.63,-1241.6754,-1.31,-7313.625,-7.31,6 +6,Exit short,2025-04-04 20:15,Close position order,1780.11,52.539,93466.881,-1394.4635,-1.48,1336.1452605,128.53772,0.14,-1307.8785,-1.38,-7708.0884,-7.71,4 +6,Entry short,2025-04-04 19:15,B3,1779,52.539,93466.881,-1394.4635,-1.48,1336.1452605,128.53772,0.14,-1307.8785,-1.38,-7708.0884,-7.71,4 +7,Exit long,2025-04-08 18:45,Close entry(s) order C1,1569,29.1258,45645.95376,-1447.5736,-3.14,1500,0,0.00,-523.5919,-1.13,-9655.662,-9.66,2 +7,Entry long,2025-04-08 18:15,C1,1567.2,29.1258,45645.95376,-1447.5736,-3.14,1500,0,0.00,-523.5919,-1.13,-9655.662,-9.66,2 +8,Exit long,2025-04-08 20:15,Close position order,1584.27,29.1258,45645.95376,-340.95956,-0.74,838.1369716,245.32922,0.53,-588.5424,-1.28,-10496.621,-10.50,8 +8,Entry long,2025-04-08 18:15,C1,1567.2,29.1258,45645.95376,-340.95956,-0.74,838.1369716,245.32922,0.53,-588.5424,-1.28,-10496.621,-10.50,8 +9,Exit long,2025-04-08 20:15,Close position order,1584.27,57.0103,89342.551439,-684.7065,-0.76,1661.8630284,462.8843,0.51,-1149.9371,-1.27,-10181.328,-10.18,4 +9,Entry long,2025-04-08 19:15,C2,1567.13,57.0103,89342.551439,-684.7065,-0.76,1661.8630284,462.8843,0.51,-1149.9371,-1.27,-10181.328,-10.18,4 +10,Exit long,2025-04-10 18:45,D2,1590.36,55.4711,88818.661187,-2102.4927,-2.34,1502.8500617,0,0.00,-1674.5286,-1.86,-12283.82,-12.28,2 +10,Entry long,2025-04-10 18:15,D1,1601.17,55.4711,88818.661187,-2102.4927,-2.34,1502.8500617,0,0.00,-1674.5286,-1.86,-12283.82,-12.28,2 +11,Exit short,2025-04-10 20:15,Close position order,1595.9,54.8423,87219.000228,-1800.9763,-2.05,1497.1499383,43.59514,0.05,-2185.196,-2.49,-14084.797,-14.08,6 +11,Entry short,2025-04-10 18:45,D2,1590.36,54.8423,87219.000228,-1800.9763,-2.05,1497.1499383,43.59514,0.05,-2185.196,-2.49,-14084.797,-14.08,6 +12,Exit short,2025-04-15 18:45,E2,1628.64,51.6978,84914.670456,-781.1414,-0.91,1498.7068762,0,0.00,-1025.3319,-1.19,-14865.938,-14.87,2 +12,Entry short,2025-04-15 18:15,E1,1642.52,51.6978,84914.670456,-781.1414,-0.91,1498.7068762,0,0.00,-1025.3319,-1.19,-14865.938,-14.87,2 +13,Exit long,2025-04-15 20:15,Close position order,1635.17,51.9659,84633.743376,-1161.9558,-1.36,1501.2931238,0,0.00,-881.68353,-1.04,-16027.895,-16.03,6 +13,Entry long,2025-04-15 18:45,E2,1628.64,51.9659,84633.743376,-1161.9558,-1.36,1501.2931238,0,0.00,-881.68353,-1.04,-16027.895,-16.03,6 +14,Exit long,2025-04-17 20:15,Close position order,1594.77,52.2175,82972.56315,-1697.6606,-2.02,2000,0,0.00,-1067.3606,-1.27,-17725.555,-17.73,8 +14,Entry long,2025-04-17 18:15,F1,1588.98,52.2175,82972.56315,-1697.6606,-2.02,2000,0,0.00,-1067.3606,-1.27,-17725.555,-17.73,8 +15,Exit short,2025-04-22 20:15,Close position order,1631.46,50.0637,81274.411854,-2402.5122,-2.92,2000,0,0.00,-1729.9287,-2.10,-20128.066,-20.13,8 +15,Entry short,2025-04-22 18:15,G1,1623.42,50.0637,81274.411854,-2402.5122,-2.92,2000,0,0.00,-1729.9287,-2.10,-20128.066,-20.13,8 \ No newline at end of file diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/meta.json b/tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/meta.json new file mode 100644 index 00000000..a4e0637f --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/meta.json @@ -0,0 +1,17 @@ +{ + "title": "PAR-CASHFEE pcf-order-p200-m25", + "slug": "pcf-order-p200-m25", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T03:46:53.995Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "timeoutMs": 120000 + }, + "pine_sha256": "49161558ee4d51129f47e6ccd254aa47c5c5cd36687cbd39dd61b55ddcb83951", + "tv_trades_sha256": "8e105f593fe1da7900b19eaba74a28b9ce61c20d0178f18d88e09bb673aa9b85", + "tv_trades_rows": 12 +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/metrics.json b/tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/metrics.json new file mode 100644 index 00000000..aacd7e0a --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pcf-order-p200-m25", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "deepBacktesting": true, + "trades": 6, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "49161558ee4d51129f47e6ccd254aa47c5c5cd36687cbd39dd61b55ddcb83951", + "tvTradesCsvHash": "8e105f593fe1da7900b19eaba74a28b9ce61c20d0178f18d88e09bb673aa9b85", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.81529", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "d034f5e1e6e2d7dcdb1a578ca106af38277bc68c1289f4b9646c39644cb8b2de", + "decodedReportSha256": "6521dd8fcb541bcef2e284f9369fa522b0ed8bcc8b202b6e437b53d00325a342", + "closedTrades": 6, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T03:46:53.990Z" +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/strategy.pine b/tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/strategy.pine new file mode 100644 index 00000000..f67a77dc --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/strategy.pine @@ -0,0 +1,39 @@ +// R5 lane PAR-CASHFEE: TradingView's percent-of-equity default sizing under +// a cash commission (cash_per_order 1000), default_qty_value 200, margin 25/25, +// pyramiding 1, initial capital 100000. Each scenario is flat at its start (its own +// UTC day) and is closed by strategy.close_all; orders fill at the next bar's open. +// F 2025-04-17: flat long F1 alone +// G 2025-04-22: flat short G1 alone +// D 2025-04-10: long D1, then a default short D2 reverses it +// E 2025-04-15: short E1, then a default long E2 reverses it +//@version=6 +strategy("PAR-CASHFEE pcf-order-p200-m25", overlay=true, initial_capital=100000, + default_qty_type=strategy.percent_of_equity, default_qty_value=200, pyramiding=1, + commission_type=strategy.commission.cash_per_order, commission_value=1000, + process_orders_on_close=false, margin_long=25, margin_short=25) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 4, 17, 10, 0) + strategy.entry("F1", strategy.long) +if at(2025, 4, 17, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 22, 10, 0) + strategy.entry("G1", strategy.short) +if at(2025, 4, 22, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 10, 10, 0) + strategy.entry("D1", strategy.long) +if at(2025, 4, 10, 10, 30) + strategy.entry("D2", strategy.short) +if at(2025, 4, 10, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 15, 10, 0) + strategy.entry("E1", strategy.short) +if at(2025, 4, 15, 10, 30) + strategy.entry("E2", strategy.long) +if at(2025, 4, 15, 12, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/tv_trades.csv b/tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/tv_trades.csv new file mode 100644 index 00000000..39ffa86c --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p200-m25/tv_trades.csv @@ -0,0 +1,13 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-10 18:45,D2,1590.36,124.2841,198999.972397,-2847.7761,-1.42,1504.2650948,0,0.00,-2511.2947,-1.26,-2847.7761,-2.85,2 +1,Entry long,2025-04-10 18:15,D1,1601.17,124.2841,198999.972397,-2847.7761,-1.42,1504.2650948,0,0.00,-2511.2947,-1.26,-2847.7761,-2.85,2 +2,Exit short,2025-04-10 20:15,Close position order,1595.9,122.1817,194312.888412,-2172.6216,-1.12,1495.7349052,708.9767,0.36,-4256.488,-2.18,-5020.398,-5.02,6 +2,Entry short,2025-04-10 18:45,D2,1590.36,122.1817,194312.888412,-2172.6216,-1.12,1495.7349052,708.9767,0.36,-4256.488,-2.18,-5020.398,-5.02,6 +3,Exit short,2025-04-15 18:45,E2,1628.64,115.0415,188957.96458,100.477715,0.05,1496.2983031,986.7667,0.52,-1056.3704,-0.56,-4919.92,-4.92,2 +3,Entry short,2025-04-15 18:15,E1,1642.52,115.0415,188957.96458,100.477715,0.05,1496.2983031,986.7667,0.52,-1056.3704,-0.56,-4919.92,-4.92,2 +4,Exit long,2025-04-15 20:15,Close position order,1635.17,116.7576,190156.097664,-741.2746,-0.39,1503.7016969,536.6085,0.28,-1358.3673,-0.71,-5661.195,-5.66,6 +4,Entry long,2025-04-15 18:45,E2,1628.64,116.7576,190156.097664,-741.2746,-0.39,1503.7016969,536.6085,0.28,-1358.3673,-0.71,-5661.195,-5.66,6 +5,Exit long,2025-04-17 20:15,Close position order,1594.77,118.1127,187678.718046,-1316.1274,-0.70,2000,120.88953,0.06,-1152.3654,-0.61,-6977.3223,-6.98,8 +5,Entry long,2025-04-17 18:15,F1,1588.98,118.1127,187678.718046,-1316.1274,-0.70,2000,120.88953,0.06,-1152.3654,-0.61,-6977.3223,-6.98,8 +6,Exit short,2025-04-22 20:15,Close position order,1631.46,113.9848,185045.204016,-2916.4377,-1.57,2000,254.97264,0.14,-2661.8984,-1.43,-9893.76,-9.89,8 +6,Entry short,2025-04-22 18:15,G1,1623.42,113.9848,185045.204016,-2916.4377,-1.57,2000,254.97264,0.14,-2661.8984,-1.43,-9893.76,-9.89,8 \ No newline at end of file diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/meta.json b/tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/meta.json new file mode 100644 index 00000000..fa54c345 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/meta.json @@ -0,0 +1,17 @@ +{ + "title": "PAR-CASHFEE pcf-order-p50-m25", + "slug": "pcf-order-p50-m25", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T03:39:39.878Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "timeoutMs": 120000 + }, + "pine_sha256": "34e2a525dc44a2389221779bdd837ea546804407c66d6ac9bd1a292790e4ab6e", + "tv_trades_sha256": "5b858a622a4d8afdb8e3297fbe74e83fdeb6ba2701be8fd8a4bc13c8dac55849", + "tv_trades_rows": 30 +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/metrics.json b/tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/metrics.json new file mode 100644 index 00000000..b71de3cb --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pcf-order-p50-m25", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "deepBacktesting": true, + "trades": 15, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "34e2a525dc44a2389221779bdd837ea546804407c66d6ac9bd1a292790e4ab6e", + "tvTradesCsvHash": "5b858a622a4d8afdb8e3297fbe74e83fdeb6ba2701be8fd8a4bc13c8dac55849", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.81269", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "5502352490cae3397216408593f2de4d1b5a2a16d9319fd7accfd574ca7a4a9b", + "decodedReportSha256": "0bdeabedbe9b5d715e9c21ac079f637a5bef557aec3bcc454afd43faa78a2a47", + "closedTrades": 15, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T03:39:39.878Z" +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/strategy.pine b/tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/strategy.pine new file mode 100644 index 00000000..ab7416be --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/strategy.pine @@ -0,0 +1,69 @@ +// R5 lane PAR-CASHFEE: TradingView's percent-of-equity default sizing under +// a cash commission (cash_per_order 1000), default_qty_value 50, margin 25/25, +// pyramiding 3. Each scenario is flat at its start (its own UTC day) and is +// closed by strategy.close_all; orders fill at the next bar's open. +// A 2025-04-02: flat long L1, then adds L2 and L3 while the earlier lots are open +// B 2025-04-04: flat short S1, then adds S2 and S3 while the earlier lots are open +// C 2025-04-08: long C1, strategy.close(C1, qty_percent=50), then add C2 on the half lot +// D 2025-04-10: long D1, then a default short D2 reverses it +// E 2025-04-15: short E1, then a default long E2 reverses it +// F 2025-04-17: flat long F1 alone +// G 2025-04-22: flat short G1 alone +//@version=6 +strategy("PAR-CASHFEE pcf-order-p50-m25", overlay=true, initial_capital=100000, + default_qty_type=strategy.percent_of_equity, default_qty_value=50, pyramiding=3, + commission_type=strategy.commission.cash_per_order, commission_value=1000, + process_orders_on_close=false, margin_long=25, margin_short=25) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 4, 2, 10, 0) + strategy.entry("A1", strategy.long) +if at(2025, 4, 2, 10, 30) + strategy.entry("A2", strategy.long) +if at(2025, 4, 2, 11, 0) + strategy.entry("A3", strategy.long) +if at(2025, 4, 2, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 4, 10, 0) + strategy.entry("B1", strategy.short) +if at(2025, 4, 4, 10, 30) + strategy.entry("B2", strategy.short) +if at(2025, 4, 4, 11, 0) + strategy.entry("B3", strategy.short) +if at(2025, 4, 4, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 8, 10, 0) + strategy.entry("C1", strategy.long) +if at(2025, 4, 8, 10, 30) + strategy.close("C1", qty_percent=50) +if at(2025, 4, 8, 11, 0) + strategy.entry("C2", strategy.long) +if at(2025, 4, 8, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 10, 10, 0) + strategy.entry("D1", strategy.long) +if at(2025, 4, 10, 10, 30) + strategy.entry("D2", strategy.short) +if at(2025, 4, 10, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 15, 10, 0) + strategy.entry("E1", strategy.short) +if at(2025, 4, 15, 10, 30) + strategy.entry("E2", strategy.long) +if at(2025, 4, 15, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 17, 10, 0) + strategy.entry("F1", strategy.long) +if at(2025, 4, 17, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 22, 10, 0) + strategy.entry("G1", strategy.short) +if at(2025, 4, 22, 12, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/tv_trades.csv b/tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/tv_trades.csv new file mode 100644 index 00000000..23787c05 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-p50-m25/tv_trades.csv @@ -0,0 +1,31 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-02 20:15,Close position order,1865.26,26.0634,48999.713268,-1721.1396,-3.44,1336.4438931,0,0.00,-1534.0391,-3.07,-3721.1396,-3.72,8 +1,Entry long,2025-04-02 18:15,A1,1880.02,26.0634,48999.713268,-1721.1396,-3.44,1336.4438931,0,0.00,-1534.0391,-3.07,-3721.1396,-3.72,8 +2,Exit long,2025-04-02 20:15,Close position order,1865.26,25.8353,48427.494791,-1571.4425,-3.18,1333.4994249,0,0.00,-1385.9794,-2.80,-4292.582,-4.29,6 +2,Entry long,2025-04-02 18:45,A2,1874.47,25.8353,48427.494791,-1571.4425,-3.18,1333.4994249,0,0.00,-1385.9794,-2.80,-4292.582,-4.29,6 +3,Exit long,2025-04-02 20:15,Close position order,1865.26,25.5686,47930.641874,-1568.6117,-3.21,1330.056682,0,0.00,-1385.0631,-2.83,-4861.194,-4.86,4 +3,Entry long,2025-04-02 19:15,A3,1874.59,25.5686,47930.641874,-1568.6117,-3.21,1330.056682,0,0.00,-1385.0631,-2.83,-4861.194,-4.86,4 +4,Exit short,2025-04-04 20:15,Close position order,1780.11,25.6906,46569.093714,-494.24353,-1.04,1331.2432873,417.3504,0.88,-1053.1796,-2.21,-7355.4375,-7.36,8 +4,Entry short,2025-04-04 18:15,B1,1812.69,25.6906,46569.093714,-494.24353,-1.04,1331.2432873,417.3504,0.88,-1053.1796,-2.21,-7355.4375,-7.36,8 +5,Exit short,2025-04-04 20:15,Close position order,1780.11,25.9432,46386.4416,-1129.8083,-2.38,1334.5002005,0,0.00,-1119.8575,-2.36,-7485.2456,-7.49,6 +5,Entry short,2025-04-04 18:45,B2,1788,25.9432,46386.4416,-1129.8083,-2.38,1334.5002005,0,0.00,-1119.8575,-2.36,-7485.2456,-7.49,6 +6,Exit short,2025-04-04 20:15,Close position order,1780.11,25.9243,46119.3297,-1363.0325,-2.89,1334.2565122,0,0.00,-1151.9164,-2.44,-7848.2783,-7.85,4 +6,Entry short,2025-04-04 19:15,B3,1779,25.9243,46119.3297,-1363.0325,-2.89,1334.2565122,0,0.00,-1151.9164,-2.44,-7848.2783,-7.85,4 +7,Exit long,2025-04-08 18:45,Close entry(s) order C1,1569,14.381,22537.9032,-1474.1143,-6.40,1500,0,0.00,-511.64862,-2.22,-9822.393,-9.82,2 +7,Entry long,2025-04-08 18:15,C1,1567.2,14.381,22537.9032,-1474.1143,-6.40,1500,0,0.00,-511.64862,-2.22,-9822.393,-9.82,2 +8,Exit long,2025-04-08 20:15,Close position order,1584.27,14.381,22537.9032,-592.7798,-2.57,838.2634508,0,0.00,-543.71826,-2.36,-10915.172,-10.92,8 +8,Entry long,2025-04-08 18:15,C1,1567.2,14.381,22537.9032,-592.7798,-2.57,838.2634508,0,0.00,-543.71826,-2.36,-10915.172,-10.92,8 +9,Exit long,2025-04-08 20:15,Close position order,1584.27,28.1332,44088.381716,-1179.5334,-2.62,1661.7365492,0,0.00,-1073.9904,-2.38,-11094.706,-11.09,4 +9,Entry long,2025-04-08 19:15,C2,1567.13,28.1332,44088.381716,-1179.5334,-2.62,1661.7365492,0,0.00,-1073.9904,-2.38,-11094.706,-11.09,4 +10,Exit long,2025-04-10 18:45,D2,1590.36,27.138,43452.55146,-1795.4171,-4.04,1502.0553597,0,0.00,-1329.998,-2.99,-12890.123,-12.89,2 +10,Entry long,2025-04-10 18:15,D1,1601.17,27.138,43452.55146,-1795.4171,-4.04,1502.0553597,0,0.00,-1329.998,-2.99,-12890.123,-12.89,2 +11,Exit short,2025-04-10 20:15,Close position order,1595.9,26.9158,42805.811688,-1647.0582,-3.80,1497.9446403,0,0.00,-1326.413,-3.06,-14537.181,-14.54,6 +11,Entry short,2025-04-10 18:45,D2,1590.36,26.9158,42805.811688,-1647.0582,-3.80,1497.9446403,0,0.00,-1326.413,-3.06,-14537.181,-14.54,6 +12,Exit short,2025-04-15 18:45,E2,1628.64,25.4067,41731.012884,-1147.176,-2.68,1499.8209775,0,0.00,-1012.4493,-2.37,-15684.357,-15.68,2 +12,Entry short,2025-04-15 18:15,E1,1642.52,25.4067,41731.012884,-1147.176,-2.68,1499.8209775,0,0.00,-1012.4493,-2.37,-15684.357,-15.68,2 +13,Exit long,2025-04-15 20:15,Close position order,1635.17,25.4249,41408.009136,-1334.1544,-3.18,1500.1790225,0,0.00,-686.2893,-1.64,-17018.512,-17.02,6 +13,Entry long,2025-04-15 18:45,E2,1628.64,25.4249,41408.009136,-1334.1544,-3.18,1500.1790225,0,0.00,-686.2893,-1.64,-17018.512,-17.02,6 +14,Exit long,2025-04-17 20:15,Close position order,1594.77,25.4823,40490.865054,-1852.4575,-4.46,2000,0,0.00,-1032.8722,-2.49,-18870.969,-18.87,8 +14,Entry long,2025-04-17 18:15,F1,1588.98,25.4823,40490.865054,-1852.4575,-4.46,2000,0,0.00,-1032.8722,-2.49,-18870.969,-18.87,8 +15,Exit short,2025-04-22 20:15,Close position order,1631.46,24.371,39564.36882,-2195.9429,-5.41,2000,0,0.00,-1355.3292,-3.34,-21066.912,-21.07,8 +15,Entry short,2025-04-22 18:15,G1,1623.42,24.371,39564.36882,-2195.9429,-5.41,2000,0,0.00,-1355.3292,-3.34,-21066.912,-21.07,8 \ No newline at end of file diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/meta.json b/tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/meta.json new file mode 100644 index 00000000..7e3ee1cb --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/meta.json @@ -0,0 +1,17 @@ +{ + "title": "PAR-CASHFEE pcf-order-priced-m25", + "slug": "pcf-order-priced-m25", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T04:05:00.840Z", + "chart": { + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "timeoutMs": 120000 + }, + "pine_sha256": "43ac98cfca26b25263b53b032615b3d100ed58f2278f46d418b3fa5bce51b9ab", + "tv_trades_sha256": "3210a82cc426fb6cd5d7a1a09027a7aeab8eccc2b512cd88b893c42efee6da57", + "tv_trades_rows": 14 +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/metrics.json b/tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/metrics.json new file mode 100644 index 00000000..fea42592 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "pcf-order-priced-m25", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "from": "2025-04-01", + "to": "2025-05-01", + "deepBacktesting": true, + "trades": 7, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "43ac98cfca26b25263b53b032615b3d100ed58f2278f46d418b3fa5bce51b9ab", + "tvTradesCsvHash": "3210a82cc426fb6cd5d7a1a09027a7aeab8eccc2b512cd88b893c42efee6da57", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.82088", + "symbol": "BINANCE:ETHUSDT.P", + "interval": "15", + "requestedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "returnedRange": { + "from": 1743465600000, + "to": 1746057600000 + }, + "rangeProof": "covered", + "rawFrameSha256": "47f257da7b3af7c9eab14b7b81f0d378f387101ffee8f0e34ea821b5ab04168e", + "decodedReportSha256": "3104602294325a9e3e3ececca1c7f37b370a4157c54bbb40a3d4ed37489ad16e", + "closedTrades": 7, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T04:05:00.838Z" +} diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/strategy.pine b/tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/strategy.pine new file mode 100644 index 00000000..92f6ae52 --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/strategy.pine @@ -0,0 +1,50 @@ +// R5 lane PAR-CASHFEE: TradingView's percent-of-equity default sizing under +// a cash commission (cash_per_order 1000) for PRICED default entries (limit, stop) and +// for strategy.order; default_qty_value 50, margin 25/25, pyramiding 1. Each +// scenario is flat at its start (its own UTC day) and is closed by +// strategy.close_all. +// O 2025-04-23: strategy.order long O1 from flat, then strategy.order long O2 while O1 is open +// M 2025-04-24: limit short M1 at the signal close + 3 +// N 2025-04-25: stop long N1 at the signal close + 3 +// V 2025-04-28: stop short V1 at the signal close - 3 +// K 2025-04-29: limit long K1 at the signal close - 3 +// W 2025-04-30: strategy.order short W1 from flat +//@version=6 +strategy("PAR-CASHFEE pcf-order-priced-m25", overlay=true, initial_capital=100000, + default_qty_type=strategy.percent_of_equity, default_qty_value=50, pyramiding=1, + commission_type=strategy.commission.cash_per_order, commission_value=1000, + process_orders_on_close=false, margin_long=25, margin_short=25) + +at(int y, int mo, int d, int h, int mi) => time == timestamp("UTC", y, mo, d, h, mi) + +if at(2025, 4, 23, 10, 0) + strategy.order("O1", strategy.long) +if at(2025, 4, 23, 10, 30) + strategy.order("O2", strategy.long) +if at(2025, 4, 23, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 24, 10, 0) + strategy.entry("M1", strategy.short, limit=close + 3) +if at(2025, 4, 24, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 25, 10, 0) + strategy.entry("N1", strategy.long, stop=close + 3) +if at(2025, 4, 25, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 28, 10, 0) + strategy.entry("V1", strategy.short, stop=close - 3) +if at(2025, 4, 28, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 29, 10, 0) + strategy.entry("K1", strategy.long, limit=close - 3) +if at(2025, 4, 29, 12, 0) and strategy.position_size != 0 + strategy.close_all() + +if at(2025, 4, 30, 10, 0) + strategy.order("W1", strategy.short) +if at(2025, 4, 30, 12, 0) and strategy.position_size != 0 + strategy.close_all() diff --git a/tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/tv_trades.csv b/tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/tv_trades.csv new file mode 100644 index 00000000..7f78890d --- /dev/null +++ b/tests/fixtures/cash_fee_sizing/pcf-order-priced-m25/tv_trades.csv @@ -0,0 +1,15 @@ +Trade number,Type,Date and time,Signal,Price USDT,Size (qty),Size (value),Net PnL USDT,Return %,Commission USDT,Favorable excursion USDT,Favorable excursion %,Adverse excursion USDT,Adverse excursion %,Cumulative PnL USDT,Cumulative PnL %,Duration (bars) +1,Exit long,2025-04-23 20:15,Close position order,1817.08,27.4757,48999.888623,-577.68146,-1.16,1503.337791,500.44797,1.00,-1024.4534,-2.05,-1577.6815,-1.58,8 +1,Entry long,2025-04-23 18:15,O1,1783.39,27.4757,48999.888623,-577.68146,-1.16,1503.337791,500.44797,1.00,-1024.4534,-2.05,-1577.6815,-1.58,8 +2,Exit long,2025-04-23 20:15,Close position order,1817.08,27.1113,48653.93898,-887.2002,-1.79,1496.662209,176.63042,0.36,-1075.0983,-2.17,-1464.8816,-1.46,6 +2,Entry long,2025-04-23 18:45,O2,1794.6,27.1113,48653.93898,-887.2002,-1.79,1496.662209,176.63042,0.36,-1075.0983,-2.17,-1464.8816,-1.46,6 +3,Exit short,2025-04-24 20:15,Close position order,1750.11,27.6484,48267.470784,-2120.2705,-4.30,2000,0,0.00,-1302.4735,-2.64,-3585.152,-3.59,8 +3,Entry short,2025-04-24 18:15,M1,1745.76,27.6484,48267.470784,-2120.2705,-4.30,2000,0,0.00,-1302.4735,-2.64,-3585.152,-3.59,8 +4,Exit long,2025-04-25 20:15,Close position order,1795.64,26.4258,47207.313378,-1756.0898,-3.64,2000,0,0.00,-1486.499,-3.08,-5341.242,-5.34,8 +4,Entry long,2025-04-25 18:15,N1,1786.41,26.4258,47207.313378,-1756.0898,-3.64,2000,0,0.00,-1486.499,-3.08,-5341.242,-5.34,8 +5,Exit short,2025-04-28 20:15,Close position order,1808.09,25.7206,46329.23075,-2175.929,-4.60,2000,0,0.00,-1654.8464,-3.50,-7517.171,-7.52,8 +5,Entry short,2025-04-28 18:15,V1,1801.25,25.7206,46329.23075,-2175.929,-4.60,2000,0,0.00,-1654.8464,-3.50,-7517.171,-7.52,8 +6,Exit long,2025-04-29 20:15,Close position order,1830.2,24.6851,45241.370174,-2062.7002,-4.46,2000,0,0.00,-1266.1053,-2.74,-9579.871,-9.58,8 +6,Entry long,2025-04-29 18:15,K1,1832.74,24.6851,45241.370174,-2062.7002,-4.46,2000,0,0.00,-1266.1053,-2.74,-9579.871,-9.58,8 +7,Exit short,2025-04-30 20:15,Close position order,1809.6,24.4815,44210.16159,-2091.5608,-4.63,2000,0,0.00,-1248.2424,-2.76,-11671.432,-11.67,8 +7,Entry short,2025-04-30 18:15,W1,1805.86,24.4815,44210.16159,-2091.5608,-4.63,2000,0,0.00,-1248.2424,-2.76,-11671.432,-11.67,8 \ No newline at end of file diff --git a/tests/test_adapter_quiet_bar_pinned.inc b/tests/test_adapter_quiet_bar_pinned.inc index 3a55b10c..6135cc32 100644 --- a/tests/test_adapter_quiet_bar_pinned.inc +++ b/tests/test_adapter_quiet_bar_pinned.inc @@ -1119,6 +1119,34 @@ // Random48/0/0: trades 0x62a7be3a970dee6a -> 0xda74ceb1a9e7d904, trade_count 9 -> 8 // Random48/1/1: trades 0xee7c0d61bfb7dd62 -> 0x0876fbb01d747e2d, trade_count 9 -> 8 // Random48/1/0: trades 0xee7c0d61bfb7dd62 -> 0x0876fbb01d747e2d, trade_count 9 -> 8 +// R5 lane PAR-CASHFEE moves these runs' trade digests (INT26: the behaviour half +// of its hash step 214c082c, harvested on the integrated tree with this TU's own +// switch): a percent-of-equity default quantity under a cash commission sizes +// from strategy.equity net of the open entries' fees, less its own order's fee. +// Random05/0/* had been moved by an earlier pick too. Their hash values move once, +// in the INT26 v19 hash re-pin: +// Random05/0/1: trades 0x3d4c3aae7bbc3b85 -> 0xf91857e72781f5ae +// Random05/0/0: trades 0x3d4c3aae7bbc3b85 -> 0xf91857e72781f5ae +// Random06/0/1: trades 0x19f4b2ac9f3f76f7 -> 0x75cd705050ee6ca5 +// Random06/0/0: trades 0x19f4b2ac9f3f76f7 -> 0x75cd705050ee6ca5 +// Random06/1/1: trades 0x3561041c80b6994f -> 0x5f84022dee0bd36f +// Random06/1/0: trades 0x3561041c80b6994f -> 0x5f84022dee0bd36f +// Random20/0/1: trades 0x3e0974d0ca2bfa0b -> 0x936a146fbbfb7d4b +// Random20/0/0: trades 0x3e0974d0ca2bfa0b -> 0x936a146fbbfb7d4b +// Random20/1/1: trades 0x1cd39188275caa5d -> 0x57a697ba56dd611e +// Random20/1/0: trades 0x1cd39188275caa5d -> 0x57a697ba56dd611e +// Random35/0/1: trades 0x2e59ea5f57457b34 -> 0x834764512474d77c +// Random35/0/0: trades 0x2e59ea5f57457b34 -> 0x834764512474d77c +// Random35/1/1: trades 0xf9dd1b1a0c3d9f6a -> 0xce2ab725ccda6ebd +// Random35/1/0: trades 0xf9dd1b1a0c3d9f6a -> 0xce2ab725ccda6ebd +// Random38/0/1: trades 0xe38062999cae5744 -> 0xac12ee6aa53f424c +// Random38/0/0: trades 0xe38062999cae5744 -> 0xac12ee6aa53f424c +// Random38/1/1: trades 0x13e4cce8783da942 -> 0xc6a0970cf0716d7c +// Random38/1/0: trades 0x13e4cce8783da942 -> 0xc6a0970cf0716d7c +// Random46/0/1: trades 0x8fe7667934a81f87 -> 0xfaba8060a07d20fd +// Random46/0/0: trades 0x8fe7667934a81f87 -> 0xfaba8060a07d20fd +// Random46/1/1: trades 0x8fe7667934a81f87 -> 0xfaba8060a07d20fd +// Random46/1/0: trades 0x8fe7667934a81f87 -> 0xfaba8060a07d20fd constexpr Pinned kPinned[] = { {"Quiet", 0, 1, 0x1c2b2e37a44f6877ull, 0x81799b58f0e052b1ull, 0x47fe0d7eaf8e51e3ull, 0xe0aa4c0240939bcfull, 40, 40, 0}, {"Quiet", 0, 0, 0x1c2b2e37a44f6877ull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0xe0aa4c0240939bcfull, 40, 0, 0}, @@ -1216,14 +1244,14 @@ constexpr Pinned kPinned[] = { {"Random04", 0, 0, 0x0c41be29e4497a7cull, 0x14650fb0739d0383ull, 0x38df0af477021418ull, 0x542f1b01ef32d535ull, 44, 0, 18}, {"Random04", 1, 1, 0x93ade030662e0b77ull, 0x5beecc5dc910feb8ull, 0x28a43578727c43b5ull, 0x254f4f4933be69e0ull, 44, 44, 18}, {"Random04", 1, 0, 0x93ade030662e0b77ull, 0x14650fb0739d0383ull, 0x28a43578727c43b5ull, 0x254f4f4933be69e0ull, 44, 0, 18}, - {"Random05", 0, 1, 0x3e8e6495fe2e6dc1ull, 0x2703dbc59d3516a6ull, 0x3d4c3aae7bbc3b85ull, 0xb72557b7f6894eafull, 58, 53, 3}, - {"Random05", 0, 0, 0x3e8e6495fe2e6dc1ull, 0x14650fb0739d0383ull, 0x3d4c3aae7bbc3b85ull, 0xb72557b7f6894eafull, 58, 0, 3}, + {"Random05", 0, 1, 0x3e8e6495fe2e6dc1ull, 0x2703dbc59d3516a6ull, 0xf91857e72781f5aeull, 0xb72557b7f6894eafull, 58, 53, 3}, + {"Random05", 0, 0, 0x3e8e6495fe2e6dc1ull, 0x14650fb0739d0383ull, 0xf91857e72781f5aeull, 0xb72557b7f6894eafull, 58, 0, 3}, {"Random05", 1, 1, 0x4be132773d14c0e0ull, 0xa6803dead66d3343ull, 0xc83a1288cfd7ee4dull, 0xb3561169b361d8bfull, 66, 53, 10}, {"Random05", 1, 0, 0x4be132773d14c0e0ull, 0x14650fb0739d0383ull, 0xc83a1288cfd7ee4dull, 0xb3561169b361d8bfull, 66, 0, 10}, - {"Random06", 0, 1, 0xecb44da2cbdb5c42ull, 0x5485171eedfc86cfull, 0x19f4b2ac9f3f76f7ull, 0xff2efe1925e45a9dull, 46, 46, 13}, - {"Random06", 0, 0, 0xecb44da2cbdb5c42ull, 0x14650fb0739d0383ull, 0x19f4b2ac9f3f76f7ull, 0xff2efe1925e45a9dull, 46, 0, 13}, - {"Random06", 1, 1, 0x65cc03efd59ec6b5ull, 0x2dce2153546130dbull, 0x3561041c80b6994full, 0x1130940f86f108d2ull, 46, 46, 13}, - {"Random06", 1, 0, 0x65cc03efd59ec6b5ull, 0x14650fb0739d0383ull, 0x3561041c80b6994full, 0x1130940f86f108d2ull, 46, 0, 13}, + {"Random06", 0, 1, 0xecb44da2cbdb5c42ull, 0x5485171eedfc86cfull, 0x75cd705050ee6ca5ull, 0xff2efe1925e45a9dull, 46, 46, 13}, + {"Random06", 0, 0, 0xecb44da2cbdb5c42ull, 0x14650fb0739d0383ull, 0x75cd705050ee6ca5ull, 0xff2efe1925e45a9dull, 46, 0, 13}, + {"Random06", 1, 1, 0x65cc03efd59ec6b5ull, 0x2dce2153546130dbull, 0x5f84022dee0bd36full, 0x1130940f86f108d2ull, 46, 46, 13}, + {"Random06", 1, 0, 0x65cc03efd59ec6b5ull, 0x14650fb0739d0383ull, 0x5f84022dee0bd36full, 0x1130940f86f108d2ull, 46, 0, 13}, {"Random07", 0, 1, 0x7bf47df91573ab9cull, 0x3c1feb47a4d6d81eull, 0x67ce7c56c16f12a7ull, 0x438a828933e58c5bull, 46, 46, 12}, {"Random07", 0, 0, 0x7bf47df91573ab9cull, 0x14650fb0739d0383ull, 0x67ce7c56c16f12a7ull, 0x438a828933e58c5bull, 46, 0, 12}, {"Random07", 1, 1, 0x29fcc776be13bdf5ull, 0x9a5ab1b2b00efff1ull, 0x9cbc1f07c0648d08ull, 0x353c5f0e5bc1c22bull, 46, 46, 12}, @@ -1276,10 +1304,10 @@ constexpr Pinned kPinned[] = { {"Random19", 0, 0, 0x3c5cc975f928b775ull, 0x14650fb0739d0383ull, 0x95d7b888983ad8e6ull, 0x17d53da80c3a895full, 40, 0, 10}, {"Random19", 1, 1, 0x92536ad17b9fbf26ull, 0xabc10759f5e1ffa1ull, 0x011bec7774b82ec9ull, 0x168791086f51c368ull, 40, 40, 11}, {"Random19", 1, 0, 0x92536ad17b9fbf26ull, 0x14650fb0739d0383ull, 0x011bec7774b82ec9ull, 0x168791086f51c368ull, 40, 0, 11}, - {"Random20", 0, 1, 0x90b20d166e1357e0ull, 0x808745030887aa13ull, 0x3e0974d0ca2bfa0bull, 0x3968c24c22199036ull, 70, 57, 21}, - {"Random20", 0, 0, 0x90b20d166e1357e0ull, 0x14650fb0739d0383ull, 0x3e0974d0ca2bfa0bull, 0x3968c24c22199036ull, 70, 0, 21}, - {"Random20", 1, 1, 0xa82d4e53d2192a5full, 0x8e1411c055e37af3ull, 0x1cd39188275caa5dull, 0x5c29f15f02417fcaull, 72, 57, 21}, - {"Random20", 1, 0, 0xa82d4e53d2192a5full, 0x14650fb0739d0383ull, 0x1cd39188275caa5dull, 0x5c29f15f02417fcaull, 72, 0, 21}, + {"Random20", 0, 1, 0x90b20d166e1357e0ull, 0x808745030887aa13ull, 0x936a146fbbfb7d4bull, 0x3968c24c22199036ull, 70, 57, 21}, + {"Random20", 0, 0, 0x90b20d166e1357e0ull, 0x14650fb0739d0383ull, 0x936a146fbbfb7d4bull, 0x3968c24c22199036ull, 70, 0, 21}, + {"Random20", 1, 1, 0xa82d4e53d2192a5full, 0x8e1411c055e37af3ull, 0x57a697ba56dd611eull, 0x5c29f15f02417fcaull, 72, 57, 21}, + {"Random20", 1, 0, 0xa82d4e53d2192a5full, 0x14650fb0739d0383ull, 0x57a697ba56dd611eull, 0x5c29f15f02417fcaull, 72, 0, 21}, {"Random21", 0, 1, 0x18e96beb584671fcull, 0x6c5b4e5a67209f94ull, 0xbc89178a735678f6ull, 0xca719be0f46f42acull, 54, 54, 18}, {"Random21", 0, 0, 0x18e96beb584671fcull, 0x14650fb0739d0383ull, 0xbc89178a735678f6ull, 0xca719be0f46f42acull, 54, 0, 18}, {"Random21", 1, 1, 0xd32a038cc5b6ea6eull, 0x9074957bf890f8e6ull, 0x0cb28a314aa970aeull, 0xb6387745e72b0075ull, 54, 54, 18}, @@ -1336,10 +1364,10 @@ constexpr Pinned kPinned[] = { {"Random34", 0, 0, 0xea1b68237d95805full, 0x14650fb0739d0383ull, 0x81535eb933e496e7ull, 0xb03d41aea9c00c63ull, 51, 0, 1}, {"Random34", 1, 1, 0x4ecacf8ab2d85c47ull, 0x294fe821fef3729dull, 0xa788aa75c6b931e3ull, 0xe4e38b6ada294505ull, 51, 51, 2}, {"Random34", 1, 0, 0x4ecacf8ab2d85c47ull, 0x14650fb0739d0383ull, 0xa788aa75c6b931e3ull, 0xe4e38b6ada294505ull, 51, 0, 2}, - {"Random35", 0, 1, 0xeb16fb1a6bc80c17ull, 0x7265444996669f20ull, 0x2e59ea5f57457b34ull, 0xb971c6bc6d7ac182ull, 63, 63, 24}, - {"Random35", 0, 0, 0xeb16fb1a6bc80c17ull, 0x14650fb0739d0383ull, 0x2e59ea5f57457b34ull, 0xb971c6bc6d7ac182ull, 63, 0, 24}, - {"Random35", 1, 1, 0x193ec2cb9a163900ull, 0x9233c78214742aa3ull, 0xf9dd1b1a0c3d9f6aull, 0xa9bdcf22e28567b2ull, 63, 63, 24}, - {"Random35", 1, 0, 0x193ec2cb9a163900ull, 0x14650fb0739d0383ull, 0xf9dd1b1a0c3d9f6aull, 0xa9bdcf22e28567b2ull, 63, 0, 24}, + {"Random35", 0, 1, 0xeb16fb1a6bc80c17ull, 0x7265444996669f20ull, 0x834764512474d77cull, 0xb971c6bc6d7ac182ull, 63, 63, 24}, + {"Random35", 0, 0, 0xeb16fb1a6bc80c17ull, 0x14650fb0739d0383ull, 0x834764512474d77cull, 0xb971c6bc6d7ac182ull, 63, 0, 24}, + {"Random35", 1, 1, 0x193ec2cb9a163900ull, 0x9233c78214742aa3ull, 0xce2ab725ccda6ebdull, 0xa9bdcf22e28567b2ull, 63, 63, 24}, + {"Random35", 1, 0, 0x193ec2cb9a163900ull, 0x14650fb0739d0383ull, 0xce2ab725ccda6ebdull, 0xa9bdcf22e28567b2ull, 63, 0, 24}, {"Random36", 0, 1, 0xd4b5588948d4b755ull, 0x6af2dc7dd208ef5dull, 0x71b837070cbf3386ull, 0x1980e25b047dea78ull, 87, 60, 21}, {"Random36", 0, 0, 0xd4b5588948d4b755ull, 0x14650fb0739d0383ull, 0x71b837070cbf3386ull, 0x1980e25b047dea78ull, 87, 0, 21}, {"Random36", 1, 1, 0xd1532dd9d52ae5f4ull, 0x0e1d68d8d6c4c5e2ull, 0xc91ddda9c3e46569ull, 0xe93d9e6c724efef6ull, 87, 60, 21}, @@ -1348,10 +1376,10 @@ constexpr Pinned kPinned[] = { {"Random37", 0, 0, 0x3a89ff92c403a3e7ull, 0x14650fb0739d0383ull, 0x52bf39ca2f2765c6ull, 0xff9c74898bf3a72aull, 71, 0, 18}, {"Random37", 1, 1, 0x8a36a09ee42999ecull, 0xf12dd4800e96f492ull, 0x41de938f8a060981ull, 0x9f14bee9808ee8baull, 70, 45, 17}, {"Random37", 1, 0, 0x8a36a09ee42999ecull, 0x14650fb0739d0383ull, 0x41de938f8a060981ull, 0x9f14bee9808ee8baull, 70, 0, 17}, - {"Random38", 0, 1, 0x36ed3eee0da888c2ull, 0x2bf58dbe1c5bd9acull, 0xe38062999cae5744ull, 0x5b82e26291364fc0ull, 54, 54, 25}, - {"Random38", 0, 0, 0x36ed3eee0da888c2ull, 0x14650fb0739d0383ull, 0xe38062999cae5744ull, 0x5b82e26291364fc0ull, 54, 0, 25}, - {"Random38", 1, 1, 0xa3225feaf19b98feull, 0x59c34402fcf68e2aull, 0x13e4cce8783da942ull, 0x135197c1ef34cbbdull, 54, 54, 25}, - {"Random38", 1, 0, 0xa3225feaf19b98feull, 0x14650fb0739d0383ull, 0x13e4cce8783da942ull, 0x135197c1ef34cbbdull, 54, 0, 25}, + {"Random38", 0, 1, 0x36ed3eee0da888c2ull, 0x2bf58dbe1c5bd9acull, 0xac12ee6aa53f424cull, 0x5b82e26291364fc0ull, 54, 54, 25}, + {"Random38", 0, 0, 0x36ed3eee0da888c2ull, 0x14650fb0739d0383ull, 0xac12ee6aa53f424cull, 0x5b82e26291364fc0ull, 54, 0, 25}, + {"Random38", 1, 1, 0xa3225feaf19b98feull, 0x59c34402fcf68e2aull, 0xc6a0970cf0716d7cull, 0x135197c1ef34cbbdull, 54, 54, 25}, + {"Random38", 1, 0, 0xa3225feaf19b98feull, 0x14650fb0739d0383ull, 0xc6a0970cf0716d7cull, 0x135197c1ef34cbbdull, 54, 0, 25}, {"Random39", 0, 1, 0x9bcdaebb98702055ull, 0xf4194fbb3f8e63f4ull, 0xc193386eb35a8e65ull, 0x3026f01eb73dfcdaull, 44, 44, 19}, {"Random39", 0, 0, 0x9bcdaebb98702055ull, 0x14650fb0739d0383ull, 0xc193386eb35a8e65ull, 0x3026f01eb73dfcdaull, 44, 0, 19}, {"Random39", 1, 1, 0x5fa98522adc27aafull, 0x50b22f98ba965993ull, 0xeea051f0749b0428ull, 0x123f0c5b09aeae91ull, 44, 44, 19}, @@ -1380,10 +1408,10 @@ constexpr Pinned kPinned[] = { {"Random45", 0, 0, 0xbf351c50f4a44f8full, 0x14650fb0739d0383ull, 0xf7b0f07cda17c110ull, 0x7f2867c87c78723eull, 99, 0, 25}, {"Random45", 1, 1, 0x32a1d85e85b93f0aull, 0xebcfcdf78cd848a5ull, 0x797aff1ec9239dffull, 0x56b0e295081820c0ull, 99, 63, 24}, {"Random45", 1, 0, 0x32a1d85e85b93f0aull, 0x14650fb0739d0383ull, 0x797aff1ec9239dffull, 0x56b0e295081820c0ull, 99, 0, 24}, - {"Random46", 0, 1, 0x3ad713968f2546d9ull, 0x84dd0930114cd829ull, 0x8fe7667934a81f87ull, 0x6bb382cfb976e148ull, 52, 52, 1}, - {"Random46", 0, 0, 0x3ad713968f2546d9ull, 0x14650fb0739d0383ull, 0x8fe7667934a81f87ull, 0x6bb382cfb976e148ull, 52, 0, 1}, - {"Random46", 1, 1, 0x997f2e0ee9786d91ull, 0x1ce86cd2c755bea3ull, 0x8fe7667934a81f87ull, 0x556134f8585397c9ull, 52, 52, 1}, - {"Random46", 1, 0, 0x997f2e0ee9786d91ull, 0x14650fb0739d0383ull, 0x8fe7667934a81f87ull, 0x556134f8585397c9ull, 52, 0, 1}, + {"Random46", 0, 1, 0x3ad713968f2546d9ull, 0x84dd0930114cd829ull, 0xfaba8060a07d20fdull, 0x6bb382cfb976e148ull, 52, 52, 1}, + {"Random46", 0, 0, 0x3ad713968f2546d9ull, 0x14650fb0739d0383ull, 0xfaba8060a07d20fdull, 0x6bb382cfb976e148ull, 52, 0, 1}, + {"Random46", 1, 1, 0x997f2e0ee9786d91ull, 0x1ce86cd2c755bea3ull, 0xfaba8060a07d20fdull, 0x556134f8585397c9ull, 52, 52, 1}, + {"Random46", 1, 0, 0x997f2e0ee9786d91ull, 0x14650fb0739d0383ull, 0xfaba8060a07d20fdull, 0x556134f8585397c9ull, 52, 0, 1}, {"Random47", 0, 1, 0x8ce025f147bbdb2dull, 0x74895c0d69d6b4a8ull, 0x072cedccf12d6a31ull, 0x31ec37588708d5f1ull, 57, 57, 13}, {"Random47", 0, 0, 0x8ce025f147bbdb2dull, 0x14650fb0739d0383ull, 0x072cedccf12d6a31ull, 0x31ec37588708d5f1ull, 57, 0, 13}, {"Random47", 1, 1, 0xebf0544eb982483aull, 0xca410d7365dca790ull, 0x33811b67f07874f3ull, 0xa8aff582382caf48ull, 57, 57, 13}, diff --git a/tests/test_cash_fee_sizing_tapes.cpp b/tests/test_cash_fee_sizing_tapes.cpp new file mode 100644 index 00000000..822d23b4 --- /dev/null +++ b/tests/test_cash_fee_sizing_tapes.cpp @@ -0,0 +1,516 @@ +/* + * test_cash_fee_sizing_tapes.cpp — R5 lane PAR-CASHFEE. + * + * TradingView sizes a percent-of-equity DEFAULT quantity under a cash + * commission from strategy.equity -- every open entry fee charged, the cash + * ones included -- and leaves out of that money the fee the order itself pays: + * + * cash_per_order units = floor((pct * E - fee) / (price * pointvalue)) + * cash_per_contract units = floor(pct * E / (price * pointvalue + fee)) + * + * on the symbol's quantity grid, `price` being the price the quantity is sized + * at: the signal close for a market strategy.entry and for strategy.order, the + * level for a priced entry. Before this lane the Pine adapter took the + * percentage of an equity that restored the open entries' cash fees and + * reserved none for the order (default_sizing_cash), so every sized quantity + * came out too large. + * + * The tapes are `lab tv` exports (channel ws-report-v1, rangeProof covered) + * under tests/fixtures/cash_fee_sizing, whose README.md lists them; the bars are + * the scenario windows of their feeds (fixtures/cash_fee_sizing/bars.inc). The + * host below runs each tape's script through the Pine adapter. + * + * 1. Eleven tapes the adapter books trade for trade: percentages 50, 100 and + * 200, long and short, flat, pyramided (the open lots' fee shares charged), + * after a partial close and reversing (one fee for the reversing order), + * priced entries and strategy.order, point values 1 (BINANCE:ETHUSDT.P) and + * 50 (CME_MINI:ES1!), and lane H-THIN's FIFO fee-basis tape. Every trade's + * entry id, side, times, prices and quantity equal the tape's, its + * commission to 1e-9 and its net profit to 1e-7 of |profit| + |commission| + * (the tape's rounding of a million-dollar profit). 111 trades. + * 2. Two tapes at default 100 % with margin 100 / 100 (pcf-*-p100-m100): the + * quantity the adapter sizes is TradingView's (the first scenario books + * TradingView's trade; the second scenario's quantity is the sum of + * TradingView's pieces), but TradingView then margin-calls a sliver of that + * long on its own entry bar, fired by the order's cash fee, and the adapter + * does not (its margin equity restores a cash entry fee, + * NativeMarginEquityBasis::MarkedEquityBeforeOpenCommission). This is a + * RECORDED divergence, not a sizing question: the adapter's rows are pinned + * exactly, so the tapes fail loudly the day the margin equity changes and + * must be re-pinned deliberately. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; + +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +#ifndef PINEFORGE_CASH_FEE_FIXTURE_DIR +#error "PINEFORGE_CASH_FEE_FIXTURE_DIR must name tests/fixtures/cash_fee_sizing" +#endif + +namespace { + +constexpr double kNaN = std::numeric_limits::quiet_NaN(); + +struct FeedBar { + std::int64_t ts; + double open, high, low, close; +}; + +#include "fixtures/cash_fee_sizing/bars.inc" + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +std::int64_t utc_ms(int month, int day, int hour, int minute) { + const std::int64_t days = days_from_civil(2025, static_cast(month), + static_cast(day)); + return ((days * 24 + hour) * 60 + minute) * 60'000; +} + +// ── the scripts ────────────────────────────────────────────────────── + +// One statement of a tape's script, issued on the bar that OPENS at the +// step's UTC time (the script's `at(...)` guard). +enum class Op { + Long, Short, // strategy.entry(id, dir), default quantity + LongQty1, // strategy.entry(id, long, qty=1) + LimitLongBelow3, // strategy.entry(id, long, limit=close - 3) + LimitShortAbove3, // strategy.entry(id, short, limit=close + 3) + StopLongAbove3, // strategy.entry(id, long, stop=close + 3) + StopShortBelow3, // strategy.entry(id, short, stop=close - 3) + OrderLong, OrderShort, // strategy.order(id, dir), default quantity + CloseHalf, // strategy.close(id, qty_percent=50) + ExitLimitQtyPositionLess1, // strategy.exit(id, from, qty=position-1, limit=close*0.9) + ExitLimit, // strategy.exit(id, from, limit=close*0.9) + CloseAll, // if strategy.position_size != 0: strategy.close_all() +}; + +struct Step { + int month, day, hour, minute; + Op op; + const char* id; + const char* from; +}; + +std::vector pyramid_steps() { + return { + {4, 2, 10, 0, Op::Long, "A1", ""}, {4, 2, 10, 30, Op::Long, "A2", ""}, + {4, 2, 11, 0, Op::Long, "A3", ""}, {4, 2, 12, 0, Op::CloseAll, "", ""}, + {4, 4, 10, 0, Op::Short, "B1", ""}, {4, 4, 10, 30, Op::Short, "B2", ""}, + {4, 4, 11, 0, Op::Short, "B3", ""}, {4, 4, 12, 0, Op::CloseAll, "", ""}, + {4, 8, 10, 0, Op::Long, "C1", ""}, {4, 8, 10, 30, Op::CloseHalf, "C1", ""}, + {4, 8, 11, 0, Op::Long, "C2", ""}, {4, 8, 12, 0, Op::CloseAll, "", ""}, + {4, 10, 10, 0, Op::Long, "D1", ""}, {4, 10, 10, 30, Op::Short, "D2", ""}, + {4, 10, 12, 0, Op::CloseAll, "", ""}, + {4, 15, 10, 0, Op::Short, "E1", ""}, {4, 15, 10, 30, Op::Long, "E2", ""}, + {4, 15, 12, 0, Op::CloseAll, "", ""}, + {4, 17, 10, 0, Op::Long, "F1", ""}, {4, 17, 12, 0, Op::CloseAll, "", ""}, + {4, 22, 10, 0, Op::Short, "G1", ""}, {4, 22, 12, 0, Op::CloseAll, "", ""}, + }; +} + +std::vector leverage_steps() { + return { + {4, 17, 10, 0, Op::Long, "F1", ""}, {4, 17, 12, 0, Op::CloseAll, "", ""}, + {4, 22, 10, 0, Op::Short, "G1", ""}, {4, 22, 12, 0, Op::CloseAll, "", ""}, + {4, 10, 10, 0, Op::Long, "D1", ""}, {4, 10, 10, 30, Op::Short, "D2", ""}, + {4, 10, 12, 0, Op::CloseAll, "", ""}, + {4, 15, 10, 0, Op::Short, "E1", ""}, {4, 15, 10, 30, Op::Long, "E2", ""}, + {4, 15, 12, 0, Op::CloseAll, "", ""}, + }; +} + +std::vector es_steps() { + return { + {6, 2, 14, 0, Op::Long, "A1", ""}, {6, 2, 14, 30, Op::Long, "A2", ""}, + {6, 2, 16, 0, Op::CloseAll, "", ""}, + {6, 3, 14, 0, Op::Short, "B1", ""}, {6, 3, 14, 30, Op::Short, "B2", ""}, + {6, 3, 16, 0, Op::CloseAll, "", ""}, + {6, 4, 14, 0, Op::Long, "D1", ""}, {6, 4, 14, 30, Op::Short, "D2", ""}, + {6, 4, 16, 0, Op::CloseAll, "", ""}, + {6, 5, 14, 0, Op::Long, "F1", ""}, {6, 5, 16, 0, Op::CloseAll, "", ""}, + }; +} + +std::vector priced_steps() { + return { + {4, 23, 10, 0, Op::OrderLong, "O1", ""}, {4, 23, 10, 30, Op::OrderLong, "O2", ""}, + {4, 23, 12, 0, Op::CloseAll, "", ""}, + {4, 24, 10, 0, Op::LimitShortAbove3, "M1", ""}, {4, 24, 12, 0, Op::CloseAll, "", ""}, + {4, 25, 10, 0, Op::StopLongAbove3, "N1", ""}, {4, 25, 12, 0, Op::CloseAll, "", ""}, + {4, 28, 10, 0, Op::StopShortBelow3, "V1", ""}, {4, 28, 12, 0, Op::CloseAll, "", ""}, + {4, 29, 10, 0, Op::LimitLongBelow3, "K1", ""}, {4, 29, 12, 0, Op::CloseAll, "", ""}, + {4, 30, 10, 0, Op::OrderShort, "W1", ""}, {4, 30, 12, 0, Op::CloseAll, "", ""}, + }; +} + +std::vector margin_steps() { + return { + {4, 2, 10, 0, Op::Long, "P1", ""}, {4, 2, 12, 0, Op::CloseAll, "", ""}, + {4, 3, 10, 0, Op::Long, "Q1", ""}, {4, 3, 12, 0, Op::CloseAll, "", ""}, + {4, 4, 10, 0, Op::Short, "R1", ""}, {4, 4, 12, 0, Op::CloseAll, "", ""}, + {4, 10, 10, 0, Op::Long, "T1", ""}, {4, 10, 10, 30, Op::Short, "T2", ""}, + {4, 10, 12, 0, Op::CloseAll, "", ""}, + {4, 12, 10, 0, Op::Long, "U1", ""}, {4, 12, 12, 0, Op::CloseAll, "", ""}, + }; +} + +// Lane H-THIN's X15(b) tape: a FIFO strategy.exit(from_entry) closes an older +// lot of another id before the next default entry sizes. +std::vector fifo_steps() { + return { + {4, 8, 10, 0, Op::LongQty1, "L2A", ""}, {4, 8, 10, 30, Op::Long, "LA", ""}, + {4, 8, 11, 0, Op::ExitLimitQtyPositionLess1, "XA", "LA"}, + {4, 8, 11, 30, Op::Long, "NA", ""}, {4, 8, 12, 30, Op::CloseAll, "", ""}, + {4, 10, 10, 0, Op::LongQty1, "L2B", ""}, {4, 10, 10, 30, Op::Long, "LB", ""}, + {4, 10, 11, 0, Op::ExitLimit, "XB", "LB"}, + {4, 10, 11, 30, Op::Long, "NB", ""}, {4, 10, 12, 30, Op::CloseAll, "", ""}, + {4, 14, 10, 0, Op::Long, "LC", ""}, {4, 14, 10, 30, Op::LongQty1, "L2C", ""}, + {4, 14, 11, 0, Op::ExitLimit, "XC", "LC"}, + {4, 14, 11, 30, Op::Long, "NC", ""}, {4, 14, 12, 30, Op::CloseAll, "", ""}, + }; +} + +enum class Market { Eth, Es }; + +struct Tape { + const char* slug; + CommissionType commission; + double commission_value; + double percent; + int pyramiding; + double margin; // margin_long == margin_short + Market market; + double capital; + std::vector steps; +}; + +// ── the host, running a tape's script through the Pine adapter ─────── + +class TapeHost final : public source::PineStrategyHost { +public: + explicit TapeHost(const Tape& tape) : tape_(tape) { + source::PineStrategyConfig config; + config.initial_capital = tape.capital; + config.default_qty_type = static_cast(QtyType::PERCENT_OF_EQUITY); + config.default_qty_value = tape.percent; + config.pyramiding = tape.pyramiding; + config.process_orders_on_close = false; + config.commission_type = static_cast(tape.commission); + config.commission_value = tape.commission_value; + config.slippage = 0; + config.margin_long = tape.margin; + config.margin_short = tape.margin; + configure_pine_strategy(config); + if (tape.market == Market::Es) { + set_syminfo_mintick(0.25); + set_syminfo_pointvalue(50.0); + set_syminfo_metadata("qty_step", 1.0); + } else { + set_syminfo_mintick(0.01); + set_syminfo_metadata("qty_step", 0.0001); + } + for (const auto& step : tape.steps) + step_ms_.push_back(utc_ms(step.month, step.day, step.hour, step.minute)); + } + + void on_source_bar(const Bar& bar) override { + const double position = live_position_size(); + for (std::size_t i = 0; i < tape_.steps.size(); ++i) { + if (step_ms_[i] != current_bar_.timestamp) continue; + const Step& s = tape_.steps[i]; + switch (s.op) { + case Op::Long: strategy_entry(s.id, true); break; + case Op::Short: strategy_entry(s.id, false); break; + case Op::LongQty1: strategy_entry(s.id, true, kNaN, kNaN, 1.0); break; + case Op::LimitLongBelow3: strategy_entry(s.id, true, bar.close - 3.0); break; + case Op::LimitShortAbove3: strategy_entry(s.id, false, bar.close + 3.0); break; + case Op::StopLongAbove3: strategy_entry(s.id, true, kNaN, bar.close + 3.0); break; + case Op::StopShortBelow3: strategy_entry(s.id, false, kNaN, bar.close - 3.0); break; + case Op::OrderLong: strategy_order(s.id, true, kNaN); break; + case Op::OrderShort: strategy_order(s.id, false, kNaN); break; + case Op::CloseHalf: strategy_close(s.id, "", kNaN, 50.0); break; + case Op::ExitLimitQtyPositionLess1: + strategy_exit(s.id, s.from, bar.close * 0.9, kNaN, kNaN, kNaN, kNaN, 100.0, "", + position - 1.0); + break; + case Op::ExitLimit: strategy_exit(s.id, s.from, bar.close * 0.9, kNaN); break; + case Op::CloseAll: + if (position != 0.0) strategy_close_all(); + break; + } + } + } + +private: + const Tape& tape_; + std::vector step_ms_; +}; + +std::vector replay(const Tape& tape) { + const FeedBar* rows = tape.market == Market::Es ? kEsBars : kEthBars; + const std::size_t count = tape.market == Market::Es ? sizeof(kEsBars) / sizeof(kEsBars[0]) + : sizeof(kEthBars) / sizeof(kEthBars[0]); + std::vector bars; + for (std::size_t i = 0; i < count; ++i) + bars.push_back({rows[i].open, rows[i].high, rows[i].low, rows[i].close, 0.0, rows[i].ts}); + TapeHost host(tape); + host.run(bars.data(), static_cast(bars.size()), "15", "15"); + CHECK(host.last_error().empty()); + std::vector trades; + for (int i = 0; i < host.trade_count(); ++i) trades.push_back(host.get_trade(i)); + return trades; +} + +// ── the tape ───────────────────────────────────────────────────────── + +struct TapeTrade { + std::string signal; // the entry's id + bool is_long = true; + std::int64_t entry_ms = 0; + double entry_price = kNaN; + std::int64_t exit_ms = 0; + double exit_price = kNaN; + std::string exit_signal; + double qty = kNaN; + double net_profit = kNaN; + double commission = kNaN; +}; + +// "YYYY-MM-DD HH:MM" in the tape's UTC+8 -> UTC milliseconds. +std::int64_t tape_ms(const std::string& text) { + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + if (std::sscanf(text.c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) return -1; + const std::int64_t days = days_from_civil(y, static_cast(mo), + static_cast(d)); + return ((days * 24 + h - 8) * 60 + mi) * 60'000; +} + +// Columns: Trade number, Type, Date and time, Signal, Price, Size (qty), +// Size (value), Net PnL, Return %, Commission, ... +std::vector read_tape(const std::string& slug) { + std::ifstream in(std::string(PINEFORGE_CASH_FEE_FIXTURE_DIR) + "/" + slug + "/tv_trades.csv"); + std::vector trades; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 10) continue; + const std::size_t n = static_cast(std::stoi(cell[0])); + if (trades.size() < n) trades.resize(n); + TapeTrade& t = trades[n - 1]; + if (cell[1].rfind("Entry", 0) == 0) { + t.signal = cell[3]; + t.is_long = cell[1] == "Entry long"; + t.entry_ms = tape_ms(cell[2]); + t.entry_price = std::stod(cell[4]); + t.qty = std::stod(cell[5]); + t.net_profit = std::stod(cell[7]); + t.commission = std::stod(cell[9]); + } else { + t.exit_ms = tape_ms(cell[2]); + t.exit_price = std::stod(cell[4]); + t.exit_signal = cell[3]; + } + } + return trades; +} + +bool same(double a, double b, double tolerance = 1e-9) { return std::fabs(a - b) <= tolerance; } + +// Everything the tape states about a trade, at the tape's own precision. +bool books_tape_trade(const Trade& trade, const TapeTrade& tape) { + return trade.entry_id == tape.signal && trade.is_long == tape.is_long + && trade.entry_time == tape.entry_ms && trade.exit_time == tape.exit_ms + && same(trade.entry_price, tape.entry_price) && same(trade.exit_price, tape.exit_price) + && same(trade.qty, tape.qty) + && same(trade.commission, tape.commission, 1e-9 * std::fmax(1.0, std::fabs(tape.commission))) + && same(trade.pnl, tape.net_profit, + 1e-7 * (std::fabs(tape.net_profit) + std::fabs(tape.commission)) + 1e-6); +} + +void print_mismatch(const char* slug, std::size_t i, const Trade& e, const TapeTrade& t) { + std::printf(" %s #%zu engine %s %s %lld..%lld %.10g @%.2f->%.2f pnl %.6f comm %.9f\n" + " %s #%zu tape %s %s %lld..%lld %.10g @%.2f->%.2f pnl %.6f comm %.9f\n", + slug, i + 1, e.entry_id.c_str(), e.is_long ? "L" : "S", + static_cast(e.entry_time), static_cast(e.exit_time), e.qty, + e.entry_price, e.exit_price, e.pnl, e.commission, slug, i + 1, t.signal.c_str(), + t.is_long ? "L" : "S", static_cast(t.entry_ms), + static_cast(t.exit_ms), t.qty, t.entry_price, t.exit_price, + t.net_profit, t.commission); +} + +// ── 1. the tapes the adapter books trade for trade ─────────────────── + +std::vector exact_tapes() { + const auto order = CommissionType::CASH_PER_ORDER; + const auto contract = CommissionType::CASH_PER_CONTRACT; + return { + {"pcf-order-p50-m25", order, 1000.0, 50.0, 3, 25.0, Market::Eth, 100000.0, pyramid_steps()}, + {"pcf-order-p100-m25", order, 1000.0, 100.0, 3, 25.0, Market::Eth, 100000.0, pyramid_steps()}, + {"pcf-contract-p50-m25", contract, 20.0, 50.0, 3, 25.0, Market::Eth, 100000.0, + pyramid_steps()}, + {"pcf-contract-p100-m25", contract, 20.0, 100.0, 3, 25.0, Market::Eth, 100000.0, + pyramid_steps()}, + {"pcf-order-p200-m25", order, 1000.0, 200.0, 1, 25.0, Market::Eth, 100000.0, + leverage_steps()}, + {"pcf-contract-p200-m25", contract, 20.0, 200.0, 1, 25.0, Market::Eth, 100000.0, + leverage_steps()}, + {"pcf-es-order-p50-m25", order, 1000000.0, 50.0, 2, 25.0, Market::Es, 1e9, es_steps()}, + {"pcf-es-contract-p50-m25", contract, 1000.0, 50.0, 2, 25.0, Market::Es, 1e9, es_steps()}, + {"pcf-order-priced-m25", order, 1000.0, 50.0, 1, 25.0, Market::Eth, 100000.0, + priced_steps()}, + {"pcf-contract-priced-m25", contract, 20.0, 50.0, 1, 25.0, Market::Eth, 100000.0, + priced_steps()}, + {"hthin-x15b-fifo-fee-basis", order, 1000.0, 50.0, 5, 100.0, Market::Eth, 100000.0, + fifo_steps()}, + }; +} + +void test_tapes_book_tradingview_trades() { + int booked = 0; + int total = 0; + for (const auto& tape : exact_tapes()) { + const auto engine = replay(tape); + const auto tv = read_tape(tape.slug); + CHECK(!tv.empty()); + CHECK(engine.size() == tv.size()); + for (std::size_t i = 0; i < tv.size() && i < engine.size(); ++i) { + const bool ok = books_tape_trade(engine[i], tv[i]); + CHECK(ok); + if (!ok) print_mismatch(tape.slug, i, engine[i], tv[i]); + booked += ok; + } + total += static_cast(tv.size()); + } + std::printf(" eleven tapes: %d of %d trades booked as TradingView books them\n", booked, total); + CHECK(total == 111); + CHECK(booked == total); +} + +// ── 2. margin 100: the sizing agrees, the entry-bar margin call does not ── + +struct PinnedRow { + const char* entry_id; + const char* exit_id; + bool is_long; + std::int64_t entry_ms; + double entry_price; + std::int64_t exit_ms; + double exit_price; + double qty; +}; + +// The adapter's rows on each margin-100 tape, pinned (a RECORDED divergence). +const PinnedRow kOrderMarginRows[] = { + {"P1", "__close__", true, 1743588900000LL, 1880.02, 1743596100000LL, 1865.26, 52.6587}, + {"Q1", "__close__", true, 1743675300000LL, 1811.58, 1743682500000LL, 1797.78, 53.1156}, + {"R1", "__close__", false, 1743761700000LL, 1812.69, 1743768900000LL, 1780.11, 51.5748}, + {"T1", "T2", true, 1744280100000LL, 1601.17, 1744281900000LL, 1590.36, 58.1887}, + {"T2", "__margin_call__", false, 1744281900000LL, 1590.36, 1744281900000LL, 1594.81, 0.0372}, + {"T2", "__margin_call__", false, 1744281900000LL, 1590.36, 1744282800000LL, 1594.00, 2.1648}, + {"T2", "__margin_call__", false, 1744281900000LL, 1590.36, 1744286400000LL, 1621.14, 3.542}, + {"T2", "__close__", false, 1744281900000LL, 1590.36, 1744287300000LL, 1595.90, 51.8159}, + {"U1", "__close__", true, 1744452900000LL, 1592.65, 1744460100000LL, 1596.00, 54.084}, +}; +const PinnedRow kContractMarginRows[] = { + {"P1", "__close__", true, 1743588900000LL, 1880.02, 1743596100000LL, 1865.26, 52.6307}, + {"Q1", "__close__", true, 1743675300000LL, 1811.58, 1743682500000LL, 1797.78, 53.0244}, + {"R1", "__close__", false, 1743761700000LL, 1812.69, 1743768900000LL, 1780.11, 51.4351}, + {"T1", "T2", true, 1744280100000LL, 1601.17, 1744281900000LL, 1590.36, 57.911}, + {"T2", "__margin_call__", false, 1744281900000LL, 1590.36, 1744281900000LL, 1590.36, 0.0356}, + {"T2", "__margin_call__", false, 1744281900000LL, 1590.36, 1744281900000LL, 1594.81, 1.1732}, + {"T2", "__margin_call__", false, 1744281900000LL, 1590.36, 1744286400000LL, 1621.14, 3.9272}, + {"T2", "__close__", false, 1744281900000LL, 1590.36, 1744287300000LL, 1595.90, 52.0557}, + {"U1", "__close__", true, 1744452900000LL, 1592.65, 1744460100000LL, 1596.00, 54.7167}, +}; + +void check_margin_tape(const Tape& tape, const PinnedRow* pinned, std::size_t pinned_count) { + const auto engine = replay(tape); + const auto tv = read_tape(tape.slug); + CHECK(tv.size() >= 3); + CHECK(engine.size() == pinned_count); + if (tv.size() < 3 || engine.empty()) return; + // P1: the first scenario sizes on the initial capital; TradingView's trade. + CHECK(books_tape_trade(engine[0], tv[0])); + // Q1: TradingView fills the quantity the adapter sizes, then margin-calls + // a sliver of it at the fill bar's open (the order's own cash fee took the + // equity under the 100 % requirement); the adapter's margin equity restores + // that fee and books none. + double tv_q1 = 0.0; + int tv_q1_margin_calls = 0; + for (const auto& t : tv) { + if (t.signal != "Q1") continue; + tv_q1 += t.qty; + if (t.exit_signal == "Margin call" && t.exit_ms == t.entry_ms) ++tv_q1_margin_calls; + } + CHECK(engine.size() > 1 && engine[1].entry_id == "Q1" && same(engine[1].qty, tv_q1)); + CHECK(tv_q1_margin_calls >= 1); + CHECK(engine.size() > 1 && engine[1].exit_id == "__close__"); + // The adapter's rows, pinned: the day its margin equity charges the cash + // entry fee they move, and this tape is re-pinned deliberately. + for (std::size_t i = 0; i < pinned_count && i < engine.size(); ++i) { + const auto& e = engine[i]; + const auto& p = pinned[i]; + const bool ok = e.entry_id == p.entry_id && e.exit_id == p.exit_id + && e.is_long == p.is_long && e.entry_time == p.entry_ms && e.exit_time == p.exit_ms + && same(e.entry_price, p.entry_price) && same(e.exit_price, p.exit_price) + && same(e.qty, p.qty); + CHECK(ok); + if (!ok) print_mismatch(tape.slug, i, e, tv[std::min(i, tv.size() - 1)]); + } +} + +void test_margin_100_tapes_are_recorded_divergences() { + const Tape order{"pcf-order-p100-m100", CommissionType::CASH_PER_ORDER, 1000.0, 100.0, 1, + 100.0, Market::Eth, 100000.0, margin_steps()}; + const Tape contract{"pcf-contract-p100-m100", CommissionType::CASH_PER_CONTRACT, 20.0, + 100.0, 1, 100.0, Market::Eth, 100000.0, margin_steps()}; + check_margin_tape(order, kOrderMarginRows, sizeof(kOrderMarginRows) / sizeof(kOrderMarginRows[0])); + check_margin_tape(contract, kContractMarginRows, + sizeof(kContractMarginRows) / sizeof(kContractMarginRows[0])); +} + +} // namespace + +int main() { + test_tapes_book_tradingview_trades(); + test_margin_100_tapes_are_recorded_divergences(); + std::printf("PAR-CASHFEE cash-fee sizing tapes: %d checks, %d failures\n", + tests_passed + tests_failed, tests_failed); + return tests_failed == 0 ? 0 : 1; +} diff --git a/tests/test_l4b_policy_regressions.cpp b/tests/test_l4b_policy_regressions.cpp index 0db43177..525411b5 100644 --- a/tests/test_l4b_policy_regressions.cpp +++ b/tests/test_l4b_policy_regressions.cpp @@ -276,10 +276,17 @@ class CashCommissionSizing final : public source::PineNativeHost { } }; -void cash_entry_fee_is_not_subtracted_from_percent_sizing_equity() { +// expectation corrected (R5 lane PAR-CASHFEE): 20.0 -> 19.79, and the row's +// name with it (was cash_entry_fee_is_not_subtracted_from_percent_sizing_equity). +// TradingView sizes a percent-of-equity default quantity under a cash commission +// from strategy.equity, the open entries' cash fees charged, less the fee its +// own order pays (`lab tv` tapes, tests/fixtures/cash_fee_sizing): the first +// entry is (10 % of 10 000 - 10) / 100 = 9.9, the second, with the first's fee +// charged, (10 % of 9 990 - 10) / 100 = 9.89. +void cash_entry_fee_is_charged_and_reserved_by_percent_sizing() { CashCommissionSizing host; run(host, {bar(1'000), bar(2'000), bar(3'000), bar(4'000)}, symbol(0.01)); - CHECK(near(host.live_position_size(), 20.0)); + CHECK(near(host.live_position_size(), 19.79)); } class MagnifierCapProbe final : public source::PineNativeHost { @@ -376,7 +383,7 @@ int main() { blocked_direction_waits_for_its_fill(); maximum_position_size_is_live_and_inclusive(); deferred_percent_close_resolves_the_grown_cohort_at_fill(); - cash_entry_fee_is_not_subtracted_from_percent_sizing_equity(); + cash_entry_fee_is_charged_and_reserved_by_percent_sizing(); volume_weighted_cap_is_provider_supplied(); empty_close_drop_changes_the_hashed_adapter_receipts(); f8_sequential_transaction_bits_are_carried_by_the_native_route(); diff --git a/tests/test_publication_witness.cpp b/tests/test_publication_witness.cpp index bbe508ce..678d76ba 100644 --- a/tests/test_publication_witness.cpp +++ b/tests/test_publication_witness.cpp @@ -85,6 +85,20 @@ // books a short limit's call at its fill bar's high // (tests/fixtures/margin_entry_bar/pm2-m7-slim-*, pm2-m7-s1lim-*). The other 155 // runs keep their digest and every count. +// Re-harvested once more for R5 lane PAR-CASHFEE's hash step: a +// percent-of-equity default quantity under a cash commission takes its +// percentage of strategy.equity, the open entries' cash fees charged, and +// leaves out the fee its own order pays (tests/fixtures/cash_fee_sizing), and +// the source layer's fold folds the sizing snapshot's strategy.equity wherever +// such a quantity is recorded. 34 of the 156 runs pin a new digest: exactly +// the Config runs whose configuration sweep passes through a percent-of-equity +// default quantity with a positive cash fee (counted over the battery by an +// instrumented copy of this TU); six of them close a different number of +// trades (Config03 50 -> 48, Config03M 48 -> 44, ConfigFlags103 65 -> 63, +// ConfigFlags103M 66 -> 65, ConfigFlags104 and ConfigFlags104M 12 -> 13). The +// same harvest against the base 09f0bbda reproduces every old row. (The +// counts are the lane's tree's; INT26 pins the integrated tree's trades with +// this pick and moves the digests in its v19 hash re-pin.) #include #include diff --git a/tests/test_publication_witness_pinned.inc b/tests/test_publication_witness_pinned.inc index c5b248ea..6892794d 100644 --- a/tests/test_publication_witness_pinned.inc +++ b/tests/test_publication_witness_pinned.inc @@ -45,6 +45,15 @@ // Storm19M: trades 22 -> 23 // Storm23: trades 27 -> 26 // Storm23M: trades 27 -> 26 +// R5 lane PAR-CASHFEE moves the closed trades of five runs (INT26: the behaviour +// half of its hash step 214c082c, harvested on the integrated tree, each value +// equal to the lane's; its Config03 50 -> 48 was already pinned with R5 lane +// PAR-MARGIN-2); every digest moves once, in the INT26 v19 hash re-pin: +// ConfigFlags103: trades 65 -> 63 +// Config03M: trades 48 -> 44 +// ConfigFlags103M: trades 66 -> 65 +// ConfigFlags104: trades 12 -> 13 +// ConfigFlags104M: trades 12 -> 13 constexpr Pinned kPinned[] = { {"Config01", 0x13599d44631228e2ull, 144, 44, 144}, {"ConfigFlags101", 0x9d75aa3c41307c6aull, 145, 45, 144}, @@ -55,13 +64,13 @@ constexpr Pinned kPinned[] = { {"Config02M", 0xd04764a2d7139486ull, 48, 11, 48}, {"ConfigFlags102M", 0x854413a4a59d0d33ull, 144, 52, 144}, {"Config03", 0x461a919ebdcd41e9ull, 144, 48, 144}, - {"ConfigFlags103", 0xcf60de51b3acfcc3ull, 184, 65, 144}, - {"Config03M", 0x4511d5cd864e706full, 144, 48, 144}, - {"ConfigFlags103M", 0xdd7937eb65073a91ull, 184, 66, 144}, + {"ConfigFlags103", 0xcf60de51b3acfcc3ull, 184, 63, 144}, + {"Config03M", 0x4511d5cd864e706full, 144, 44, 144}, + {"ConfigFlags103M", 0xdd7937eb65073a91ull, 184, 65, 144}, {"Config04", 0xa0a03448564c9be0ull, 144, 21, 144}, - {"ConfigFlags104", 0xaf23be135bf66e22ull, 48, 12, 48}, + {"ConfigFlags104", 0xaf23be135bf66e22ull, 48, 13, 48}, {"Config04M", 0xe0f138c9c848b160ull, 144, 21, 144}, - {"ConfigFlags104M", 0xbf637ed723b215beull, 48, 12, 48}, + {"ConfigFlags104M", 0xbf637ed723b215beull, 48, 13, 48}, {"Config05", 0x17fc80e7cecd9737ull, 48, 14, 48}, {"ConfigFlags105", 0x270ebbdd8cfa9d63ull, 48, 13, 48}, {"Config05M", 0x3e5a9fcbb1fdcd5eull, 48, 14, 48}, From d23f20fd5c8f2ed13518ea1c0a8109055a63f168 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 12:31:02 +0800 Subject: [PATCH 76/83] Docs: a percent-of-equity quantity under a cash commission after R5 lane PAR-CASHFEE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/design/native-feature-parity.md gets §3.10: TradingView's rule (per order and per contract, strategy.equity with the open entries' cash fees charged, the order's own fee left out, the level for a priced entry), the thirteen tapes with the entries the rule fits, the adapter change, the replay, the corpus probe's move toward its tape, the hash step, and two findings recorded and not changed: TradingView margin-calls a default 100 % entry on its entry bar at margin 100 once its cash fee takes the equity under the position (the adapter's margin equity restores that fee), and the FX case is unmeasured. The SZ5 row names the cash-fee reserve as adapter policy, the X15 b row says who still reads the restored equity, and H-THIN's finding 1 points at the fix. docs/pine-adapter-kernel-notes.md §5.2 keeps the retired kernel block verbatim and marks its "cash commissions reserve nothing" claim corrected. Anchors: the lane's source edits moved lines in pine_adapter.cpp, pine_adapter.hpp and pine_state_hash.cpp; every anchor citing them is carried through an exact difflib line map from 09f0bbda (182 moved, digits only, in the design doc, ADR-0001 and native-engine.md), and the two on the SZ5 row the lane rewrote are mapped the same way by hand. check_doc_anchors: every anchor resolves to its symbol; check_doc_lint: clean. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 6bae784e66e6e6426fc461e4bc1a86911d2e3000) INT26 pick note: the docs merged on anchor digits only (they re-anchor once, in INT26 (vii)). --- docs/adr/0001-kernel-adapter-boundary.md | 24 +-- docs/design/native-feature-parity.md | 231 ++++++++++++++++------- docs/pages/native-engine.md | 2 +- docs/pine-adapter-kernel-notes.md | 10 + 4 files changed, 190 insertions(+), 77 deletions(-) diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 05ba65ac..1055faac 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -209,8 +209,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:676`) for TV sizing and fill spelling; `validate_execution_precommit` (`pine_strategy_host.cpp:701`), whose - `validate_precommit` (`pine_adapter.cpp:13034`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:12902`) to own the opening margin decision; `on_native_tick` + `validate_precommit` (`pine_adapter.cpp:12730`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:12948`) to own the opening margin decision; `on_native_tick` (`pine_strategy_host.cpp:299`) / `on_native_applied` (`pine_strategy_host.cpp:350`) 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 @@ -233,9 +233,9 @@ kernel and own these quirks, each at its site: (`select_exit_activation` src/compat/pine/exit_activation.cpp:42, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:10, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:46) 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:9591-9596`); the half-tick + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:9435-9440`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:333) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11671`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11514`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:370-376`, 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 @@ -252,12 +252,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:2023-2034`); the POOC reservation-growth population predicate + ledger (`close_logical_units_` `pine_adapter.hpp:1996-2007`); 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:14069`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:14105`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:13686`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:13722`) 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 @@ -268,7 +268,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:16804`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16421`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -317,9 +317,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:8282`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:8119`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:663`), and the two fills Pine refuses to - recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5641`). + recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5479`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the three session flags, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` @@ -372,7 +372,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:370-376`). `strategy.close` batching and the entry-id ledger - are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2023-2034`) and + are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:1996-2007`) 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:417`); what enables the generic model is the presence of `NativeRunSpec::margin`. So is the string-sentinel decoding of adapter-written comments: @@ -682,7 +682,7 @@ Every line number in this section names its symbol on this tree, which |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:258`), `source_level_on_price_grid` (`:244`), `nearest_tick` (`:182`) / `source_bar_fill_tick` (`:203`) / `directional_tick` (`:227`), 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`, `tests/test_native_price_grid.cpp` | | `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13953`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:15008`), `chart_day_key` (`pine_adapter.cpp:13850`), `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:242`) and `configure_native` (`pine_strategy_host.cpp:243`) 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:13226-13227`): 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_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:12849-12850`): 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:2193-2195`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). 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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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` | diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 229a79b2..865ed2c4 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,19 +47,19 @@ 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:2737-2748; `entry()` pine_adapter.cpp:5289, `order()` pine_adapter.cpp:10480) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5698-5745; `submit` native_host.hpp:1213 | K | — | F:A1 O:A1 S:O2 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2570-2581; `entry()` pine_adapter.cpp:5127, `order()` pine_adapter.cpp:10323) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5698-5745; `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:8417 → `Flatten{}` pine_adapter.cpp:8446-8447; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2707-3914 → `host.replace` pine_adapter.cpp:2723-2803) | **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:4722-4768, `cancel_all` pine_adapter.cpp:18032-18047; `live_by_source_key_` pine_adapter.hpp:1995; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:4064-4129) | **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. | +| OL3 | Amend / cancel one working order (`submit_or_replace` pine_adapter.cpp:2540-3747 → `host.replace` pine_adapter.cpp:2556-2636) | **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:4551-4597, `cancel_all` pine_adapter.cpp:17646-17661; `live_by_source_key_` pine_adapter.hpp:1968; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3897-3962) | **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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5513-5548, `cohort_for` pine_adapter.cpp:2501; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7473-7490) | **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:2746-2752) | **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 | — | +| 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:5351-5386, `cohort_for` pine_adapter.cpp:2501; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7310-7327) | **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:2579-2585) | **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:13004; lowering `submit_one` pine_adapter.cpp:9040-9052, `BindCohort` pine_adapter.cpp:9541-9547, `submit_leg` pine_adapter.cpp:9565-9589; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8645-8648; `materialize_relative_exits` pine_adapter.cpp:17322-17363, pine_adapter.hpp:1820-1832) | **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:5624-5658. `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:1226-1242; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19083-19112) | **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:1997-2015); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:975-1054); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3468-3635, `materialize_snapshot` pine_adapter.cpp:10880-10904), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1146-1173, `DeferredOpenMarketableSell` pine_adapter.hpp:1584-1607, `CloseCallsiteState` pine_adapter.hpp:1678-1693; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15631, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15865) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:8302; lowering `submit_one` pine_adapter.cpp:8884-8896, `BindCohort` pine_adapter.cpp:9385-9391, `submit_leg` pine_adapter.cpp:9409-9433; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8482-8485; `materialize_relative_exits` pine_adapter.cpp:16936-16977, pine_adapter.hpp:1793-1805) | **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:5624-5658. `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:1231-1247; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:18697-18726) | **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:1970-1988); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:975-1054); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3301-3468, `materialize_snapshot` pine_adapter.cpp:10723-10747), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1557-1580, `CloseCallsiteState` pine_adapter.hpp:1651-1666; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15248, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15482) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:714-746) | 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:1677); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1751). 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 @@ -67,27 +67,27 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:5710-5732; `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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:279-281) | **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:11779-11791) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4464-4469) **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:11692-11829) stays A. | -| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:4021-4067) | **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:282-284) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9591-9596, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11692-11829, retained best `retained_trail_best` pine_adapter.hpp:282-284, operand / restart rules `source_trail_points` pine_adapter.cpp:8615-8841, `trail_one_shot` pine_adapter.cpp:9680-9809 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR2 | Trail activation / offset in ticks or points, relative to the entry fill (`trail_points_to_ticks` include/pineforge/compat/pine/trail_ticks.hpp:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11622-11634) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4464-4469) **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:11535-11672) stays A. | +| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:3854-3900) | **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:287-289) is A. | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9435-9440, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11535-11672, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8452-8678, `trail_one_shot` pine_adapter.cpp:9524-9653 | 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:5902-5906, pine_adapter.cpp:5902) | **yes** — `Transact` in `OrderIntent` native_order.hpp:229 | K | — | F:C1 O:C1 S:S1 | — | +| SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5740-5744, pine_adapter.cpp:5740) | **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:10989-12193) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4935-4969 | 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:2595-2650) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4976-4978 **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: rounded equity `source_money_round` pine_adapter.cpp:2547, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:378 (→ SZ10) | -| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2556-2558) | **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:2608, `typed_entry_units` pine_adapter.cpp:2626; `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:2587; `percent_commission_live_equity` pine_adapter.cpp:4328-4368) | **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. | -| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2513-2528, `PineSizingSnapshot` pine_adapter.hpp:147-154; fill-time exceptions `default_sized` pine_adapter.cpp:6645-6671) | **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`. | +| SZ3 | Percent-of-equity sizing (`default_sizing_units` pine_adapter.cpp:2428-2483) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4976-4978 **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: rounded equity `source_money_round` pine_adapter.cpp:2380, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:378 (→ SZ10) | +| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2389-2391) | **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:2441, `typed_entry_units` pine_adapter.cpp:2459; `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:2449; `percent_commission_live_equity` pine_adapter.cpp:4190-4231) | **no** — only a dead primitive, `reserve_percent_commission` (since deleted), with no caller in `src/` **Closed:** `Sized::reserve_percent_fee` divides the sizing cash by `1 + fee_value / 100` under `NativeFeeKind::Percent` and is an exact no-op for every other fee kind; the dead `reserve_percent_commission` primitive is gone (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K | L3 | F:C3 O:C6 S:S3 | **Disagree:** F lists the reserve under quirk yet offers a knob / O kernel, "partial" / S knob. R5-1: fee-net option is generic. TradingView's cash-commission reserve stays adapter policy over the kernel's `CashValue`: a percent-of-equity default quantity under a cash-per-order or cash-per-contract fee takes its percentage of `strategy.equity` (`PineSizingSnapshot::strategy_equity`, the open entries' cash fees charged) and leaves out the fee its own order pays (`default_sizing_cash`, R5 lane PAR-CASHFEE, §3.10; `tests/test_cash_fee_sizing_tapes.cpp` on the tapes in `tests/fixtures/cash_fee_sizing`) | +| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2513-2528, `PineSizingSnapshot` pine_adapter.hpp:147-159; fill-time exceptions `default_sized` pine_adapter.cpp:6483-6509) | **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:592, 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:4992-4995; 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:12478-12506, `compute_exit_reservation` pine_adapter.cpp:4411-4532) | **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:4463-4786, `reserved_exit_qty` pine_adapter.cpp:8967-9277) | **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:369-376, `source_money_floor_lot` pine_adapter.cpp:378; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4443-4458) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6706), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7913), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12735), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13331, pine_adapter.cpp:13384) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15679) | 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:2595), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2608, `typed_entry_units` pine_adapter.cpp:2626), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18678) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7175-7526; ledger `close_batch_bar_` pine_adapter.hpp:2038-2049; 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:12321-12349, `compute_exit_reservation` pine_adapter.cpp:4244-4363) | **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:4296-4615, `reserved_exit_qty` pine_adapter.cpp:8811-9121) | **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:369-376, `source_money_floor_lot` pine_adapter.cpp:378; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4276-4291) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | +| 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:6544), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7750), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12578), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:12954, pine_adapter.cpp:13007) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15296) | 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:2428), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2441, `typed_entry_units` pine_adapter.cpp:2459), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18292) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7013-7363; ledger `close_batch_bar_` pine_adapter.hpp:2011-2022; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | ### 1.4 Margin, admission, liquidation, risk @@ -95,21 +95,21 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2361) | **yes** — `allowed_open_directions` native_run_spec.hpp:597 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:595 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:13226-13229) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6018-6064, cap left unset `max_open_lots` pine_adapter.cpp:2193-2195) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. 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:8735-8741). | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13280-13345 and leaves the kernel gate unset pine_adapter.cpp:13279-13281) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13935-13936, `margin_pct` pine_adapter.cpp:13343) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:14035-14213, `submit_margin_call_units` pine_adapter.cpp:14174-14220, `schedule_margin_call_path` pine_adapter.cpp:18185-18262) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13583-13619) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; `tests/test_adapter_margin_schedule_differential.cpp` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:595 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:12849-12852) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6062-6108, cap left unset `max_open_lots` pine_adapter.cpp:2193-2195) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. 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:8779-8785). | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:12903-12968 and leaves the kernel gate unset pine_adapter.cpp:12902-12904) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13552-13553, `margin_pct` pine_adapter.cpp:12966) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:13652-13830, `submit_margin_call_units` pine_adapter.cpp:13791-13837, `schedule_margin_call_path` pine_adapter.cpp:17799-17876) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13629-13665) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; `tests/test_adapter_margin_schedule_differential.cpp` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). | | MG8 | Liquidation price query (`margin_liquidation_price` pine_strategy_host.cpp:114-130) | **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:16283-16299, `apply_fx_opening_margin_slice` pine_adapter.cpp:18263-18291) | **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:16774-16813) | **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:1192-1200; gate `max_cons_loss_days` pine_adapter.cpp:13315-13318) | **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:13896-13910, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14922-14952) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14965-15199) | **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:13706-13785) | **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:1311 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 | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:15900-15916, `apply_fx_opening_margin_slice` pine_adapter.cpp:17877-17905) | **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:16391-16430) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:12938-12941) | **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:13513-13527, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14539-14569) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14582-14816) | **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:13329-13402) | **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:1311 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:13969, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14028, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14774-14814, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14298-14425, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14298) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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, `tests/fixtures/margin_entry_bar/pm2-*`). 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 (`tests/fixtures/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; `tests/fixtures/half_tick_rounding`, PAR-MARGIN-2). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3817-3884, `record_market_review` pine_adapter.cpp:15203-15261) | n/a | A | — | S:R2 | (single-source) | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3650-3717, `record_market_review` pine_adapter.cpp:14820-14878) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -117,15 +117,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2318-2319) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7540-7544 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:16118) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15735) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `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:7027 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7078 (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:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663; `begin_coof_recalc` pine_adapter.cpp:5610, `flush_coof_tail` pine_adapter.cpp:16261, `end_coof_recalc` pine_adapter.cpp:5631) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663; `begin_coof_recalc` pine_adapter.cpp:5448, `flush_coof_tail` pine_adapter.cpp:15878, `end_coof_recalc` pine_adapter.cpp:5469) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7166-7170; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:203-205. | | 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:7534-7653 **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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:152-238) 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:5757), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1659-1666 | 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:5595), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1632-1639 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds @@ -145,7 +145,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:5698-5745, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6281-6289; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | | FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:584 applied once native_execution_consumer.cpp:4611-4612, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12032-12041) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:388-395) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:181-184, `source_bar_fill` pine_adapter.cpp:11962-11971, `directional_tick` pine_adapter.cpp:226-230) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4073; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6363-6364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:181-184, `source_bar_fill` pine_adapter.cpp:11805-11814, `directional_tick` pine_adapter.cpp:226-230) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4073; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6363-6364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:257-305) 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:8721). **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). | @@ -210,8 +210,8 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19083-19112 | 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:522, 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:66) 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:303-580) 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 | +| 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:18697-18726 | 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:522, 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:66) 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:308-585) 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:604. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:417; 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:1274 | 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) | | m | `request.security` machinery | `register_security_eval` engine_security.cpp:27, `set_native_security_feed` engine_aux_security.cpp:76-127, `SecurityEvalState` engine.hpp:1156-1360 carry Pine semantics (lookahead, gaps, Heikin-Ashi, KI-55 range start) and are driven only by the source layer | HASH-VISIBLE / measured path | F planned to move both TUs and the state into the source host after native HTF. **R5-6 narrows this:** L6 reuses the feed store and routing, so those stay kernel; only the Pine-only evaluator semantics remain candidates. Do last. **Status (lane L12b): done** — the evaluator semantics are `source::PineSecurityEvalState` + `src/source/pine_security_eval.cpp`; the kernel keeps the registry, aggregator, feed store and one generic step | F S | @@ -324,7 +324,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:1730, `guard_native_mutation` engine_aux_security.cpp:78 | 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). No kernel TU includes a source header | `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13 | 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:739 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:370-376 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:1996-2007; `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:370-376 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2013-2024; `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) | 58 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 58 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:13933f70987f6bfe37d3015ae065a8d5ee5937c37431d48e2ecd1f17a4c29958` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:862, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:590-709, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:515-567, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:494-511, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:802-833, c_abi.cpp:929 | 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 | @@ -522,11 +522,11 @@ line numbers are this tree's (683a82f); kernel semantics are | 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:2361); 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:13226-13227; 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:16774, `update_risk_state` pine_adapter.cpp:13867-13894): 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:13219), 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:1195-1203): +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:17724-17733); 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:16277); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16389), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14952-14961, `submit_intraday_loss_close` pine_adapter.cpp:14922-14950), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17131); 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:5961, `:7620`, `:8344`, `:8472`, `:11449`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:18030-18033) | `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:13858-13860); 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:13764-13785, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_position_size` | precommit refuses a flat/same-side ENTRY at its fill when the LIVE book already holds ≥ the limit (`max_position_size` pine_adapter.cpp:12849-12850; 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:16391, `update_risk_state` pine_adapter.cpp:13484-13511): 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:12842), 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:1200-1208): +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:17724-17733); 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:15894); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16006), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14569-14578, `submit_intraday_loss_close` pine_adapter.cpp:14539-14567), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:16745); 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:5799, `:7457`, `:8181`, `:8309`, `:11292`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:17644-17647) | `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:13475-13477); 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:13381-13402, `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, @@ -1003,15 +1003,15 @@ row C). | **D** | the kernel's cohort rosters on a Pine host: O(openings) per row (V19-A's finding) | ×2.45, ×2.80, ×3.06 per doubling from 4,000 to 32,000 bars of a flip recorded per bar on `91d65ad6` (0.206 s to 4.33 s), against ×2.02 with recording off | **resolved** by R5 lane V19-FIX: the adapter takes an origin that can no longer be bound off its kernel roster, so the continuation folds the openings that still work or hold a lot | **A. The adapter's recording fold.** Until R5 lane V19-E (below), -`PineExecutionAdapter::hash_state` pine_state_hash.cpp:236 folded, at every +`PineExecutionAdapter::hash_state` pine_state_hash.cpp:242 folded, at every 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:286) — one per accepted request, cleared only by +pine_state_hash.cpp:292) — one per accepted request, cleared only by `reset_for_run` pine_adapter.cpp:1942 — and reflected the whole admission journal (since replaced by a running digest of its events, `admission_journal` -pine_state_hash.cpp:574). `PineScheduler::hash_state` pine_state_hash.cpp:612 +pine_state_hash.cpp:573). `PineScheduler::hash_state` pine_state_hash.cpp:611 re-folded the consumed source prefix too (`consumed` -pine_state_hash.cpp:619-641). Measured with recording on and off, in two +pine_state_hash.cpp:618-640). Measured with recording on and off, in two shapes: - the audit's order-and-cancel probe (one `strategy_order` accepted and @@ -1042,22 +1042,22 @@ moving a single value, but not from inside lane F9's files: 2. **The transforms need a home.** A row's transform, or a composed segment's, has to outlive the call. The fold is const; the sink is created per call (engine_state_hash.cpp:32); and none of `PlacementTable` - pine_adapter.hpp:361, `admission::Journal` market_admission.hpp:188 or + pine_adapter.hpp:366, `admission::Journal` market_admission.hpp:188 or `PineScheduler` has a member it could keep one in. Adding one is a header change in `include/pineforge/source/`, outside this lane. 3. **A cache must know which rows changed, and nothing records it.** `PlacementTable` hands out mutable rows — the non-const `find` - pine_adapter.hpp:497, the non-const `at` pine_adapter.hpp:513 and its + pine_adapter.hpp:502, the non-const `at` pine_adapter.hpp:518 and its iterators — and the adapter rewrites retained rows in place through them, not only rows still working. Lane F9's probe hashed every row callback to callback: the order-and-cancel shape rewrites none, the re-issue shape 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:8756). Full-table scans + (`has_full_entry_bracket` pine_adapter.cpp:8593). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1529, `revive_brackets_after_margin` pine_adapter.cpp:1751, - `preserved_by_close_all` pine_adapter.cpp:11480), and + `preserved_by_close_all` pine_adapter.cpp:11323), 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. @@ -1145,13 +1145,13 @@ retires every leg of each exit row it names and drops such a parked stop (`retire_cancelled_exits` pine_adapter.cpp:1677), 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:2930). Both shapes now cost ×1.89 to ×2.12 per doubling up +pine_adapter.cpp:2763). 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:3361); a lot opened +it off (`release_closed_cohort_origins` pine_adapter.cpp:3194); 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 @@ -1307,7 +1307,7 @@ witness all print the same bytes before and after. | item | the adapter's copy | now | evidence | |---|---|---|---| | a | `compute_liquidation_price`, a second closed-form solve of the margin level | gone: `strategy.margin_liquidation_price` reads `native_liquidation_price()` in TradingView's tick spelling (row MG8) | bit-identical at every bar of the 1,021 margin-call runs of a 1,200-configuration seeded sweep; the 179 runs with margin calls switched off declare no margin model and read na (`tests/test_margin_call_l4a.cpp`) | -| b | `open_entry_fees_`, the cash-fee ledger the sizing basis restores | **kept, measured**: it is not a mirror of the kernel's per-lot `open_entry_commission` | the two part after a FIFO `strategy.exit(from_entry)` closes an older lot of another id (the ledger drains the named id's fee, the kernel the lot it closed): 1 of 400 seeded fee runs and three publication-witness configurations move. The `lab tv` tape `hthin-x15b-fifo-fee-basis` shows TradingView restores neither (§3.9 finding 1), so neither answer is TradingView's and the ledger stays byte-identical | +| b | `open_entry_fees_`, the cash-fee ledger the sizing basis restored (since R5 lane PAR-CASHFEE only the money gates' and the margin money's equity restore it, §3.10) | **kept, measured**: it is not a mirror of the kernel's per-lot `open_entry_commission` | the two part after a FIFO `strategy.exit(from_entry)` closes an older lot of another id (the ledger drains the named id's fee, the kernel the lot it closed): 1 of 400 seeded fee runs and three publication-witness configurations move. The `lab tv` tape `hthin-x15b-fifo-fee-basis` shows TradingView restores neither (§3.9 finding 1), so neither answer is TradingView's and the ledger stays byte-identical | | c | `CohortFacts::live_units_by_origin`, the per-id unit ledger | **kept, measured**: it is the per-id logical exposure, not the kernel's lots | against the kernel lots booked under each id at every callback of 600 seeded runs it differs in 1,494 of 287,549 checks (1,489 beyond 1e-9): a close named by one id drains another id's older lot, and the ledger debits the named id. P10's reservation reads it as the entry's own quantity | | d | the pre-open slice's own ×4 | gone: `schedule_preopen_margin_slice` sizes through `source_margin_units` (row MG7) | byte-identical; the M10 differential's band and fudge cases now book the shared rule's 1 and 1, the kernel's and `ab9714be`'s | | e | six copies of the civil-date arithmetic | gone: `native_calendar::native_civil_date` / `native_civil_days` (ADR-0001, H-THIN rule 2 rulings) | byte-identical by construction; `tests/test_chart_day_key_arithmetic.cpp`, `tests/test_utc_month_key_arithmetic.cpp` | @@ -1404,13 +1404,116 @@ stays out of reach for the reason H-MEASURE measured: the growth since round entries, and the corpus probe `order-percent-equity-cash-commission-01` on all 366 of its trades. The engine, before and after this lane, restores the open lots' cash fees and reserves no fee for the new order, so that - probe's quantities differ from TradingView's in the fourth decimal. + probe's quantities differ from TradingView's in the fourth decimal. R5 lane + PAR-CASHFEE measured the rule on twelve more tapes and fixed it (§3.10). 2. A `calc_on_order_fills` run of the seeded witnesses can stop with the kernel's "native current evaluated allowance mismatch": 117 of the second family's 2,000 runs, every one with calc-on-fills. The base tree stops 111 of those seeds, 108 of them the same runs; the lane's E19 and P10 moves shift which runs reach the failing state, not the failure. +### 3.10 Parity lane PAR-CASHFEE — a percent-of-equity quantity under a cash commission + +H-THIN's finding 1 (§3.9) as a rule, measured and fixed. TradingView sizes a +percent-of-equity DEFAULT quantity under a cash commission from +`strategy.equity` -- every open entry fee charged, the cash ones included -- +and leaves out of that money the fee the order itself pays, on the symbol's +quantity grid: + +- cash per order: `units = floor((pct × E − fee) / (price × pointvalue))`; +- cash per contract: `units = floor(pct × E / (price × pointvalue + fee))`, + the money covering the units' notional and their fee together. + +`price` is the price the quantity is sized at: the signal close for a market +`strategy.entry` and for `strategy.order`, the level for a limit or stop +entry. A reversing entry sizes its new position the same way from the equity +that still carries the reversed position (its open profit and its entry fee +share), and its one order pays one fee, split over the closed and the opened +units. Before this lane the adapter took the percentage of an equity that +restored the open entries' cash fees (`percent_commission_live_equity`) and +reserved nothing for the order, so every such quantity came out too large. + +**Evidence: thirteen `lab tv` tapes** (`tests/fixtures/cash_fee_sizing`, +ws-report-v1, rangeProof covered; BINANCE:ETHUSDT.P 15 over 2025-04 and +CME_MINI:ES1! 15 over 2025-06). A tape's `E` is read off the tape itself -- +the initial capital, the net profit closed before the signal, and each open +piece marked at the signal close less its pro-rata entry fee. + +| tape | fee | default % | margin | scenarios | trades | sized entries the rule fits | +|---|---|---|---|---|---|---| +| `pcf-order-p50-m25` | per order 1000 | 50 | 25 | flat, pyramided, partial close, reversals | 15 | 14 / 14 | +| `pcf-order-p100-m25` | per order 1000 | 100 | 25 | the same | 15 | 14 / 14 | +| `pcf-order-p200-m25` | per order 1000 | 200 | 25 | flat, reversals | 6 | 6 / 6 | +| `pcf-order-priced-m25` | per order 1000 | 50 | 25 | limit, stop, `strategy.order` | 7 | 7 / 7 | +| `pcf-order-p100-m100` | per order 1000 | 100 | 100 | flat, reversal | 11 | 6 / 6 | +| `pcf-es-order-p50-m25` | per order 1 000 000 | 50 | 25 | ES1! (point value 50), pyramided, reversal | 7 | 7 / 7 | +| `hthin-x15b-fifo-fee-basis` | per order 1000 | 50 | 100 | FIFO exit of another id (lane H-THIN) | 11 | 6 / 6 | +| `pcf-contract-p50-m25` | per contract 20 | 50 | 25 | flat, pyramided, partial close, reversals | 15 | 14 / 14 | +| `pcf-contract-p100-m25` | per contract 20 | 100 | 25 | the same | 15 | 14 / 14 | +| `pcf-contract-p200-m25` | per contract 20 | 200 | 25 | flat, reversals | 6 | 6 / 6 | +| `pcf-contract-priced-m25` | per contract 20 | 50 | 25 | limit, stop, `strategy.order` | 7 | 7 / 7 | +| `pcf-contract-p100-m100` | per contract 20 | 100 | 100 | flat, reversal | 9 | 6 / 6 | +| `pcf-es-contract-p50-m25` | per contract 1000 | 50 | 25 | ES1! (point value 50), pyramided, reversal | 7 | 7 / 7 | + +That is 60 of 60 entries per order and 54 of 54 per contract, and all 366 +trades of the corpus probe `order-percent-equity-cash-commission-01` (25 %, +1.5 per order). The restored basis fits only the flat entries; no fee +reserve, or a per-contract fee not scaled by the point value, fits none. A +priced entry sized at the signal close instead of its level fits none of the +eight. + +**The fix is adapter policy** (`default_sizing_cash`, +`default_sizing_reserves_cash_fee`): the sizing snapshot records +`strategy.equity` beside the restored equity (`PineSizingSnapshot::strategy_equity`, +set by `mark_sizing_equity` only where the run declares a percent-of-equity +default quantity and a positive cash commission, and folded only there), and +the money it hands the kernel's `CashValue` is the percentage of it less the +per-order fee, or scaled by `unit / (unit + fee)` per contract. The money +gates and the margin money keep the restored equity; a per-call `qty_type` +(which TradingView's compiler refuses) keeps its conversion. + +**Replay** (`tests/test_cash_fee_sizing_tapes.cpp`, the tapes' scripts through +the Pine adapter on their bars): on the eleven tapes without an entry-bar +margin call the adapter books 0 of 111 trades as TradingView does before the +fix and 111 of 111 after it (ids, sides, times, prices and quantities exact, +commission to 1e-9, net profit to 1e-7 of |profit| + |commission|). + +**Corpus.** `order-percent-equity-cash-commission-01` is the corpus's only +probe with a percent-of-equity default quantity and a cash commission, and the +only one that moves. The corpus harness gives it no quantity grid, so its +quantities are the raw quotient; before → after: quantities inside +TradingView's 0.0001 cell 0 → 355 of 366 (the other 11 lie 1e-7 to 3e-6 below +the cell, where the harness's unfloored quantities left the equity a few cents +under TradingView's), largest |Δ quantity| 0.00106 → 0.0000985, 90th +percentile |Δ net profit| 0.029 → 0.0023; every one of the 366 quantities and +net profits is closer. + +**Hash step.** Only runs that record the new snapshot value move: +`tests/test_adapter_quiet_bar_pinned.inc` 80 values over 24 rows, the six +random scenarios with a percent-of-equity default quantity and a cash-per-order +fee (Random05, 06, 20, 35, 38, 46), no count; and +`tests/test_publication_witness_pinned.inc` 34 of 156 digests -- exactly the +Config runs whose configuration sweep passes through such a declaration, by a +census of the battery -- six of them closing a different number of trades. +`tests/test_l4b_policy_regressions.cpp` pinned the old rule (20 units where +the rule gives 9.9 + 9.89). + +**Findings, recorded and not changed here.** + +1. At margin 100 TradingView margin-calls a default 100 % entry on its own + entry bar when the order's cash fee leaves the equity under the position + (`pcf-order-p100-m100`: a long filled 0.01 above its signal close loses + 0.0008 at the open and 2.2032 more on the bar; a short loses 0.4592 at the + bar's high), and every margin-call order pays its own per-order fee. The + adapter's margin equity restores a cash entry fee + (`NativeMarginEquityBasis::MarkedEquityBeforeOpenCommission`) and books no + such call, so the two tapes part from their second scenario on; the test + pins the adapter's rows as a recorded divergence. The quantity sized is + TradingView's: the first scenario is TradingView's trade and the second's + quantity the sum of TradingView's pieces. +2. The rule converts at the kernel's `price × pointvalue × fx`; no tape has an + account currency other than the symbol's, so the FX case is unmeasured. + ## 4. Risks and open questions ### 4.1 Entanglements: a generic feature tangled with a TV quirk @@ -1421,7 +1524,7 @@ stays out of reach for the reason H-MEASURE measured: the growth since round | 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5782) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6290-6303); 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:16206-16235) | The complete bar cannot be masked globally | Opt-in `OpenOnly` view (A.4); loud doc warning in L0 | F | +| E5 | **Open-bar lookahead.** The adapter reads the full bar in `on_bar_open` (margin path scheduling uses H/L, pine_adapter.cpp:15735-15764) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | @@ -1441,7 +1544,7 @@ stays out of reach for the reason H-MEASURE measured: the growth since round | P5 | **Test asymmetry:** 430 adapter test files vs 14 native | native-only tests in every lane (§3.1 b) | O | | P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, non-empty FX curves, security feeds and source setters (`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:293) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:585 → `set_risk_direction` pine_adapter.cpp:18416 (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:298) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:585 → `set_risk_direction` pine_adapter.cpp:18133 (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) @@ -1466,7 +1569,7 @@ stays out of reach for the reason H-MEASURE measured: the growth since round | **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:16283-16299) 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:2645) 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:15900-15916) 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:2478) 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. | @@ -1652,7 +1755,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:1329) (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:373), 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:13690-13788); 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:13313-13405); 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:4675-4688 and the per-point walk), `native_order.hpp`, the state hash. ### A.9 Single-source proposals not adopted diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index 2534109a..df0ac551 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1484,7 +1484,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:14069-14093`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:13686-13710`), 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/docs/pine-adapter-kernel-notes.md b/docs/pine-adapter-kernel-notes.md index caf0f4fd..283161e7 100644 --- a/docs/pine-adapter-kernel-notes.md +++ b/docs/pine-adapter-kernel-notes.md @@ -496,3 +496,13 @@ double reserve_percent_commission(double cash) const { ? cash / (1.0 + commission_value_ / 100.0) : cash; } ``` + +> **Corrected by R5 lane PAR-CASHFEE.** The block's last claim -- that a +> cash-per-order or cash-per-contract commission reserves nothing -- does not +> hold on TradingView. A percent-of-equity default quantity under a cash +> commission takes its percentage of `strategy.equity` (the open entries' cash +> fees charged) and leaves out the fee its own order pays: the value per order, +> or the value per contract against the contract's notional. The adapter's +> `default_sizing_cash` does so; the tapes are under +> `tests/fixtures/cash_fee_sizing` and `docs/design/native-feature-parity.md` +> §3.10 records the measurement. From 85cf79bc9f5c81d26a8e12452554ff6d3a850923 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 12:31:02 +0800 Subject: [PATCH 77/83] Floors: release 686 -> 687 and the excluded registrations +1 for R5 lane PAR-CASHFEE's row test_cash_fee_sizing_tapes is source-bound: it registers in release, debug, sanitizers and native and not in the kernel profile. Counted with ctest -N on the lane's tree against its base 09f0bbda in the same configuration: 667 -> 668, and 251 -> 251 with PINEFORGE_BUILD_SOURCE_LAYER=OFF, so KERNEL_MIN_TESTS stays 277. RELEASE_MIN_TESTS 686 -> 687, EXCLUDED_REGISTERED_MIN debug/sanitizers 667 -> 668 and native 676 -> 677; test_ci_verify's pin of the excluded floors follows with an expectation correction, and the two pages that cite RELEASE_MIN_TESTS by line (CONTRIBUTING.md, docs/pages/contributing-llm.md) follow it to ci_verify.py:365. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit b44b10b4c634a29f89eceb459bc749bb8f669df7) INT26 pick note: the floors take this lane's one source-bound row on top of the integrated counts -- RELEASE 701 -> 702, EXCLUDED_REGISTERED_MIN 682/682/691 -> 683/683/692, KERNEL 286 unchanged -- recounted with ctest -N under ci_verify's own argv per profile on this tree (INT26-scratch/ctestN/pcf-*.txt: release 702, kernel 287 registered, debug 683, sanitizers 683, native 692; the one new name is test_cash_fee_sizing_tapes). Each floor comment lists it with wave H's other rows; test_ci_verify's pin, docs/ci.md's floor sentence and the four floor citations follow. --- CONTRIBUTING.md | 2 +- docs/ci.md | 8 ++++---- docs/pages/contributing-llm.md | 4 ++-- scripts/ci_verify.py | 17 +++++++++-------- scripts/test_ci_verify.py | 2 +- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0bace448..6f3bf200 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -153,7 +153,7 @@ What each gate refuses: `ci_verify.py` counts the CTest rows that actually **ran** and fails below a floor — `KERNEL_MIN_TESTS` ci_verify.py:260 and `RELEASE_MIN_TESTS` -ci_verify.py:389. +ci_verify.py:390. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/docs/ci.md b/docs/ci.md index 78b7b41e..e570fdd4 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -102,10 +102,10 @@ requires the rows that actually ran to equal the second count, and records `registered`, `labelled = registered - selected`, and `ran` in `ctest-exclusion.log` and `ci-summary.json`. A skip, missing executable, lost registration below the PR registration floor, unreadable enumeration, or -missing label fails. The PR registration floors at INT26 are 682 for Debug -and sanitizers and 691 for native (653 and 662 at the INT24 base, plus wave G's -six rows and wave H's twenty-three). The full-run release and kernel floors are -701 and 286 rows that ran; full runs do not exclude a label. +missing label fails. The PR registration floors at INT26 are 683 for Debug +and sanitizers and 692 for native (653 and 662 at the INT24 base, plus wave G's +six rows and wave H's twenty-four). The full-run release and kernel floors are +702 and 286 rows that ran; full runs do not exclude a label. Preflight also runs the detached-comment census of the kernel compile closure (`detached-comments`: `scripts/measure_detached_comments.py --check-ceiling`) diff --git a/docs/pages/contributing-llm.md b/docs/pages/contributing-llm.md index 80ce5484..127fb047 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -118,7 +118,7 @@ finding to report, not a step to take. 10. **A test row never silently disappears.** Each profile counts the rows that *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:260 and - `RELEASE_MIN_TESTS` ci_verify.py:389. Adding rows means raising the floor + `RELEASE_MIN_TESTS` ci_verify.py:390. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -264,7 +264,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:260, `RELEASE_MIN_TESTS` ci_verify.py:389). It +(`KERNEL_MIN_TESTS` ci_verify.py:260, `RELEASE_MIN_TESTS` ci_verify.py:390). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index d16c3332..31fc254b 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -365,7 +365,7 @@ # wave-G lane adds a source-bound row (the K-ULP4 and K-ULP5 C checks and # V19-FIX's scaling rows are rows inside existing TUs). No release row skips, # so 678 registered is 678 run. -# 701 = those 678 plus wave H's twenty-three (INT26), counted with ctest -N on +# 702 = those 678 plus wave H's twenty-four (INT26), counted with ctest -N on # the integrated tree: the nine KERNEL_MIN_TESTS lists above, which register # here too, H-MEASURE's eleven source-bound rows the kernel profile does not # build (AUDIT4 X14) and PAR-ORDERS-2's three tape rows: @@ -383,10 +383,11 @@ # +3 PAR-ORDERS-2 items 1-3 test_flat_coof_exit_tapes, # test_second_extreme_order_tapes, # test_pooc_reversing_stop_tapes +# +1 PAR-CASHFEE test_cash_fee_sizing_tapes # PAR-MARGIN's own count also held the margin differential, which that lane # had cherry-picked for its pins; it counts once, as H-MEASURE's. No release -# row skips, so 701 registered is 701 run. -RELEASE_MIN_TESTS = 701 +# row skips, so 702 registered is 702 run. +RELEASE_MIN_TESTS = 702 # ADR-0001 ruled-count floors, beside the ctest floors (R5 lane H-DOCGATES, # AUDIT4-opus X13 / docs-a N7). check_kernel_residuals.py counts the rulings # its vocabulary reads -- 174 identifiers and 45 texts on the lane's tree: the @@ -407,12 +408,12 @@ # PR-only registration floors: the complete CTest populations of the three # excluded profiles at INT25, counted with ctest -N on the integrated tree -- # 653/653/662 at 91d65ad6 (INT24) plus wave G's six rows (C-SURFACE-1 +1, -# KERNEL-EDGE +1, K-ULP4 +2, K-ULP5 +1, DOC-TRUTH-4 +1) in each; 682/682/691 -# at INT26, each with wave H's twenty-three rows of RELEASE_MIN_TESTS +# KERNEL-EDGE +1, K-ULP4 +2, K-ULP5 +1, DOC-TRUTH-4 +1) in each; 683/683/692 +# at INT26, each with wave H's twenty-four rows of RELEASE_MIN_TESTS # (H-MEASURE +12, H-DOCGATES +3, K-OCA-KEEP +1, PAR-ORDERS-2 +4, PAR-MARGIN -# +1, PAR-MARGIN-2 +1, PERF-KEDGE +1), counted the same way. An excluded run -# must still discover at least this many rows before -LE. -EXCLUDED_REGISTERED_MIN = {'debug': 682, 'sanitizers': 682, 'native': 691} +# +1, PAR-MARGIN-2 +1, PERF-KEDGE +1, PAR-CASHFEE +1), counted the same way. +# An excluded run must still discover at least this many rows before -LE. +EXCLUDED_REGISTERED_MIN = {'debug': 683, 'sanitizers': 683, 'native': 692} # The ctest stage's bound. A full sanitizers run (push to main, a manual # dispatch, the maintainers' verification) ran out of its 30 minutes twice on # main's four-core runner before every row had finished, so it gets an hour; a diff --git a/scripts/test_ci_verify.py b/scripts/test_ci_verify.py index b8267da1..fc74e49f 100644 --- a/scripts/test_ci_verify.py +++ b/scripts/test_ci_verify.py @@ -1331,7 +1331,7 @@ def test_ctest_label_exclusion_is_forwarded(self): def test_pr_exclusion_proves_registered_minus_labelled_equals_ran(self): self.assertEqual(ci_verify.EXCLUDED_REGISTERED_MIN, - {'debug': 682, 'sanitizers': 682, 'native': 691}) + {'debug': 683, 'sanitizers': 683, 'native': 692}) for profile, registered in ci_verify.EXCLUDED_REGISTERED_MIN.items(): with self.subTest(profile=profile): code, summary, scripted, _ = self.run_profile( From 5881528a0359fcd5a1a6c3de5ff9cfb7ac973446 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 10:10:39 +0800 Subject: [PATCH 78/83] Re-pin the validation corpus to the codegen a4259656 regeneration a7ad2e2 (lane CORPUS4) pineforge-corpus a7ad2e2 re-derives every generated.cpp in the corpus with pineforge-codegen-oss a4259656, the codegen 1.0 line: #140 (b0ed4967), #141 (696fa25f) and #142 (a4259656) on f3285d79. It is branch regen/codegen-a425965, one commit on bcfb0d6, which is corpus main, f989e36 plus #16. - The 314 validation/ and draft-probes/ TUs go through scripts/regen_corpus_cpp.sh. - The 11 nested special-validation/ and validation/symbol-specified/ TUs, which its corpus/*/*/ glob does not reach, go through the same image, checkout and transpile call. Engine and codegen ship 1.0.0 together, so the corpus this engine pins is now emitted by the codegen that ships. 92 of the 325 TUs change, +920/-373 lines, and no other corpus file moves: - 83 of the 312 validation probes; - both draft-probes; - 2 of the 6 special-validation probes; - all 5 symbol-specified probes. They were stale since 2026-06-10, in the old BacktestEngine-based shape that no longer compiled. Every change is attributed by re-transpiling all 325 sources at each codegen commit that touches pineforge_codegen/: - #140's 4a31a3b6, 66 TUs: na truthiness at boolean boundaries, and na-preserving int conversions at call arguments. - #140's 79584d99, 39 TUs: na-preserving history offsets, and history references typed by their series. - #140's 51bbdcda, 2 TUs: the Keltner EMA warmup. - #140's e19ce6db, 2 TUs: a user type named Box is emitted as pf_safe_Box. - #142's session.ismarket calendar lowering, 5 TUs. - #141 changes none. The pins that name that (corpus, codegen) pair move together: - The corpus gitlink moves f989e36 -> a7ad2e2. - The `# corpus` header of scripts/corpus_parity_baseline.txt is re-recorded with `corpus_trades_identity.py --update`. That ran over this engine's 312 trade lists, re-run from the new TUs with the gate's per-probe run_strategy.py command on macOS arm64. The checker refused on that header first, as it must. - The baseline diff is that one line. All 312 sha256 rows are unchanged, so every engine trade list is byte-identical under the re-emitted TUs, including the 83 whose TU changed. - The verifier grades exactly those lists against unchanged TradingView tapes, so every probe keeps its tier. - The paired-codegen pin moves f3285d7 -> a4259656: CODEGEN_COMMIT in scripts/regen_corpus_cpp.sh and its source check scripts/test_regen_corpus_cpp_pin.py. - `VERIFY=1 scripts/regen_corpus_cpp.sh` at a4259656 over this corpus: 314 verified, 0 drifted. - At f3285d7 over f989e36: 314 verified, 0 drifted. These four lines were every citation of f989e36 or f3285d7 in the tree. Each is a pin statement; no historical record cites either. The Cloud Run case runner refuses a corpus bundle whose commit differs from this gitlink (pineforge-workflow cloudrun/runner/preflight.mjs). The companion pineforge-workflow branch r5/corpus4 (migration 0048) re-pins that side. The corpus PR, this commit and that branch land together. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 3d59fad496b27cc4dfb8659f521a768fa2ff1dcd) --- corpus | 2 +- scripts/corpus_parity_baseline.txt | 2 +- scripts/regen_corpus_cpp.sh | 2 +- scripts/test_regen_corpus_cpp_pin.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/corpus b/corpus index f989e362..a7ad2e2b 160000 --- a/corpus +++ b/corpus @@ -1 +1 @@ -Subproject commit f989e36276d6993d862b8d392df89fc49aacee04 +Subproject commit a7ad2e2b6ea83a02d8f95ece5dd32b4ac6f6dc50 diff --git a/scripts/corpus_parity_baseline.txt b/scripts/corpus_parity_baseline.txt index 8c2e6829..0912f177 100644 --- a/scripts/corpus_parity_baseline.txt +++ b/scripts/corpus_parity_baseline.txt @@ -3,7 +3,7 @@ # ./scripts/check_corpus_parity.sh && \ # python3 scripts/corpus_trades_identity.py --update # and carry the evidence in the commit that moves it. -# corpus f989e36276d6993d862b8d392df89fc49aacee04 +# corpus a7ad2e2b6ea83a02d8f95ece5dd32b4ac6f6dc50 a72ee904563a1787ca8371f37f98cec74f6fb44163a5dfac169b77755a05ed1a validation/analyzer-anvil-percent-costs-01/engine_trades.csv df7fbdb2082c8982e8c38d8843fc53262df693d4ce8b32bf05bc0acd1ef41a16 validation/analyzer-cashflow-cash-costs-01/engine_trades.csv diff --git a/scripts/regen_corpus_cpp.sh b/scripts/regen_corpus_cpp.sh index 4bdfe256..33a59702 100755 --- a/scripts/regen_corpus_cpp.sh +++ b/scripts/regen_corpus_cpp.sh @@ -35,7 +35,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" IMAGE="${IMAGE:-ghcr.io/pineforge-4pass/pineforge-release@sha256:a69c3700e44868d9657697f43e3f3954a7ed8bde039b55b52ec901538720ac05}" -CODEGEN_COMMIT="f3285d79f476c029e9739599658b5de6296046bc" +CODEGEN_COMMIT="a4259656d025f36411a7ad1e4ab6c6dd020b780a" CODEGEN_REPO="${CODEGEN_REPO:-https://github.com/pineforge-4pass/pineforge-codegen-oss.git}" VERIFY="${VERIFY:-0}" diff --git a/scripts/test_regen_corpus_cpp_pin.py b/scripts/test_regen_corpus_cpp_pin.py index 022ed99a..389bced6 100644 --- a/scripts/test_regen_corpus_cpp_pin.py +++ b/scripts/test_regen_corpus_cpp_pin.py @@ -6,7 +6,7 @@ ROOT = Path(__file__).resolve().parents[1] text = (ROOT / "scripts/regen_corpus_cpp.sh").read_text() -commit = "f3285d79f476c029e9739599658b5de6296046bc" +commit = "a4259656d025f36411a7ad1e4ab6c6dd020b780a" if text.count(f'CODEGEN_COMMIT="{commit}"') != 1: raise SystemExit("regen_corpus_cpp: paired codegen commit is not pinned exactly once") if "pineforge-release:latest" in text: @@ -19,4 +19,4 @@ "--network=none", "-e PYTHONPATH=/codegen"): if required not in text: raise SystemExit("regen_corpus_cpp: missing pin enforcement: " + required) -print("regen_corpus_cpp: codegen f3285d7 and immutable runtime image pinned") +print("regen_corpus_cpp: codegen a425965 and immutable runtime image pinned") From 82445f29d2e40cf5c5939d3cd7924850d06d0ca3 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 11:04:32 +0800 Subject: [PATCH 79/83] Re-pin the validation corpus to b40aa8e: owned_strategies.json re-derived for the a4259656 TUs (lane CORPUS4) pineforge-corpus b40aa8e is a7ad2e2 plus one commit. That commit re-derives owned_strategies.json, whose generated_cpp_sha256 the regeneration had left matching 32 of its 60 owned probes. The re-derivation uses the generator and method of corpus f989e36. - 28 digests are rewritten, so all 60 match their TUs again. - validation_run names this branch's previous commit, 3d59fad4: its full parity sweep at a7ad2e2 with codegen a4259656. That commit stays in the history because the manifest cites it. - native_validation, tier_distribution and every other digest re-derive unchanged. b40aa8e keeps a7ad2e2's validation/, data/, draft-probes/ and special-validation/ trees byte-for-byte. Two pins move: - The corpus gitlink moves a7ad2e2 -> b40aa8e. - The `# corpus` header of scripts/corpus_parity_baseline.txt is re-recorded with `corpus_trades_identity.py --update`. That ran over the 312 trade lists this engine re-derives at b40aa8e, after the checker refused on that header. The baseline diff is that one line: all 312 sha256 rows are unchanged. CODEGEN_COMMIT in scripts/regen_corpus_cpp.sh and its source check stay at a4259656: that codegen still reproduces every TU of b40aa8e. The companion pineforge-workflow branch r5/corpus4 (migration 0048) registers b40aa8e's bundle. The corpus PR, this branch and that migration land together. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit cb1bbf3c0323fd492a157a2dfe335ba6d28203e2) --- corpus | 2 +- scripts/corpus_parity_baseline.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/corpus b/corpus index a7ad2e2b..b40aa8ec 160000 --- a/corpus +++ b/corpus @@ -1 +1 @@ -Subproject commit a7ad2e2b6ea83a02d8f95ece5dd32b4ac6f6dc50 +Subproject commit b40aa8ecbe3caa9ad05244cdfd9ee8138da845e4 diff --git a/scripts/corpus_parity_baseline.txt b/scripts/corpus_parity_baseline.txt index 0912f177..6333dedc 100644 --- a/scripts/corpus_parity_baseline.txt +++ b/scripts/corpus_parity_baseline.txt @@ -3,7 +3,7 @@ # ./scripts/check_corpus_parity.sh && \ # python3 scripts/corpus_trades_identity.py --update # and carry the evidence in the commit that moves it. -# corpus a7ad2e2b6ea83a02d8f95ece5dd32b4ac6f6dc50 +# corpus b40aa8ecbe3caa9ad05244cdfd9ee8138da845e4 a72ee904563a1787ca8371f37f98cec74f6fb44163a5dfac169b77755a05ed1a validation/analyzer-anvil-percent-costs-01/engine_trades.csv df7fbdb2082c8982e8c38d8843fc53262df693d4ce8b32bf05bc0acd1ef41a16 validation/analyzer-cashflow-cash-costs-01/engine_trades.csv From af177bc6691db871c4795655ca7c9d5a3f8b843c Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 13:53:35 +0800 Subject: [PATCH 80/83] INT26 (ii): the 1.0.0 entry adds what the late picks change for a user The late picks change behaviour a user can see; the entry says so, one sentence a user can act on per change, naming no lane. - What a native host must handle: a current execution refuses a request bound to a host roster (`UnsupportedRequest`, C 5) where it used to fail the run; the post-fill margin check measures the fill's own segment; two C++ calendar queries (`native_aggregates_input_bars()`, `native_civil_date` / `native_civil_days`), C++ only. - Behaviour: the margin bullet now covers the post-fill waypoint, adds and `process_orders_on_close` / `calc_on_order_fills` openings, the gap-open slice's shared sizing, the magnifier's long and short cases and a margin call's nearest-tick fill; a recalculation's market order or strategy.close at a bar's second extreme; a `process_orders_on_close` stop entry placed while the position still holds the other side; the kernel sampler's excursions for Pine strategies (19 corpus probes, 335 trades: 324 now equal TradingView's, 11 closer -- the corpus re-derived once at b40aa8e on the final tree moves no baseline line, so the probes' trades are byte for byte the ones those counts were taken on); a FIFO strategy.exit's own-entry reservation; `strategy.margin_liquidation_price` reading na with margin calls off; a percent-of-equity quantity under a cash commission sized as TradingView sizes it (order-percent-equity-cash-commission-01: 355 of 366 quantities within TradingView's 0.0001 cell, none before). The sentences it rewrites (the margin bullet, the process_orders_on_close bullet's last clause and its corpus-probe sentence) were INT26 (ii)'s own (89866205). Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 81 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd53cf11..2a408534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,6 +130,24 @@ prerelease included. `NativeMarginCheckKind::IntrabarSample` (C `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE`, 4). A requirement hook sees it; a host that switches on the kind must handle it. +- **A current execution refuses a request bound to a host roster.** + `execute_current` on a request whose owner is a host roster (`BindCohort`) + answers `NativeCurrentRefusal::UnsupportedRequest` (C + `PF_NATIVE_REFUSAL_UNSUPPORTED_REQUEST`, 5) before its point is taken; the + request stays queued and fills at the next match like any queued close. + When the roster held a live lot it used to fail the run. To close a + cohort's lots at once, bind the request to its openings (`BindOpenings`). +- **The post-fill margin check measures the fill's own segment.** The + kernel's `AfterApplied` margin point measures from the waypoint a fill was + reached on the way to, with the fill price as that point's mark, so a limit + or stop filled on the way to a bar's extreme is checked at that extreme on + its own bar. A host with a margin model can book a margin call a bar + earlier than before; a requirement hook sees that point. +- **Two C++ calendar queries.** `NativeStrategyHost::native_aggregates_input_bars()` + says whether the run's script bars are buckets the kernel gathers from its + input bars, and `native_calendar::native_civil_date` / `native_civil_days` + convert between days since the epoch and a civil date. Neither has a C + spelling (the 1.0 C boundary table). - **The C callback table's marker must be zero.** A C host built against the current `pf_native_callbacks_v1` leaves its trailing `reserved1` at 0: `strategy_native_host_create_v1` returns NULL for any other value. Tables of @@ -159,27 +177,60 @@ TradingView behaviour it matches. - A margin call never revives an exit the script cancelled (4b00da92, #287), whether the exit was live, dormant, or a gapped stop a declined reversal had parked for the next margin call (#289). -- Margin: a leveraged position (margin below 100 %) is checked on its own - entry bar, so its margin call lands at that bar's low as TradingView books - it, not a bar later or never (outside `process_orders_on_close`, - `calc_on_order_fills`, the bar magnifier and a timestamped FX curve, which - keep their own schedule). A default `percent_of_equity` stop entry - sized above 100 % keeps the quantity it was sized at when placed (at the - signal close, or at the snapped stop level when not yet marketable), as - TradingView does; it was re-sized at the fill. Under the bar magnifier - (without `process_orders_on_close` or `calc_on_order_fills`), a leveraged - position's margin call lands at the first lower-timeframe low that crosses - its line, and again at each later low that crosses the reduced position's - line. +- Margin: a leveraged position (margin below 100 %) is checked on the rest of + the bar its fill was on, from the fill to the extreme it was heading for + (the low for a long, the high for a short), so its margin call lands where + TradingView books it, not a bar later or never. This holds for an opening + and for an add to a carried position, under `process_orders_on_close` and + `calc_on_order_fills` too; a timestamped FX curve keeps its own schedule. A + default `percent_of_equity` stop entry sized above 100 % keeps the quantity + it was sized at when placed (at the signal close, or at the snapped stop + level when not yet marketable), as TradingView does; it was re-sized at the + fill. A gap-open stop entry's margin slice is sized by the rule every other + margin call takes, its one-contract minimum included. Under the bar + magnifier, the margin call of a leveraged long, or of a short at any margin, + lands at the first lower-timeframe price that crosses its line, and again at + each later one that crosses the reduced position's line, under + `calc_on_order_fills` too and, for a long, under `process_orders_on_close`. + A margin call books the nearest tick of the price it fired at, as + TradingView's market fill does; stop and limit fills keep their directional + rounding. - A trailing `strategy.exit` placed while its entry is still pending starts its running best at the activation, as TradingView's does; on TradingView's tapes it exited 1 to 24 bars late. +- `calc_on_order_fills`: a market order or a `strategy.close` that a + recalculation places at a fill on a bar's second high or low fills there, + as TradingView does; an order an earlier fill of the same bar pushed onto + that extreme still waits for the close or the next open. - `process_orders_on_close`: a stop entry whose stop the placing bar's close already reached, and the bar's only entry marketable there, fills at that close (slippage applied), on the chart, an aggregated chart and under the - bar magnifier; it filled at the next open. + bar magnifier, and so does one placed while the position still holds the + other side, once a `strategy.close` on the same bar has closed it; each + filled at the next open. An add can no longer exceed `pyramiding` because an opposite entry was - resting from an earlier bar. One corpus probe moves toward its TradingView - tape (`order-deferred-flip-pooc-cross-bar-01`). + resting from an earlier bar. The corpus probe + `order-deferred-flip-pooc-cross-bar-01` now books all 792 of its + TradingView tape's trades as TradingView does. - An aggregated chart under the bar magnifier dates every fill at its chart bar's open, as TradingView does. +- A closed trade's run-up and drawdown (`strategy.closedtrades.max_runup` / + `max_drawdown` and the report's columns) come from the kernel's per-lot + sampler for Pine strategies, as for every other host. In 19 corpus probes + the excursions of 335 trades move: 324 now equal TradingView's and 11 are + closer. +- Under FIFO closing, a default `strategy.exit(from_entry)` reserves its own + entry's quantity, oldest lots first, not the whole position, as + TradingView does; where it closed lots of other entries, the trade rows + split differently. +- A `percent_of_equity` default quantity under a cash commission sizes from + `strategy.equity`, with the open entries' fees charged, and keeps back the + fee its own order pays, as TradingView does: per order + `floor((pct × equity − fee) / (price × point value))`, per contract + `floor(pct × equity / (price × point value + fee))`. It used to size too + large. The corpus probe `order-percent-equity-cash-commission-01` now books + 355 of its tape's 366 quantities within TradingView's 0.0001 cell (none + before). +- `strategy.margin_liquidation_price` reads `na` when margin calls are + switched off (`set_margin_call_enabled(false)`, which TradingView has no + counterpart for); it used to return a price. From 7549d4d842b2b6e590a9bcd87dbebcf9e78f1324 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 14:42:30 +0800 Subject: [PATCH 81/83] INT26 v19 hash re-pin The one commit of INT26 that moves pinned hash values. Every late pick's own hash re-pin (PAR-ORDERS-2 9f7a025c, PAR-MARGIN-2 8876c108, H-THIN's hash step ab297364, PAR-CASHFEE's hash step 214c082c) folds into it, as the late-picks list rules; the behaviour those re-pins carried (trades, counts, transcript digests) landed with the picks that move it, so this commit moves hash values alone. Nothing here is an epoch change: v19 is unreleased. How: on the integrated tree each pinned TU is compiled with its own harvest switch (PINEFORGE_R1_HARVEST, PINEFORGE_F9_HARVEST, PINEFORGE_P1_HARVEST, PINEFORGE_P4_HARVEST, PINEFORGE_QUIET_BAR_HARVEST, PINEFORGE_L5_HARVEST, PINEFORGE_P7_HARVEST, PF_HOST_VIEW_MEMO_DUMP, PINEFORGE_D2D_PUBLICATION_HARVEST, PINEFORGE_K1_HARVEST, PINEFORGE_V19E_HARVEST) and the re-pin tool (INT26-scratch/tools/repin_int26.py: INT25's repin_int25.py, which is V19-FIX's, plus the publication witness and the live-state transcripts) rewrites each moved hash value under an appended note. The tool refuses any non-hash change: a moved trade, count, trade digest, fingerprint field, publication fold/trade/row count or live-state transcript is a STOP (mutation runs: a quiet-bar trade count, a quiet-bar trade digest, a publication trade count, a host-view fingerprint's trades and a live-state transcript each stop it). After the write the same harvest reproduces every pin (0 moved) and every witness row passes. Which pick moved each value: the eleven witnesses were harvested at every pick boundary (B0 = PAR-ORDERS' re-pin 1518fa8f, whose pins every later pick kept; then PAR-ORDERS-2, PAR-MARGIN, PAR-MARGIN-2, the CI/INT26/PERF-KEDGE/ CI-RUNNER stretch, H-THIN X15 a+e, H-THIN's hash step, X15 d, X15 f, A4-DUST, H-THIN's docs and floors, PAR-CASHFEE's hash step, and the final tree), and each moved value is marked with the transitions where it changed. B0's harvest reproduces B0's pins; every non-hash value at the final tree equals its pin. | witness | hash values moved | by pick | |---|---:|---| | test_adapter_report_relower | 72 of 72 | H-THIN 72 | | test_adapter_recording_hash_witness | 267 of 267 | H-THIN 267 | | test_adapter_continuation_view | 169 of 227 | H-THIN 168; PAR-ORDERS-2 + H-THIN 1 | | test_adapter_receipts_in_place | 334 of 334 | H-THIN 334 | | test_adapter_quiet_bar | 680 of 816 | H-THIN 553; PAR-ORDERS-2 + H-THIN 69; H-THIN + PAR-CASHFEE 50; PAR-ORDERS-2 + H-THIN + PAR-CASHFEE 8 | | test_adapter_command_allocations | 32 of 32 | H-THIN 32 | | test_adapter_lookup_index_witness | 948 of 948 | H-THIN 948 | | test_adapter_host_view_memo | 13 of 13 | H-THIN 13 | | test_publication_witness | 156 of 156 | H-THIN 98; H-THIN + PAR-CASHFEE 29; PAR-ORDERS-2 + H-THIN 24; PAR-ORDERS-2 + H-THIN + PAR-CASHFEE 4; PAR-MARGIN-2 + H-THIN + PAR-CASHFEE 1 | | test_native_calendar_hash_witness | 0 of 12 | -- | | test_adapter_live_state_equivalence | 0 of 0 | -- | 2671 values in all. Why they move: - H-THIN's hash step (E19, P10): the Pine host keeps the kernel's owns_lot_excursions() default, so the retired host excursion model's state leaves the source-layer fold (pineforge-source-adapter/v4) and every source-layer hash value moves; P10 moves the placement of FIFO exits. - PAR-ORDERS-2: the adapter's transient coof_fill_forced_ is folded while a recalculation runs. - PAR-MARGIN-2: the kernel's post-fill margin point measures from the fill's own waypoint, which moves publication Config03's recorded path (its trades moved with that pick: 50 -> 48). - PAR-CASHFEE: the fold folds the sizing snapshot's strategy.equity wherever a percent-of-equity quantity under a cash fee is recorded (quiet-bar's six cash-fee scenarios, 34 publication runs; their trades moved with that pick). No other pick moves a hash value (PAR-MARGIN, CI-FLAKE, the INT26 items, PERF-KEDGE, CI-RUNNER, H-THIN X15 a/e/d/f and A4-DUST, CORPUS4: 0 each). The Pine witness (PERF0-K's wah replay, spark GCC 13.3 aarch64, release and stress builds): main 0d76a099 14010942914806881059 -> this tree 8577114281377521372, release and stress agreeing -- H-THIN's hash step alone moves it (the lane measured the same pair); PAR-ORDERS-2's fold does not run in it and PAR-CASHFEE leaves it unchanged (8577114281377521372 before and after its picks). Moved (old -> new [pick]; each pinned file carries the same list): test_adapter_report_relower: 72 lines kOrdinary_hashes[0]: 10420073189004151623ull -> 243601838968427296ull [H-THIN] kOrdinary_hashes[1]: 13145042526267837939ull -> 14082045497352522076ull [H-THIN] kOrdinary_hashes[2]: 2622782777793463991ull -> 16788444728701273278ull [H-THIN] kOrdinary_hashes[3]: 17996446729054892726ull -> 4735244315032400709ull [H-THIN] kOrdinary_hashes[4]: 3969181056128787992ull -> 9274703563551556229ull [H-THIN] kOrdinary_hashes[5]: 16947117433974038ull -> 4437548837203718331ull [H-THIN] kOrdinary_hashes[6]: 423735939073054907ull -> 9447280110204500336ull [H-THIN] kOrdinary_hashes[7]: 18056612604689961163ull -> 644150700710845644ull [H-THIN] kOrdinary_hashes[8]: 12445011709498043361ull -> 12856503234686701952ull [H-THIN] kOrdinary_hashes[9]: 16382730376298748267ull -> 8228843622305023308ull [H-THIN] kOrdinary_hashes[10]: 12718112296054108390ull -> 11723584407685805665ull [H-THIN] kOrdinary_hashes[11]: 913929622865489206ull -> 12427688725613086017ull [H-THIN] kOrdinary_hashes[12]: 13218761675008378229ull -> 5717024028566739016ull [H-THIN] kOrdinary_hashes[13]: 10388057217060506235ull -> 9136288938210272546ull [H-THIN] kOrdinary_hashes[14]: 4373273430774112306ull -> 14265105010801500849ull [H-THIN] kOrdinary_hashes[15]: 11946915320520331422ull -> 5913118032193444201ull [H-THIN] kOrdinary_hashes[16]: 2472677882334572745ull -> 2601700310587933770ull [H-THIN] kOrdinary_hashes[17]: 4709222664471492344ull -> 16710877784426826175ull [H-THIN] kOrdinary_hashes[18]: 5785035152764109816ull -> 12222101465241554407ull [H-THIN] kOrdinary_hashes[19]: 15834701310167426825ull -> 1466897115669479138ull [H-THIN] kOrdinary_hashes[20]: 18416054639860847807ull -> 8818039625616895170ull [H-THIN] kOrdinary_hashes[21]: 9654818568811123148ull -> 17382764399684682567ull [H-THIN] kOrdinary_hashes[22]: 7514399512975361670ull -> 5569220102658162789ull [H-THIN] kOrdinary_hashes[23]: 375912627396258150ull -> 5858716210019128261ull [H-THIN] kCalcOnOrderFills_hashes[0]: 13492117694829868536ull -> 8076983691197291309ull [H-THIN] kCalcOnOrderFills_hashes[1]: 12939649384498536620ull -> 2440556825337174781ull [H-THIN] kCalcOnOrderFills_hashes[2]: 18283200234405460746ull -> 17660598049729719549ull [H-THIN] kCalcOnOrderFills_hashes[3]: 17715026773738590286ull -> 584654546328644269ull [H-THIN] kCalcOnOrderFills_hashes[4]: 16652393141204816958ull -> 10522330026569309375ull [H-THIN] kCalcOnOrderFills_hashes[5]: 10681558687161056850ull -> 2930399316675077519ull [H-THIN] kCalcOnOrderFills_hashes[6]: 12828138156283959725ull -> 17808710347154107934ull [H-THIN] kCalcOnOrderFills_hashes[7]: 13337213902076129151ull -> 6715617711192628536ull [H-THIN] kCalcOnOrderFills_hashes[8]: 2897666538282385134ull -> 6587502700890907205ull [H-THIN] kCalcOnOrderFills_hashes[9]: 4846519892874967202ull -> 2192689119498288561ull [H-THIN] kCalcOnOrderFills_hashes[10]: 4182996206551988888ull -> 8664008431780515081ull [H-THIN] kCalcOnOrderFills_hashes[11]: 3828630408241748525ull -> 7930584285132978374ull [H-THIN] kCalcOnOrderFills_hashes[12]: 6013095397646776827ull -> 17064940335416518660ull [H-THIN] kCalcOnOrderFills_hashes[13]: 1201561623500117444ull -> 9016627554631535731ull [H-THIN] kCalcOnOrderFills_hashes[14]: 16339691134963647305ull -> 16088314143365594964ull [H-THIN] kCalcOnOrderFills_hashes[15]: 1360168785605943262ull -> 13783309705914047335ull [H-THIN] kCalcOnOrderFills_hashes[16]: 10446097969456318121ull -> 9827199985728418388ull [H-THIN] kCalcOnOrderFills_hashes[17]: 2226082304527453549ull -> 16177976828038662588ull [H-THIN] kCalcOnOrderFills_hashes[18]: 5506525745160110349ull -> 12444021166412526756ull [H-THIN] kCalcOnOrderFills_hashes[19]: 16314828352609565244ull -> 12728919766761920669ull [H-THIN] kCalcOnOrderFills_hashes[20]: 14800719633294876469ull -> 17328862593452428978ull [H-THIN] kCalcOnOrderFills_hashes[21]: 1572183010617309931ull -> 565050300121405896ull [H-THIN] kCalcOnOrderFills_hashes[22]: 6980933373957006777ull -> 12671085486866481742ull [H-THIN] kCalcOnOrderFills_hashes[23]: 3071340150260882121ull -> 9604569239120914630ull [H-THIN] kSuppressedTail_hashes[0]: 10420073189004151623ull -> 243601838968427296ull [H-THIN] kSuppressedTail_hashes[1]: 13145042526267837939ull -> 14082045497352522076ull [H-THIN] kSuppressedTail_hashes[2]: 2622782777793463991ull -> 16788444728701273278ull [H-THIN] kSuppressedTail_hashes[3]: 17996446729054892726ull -> 4735244315032400709ull [H-THIN] kSuppressedTail_hashes[4]: 3969181056128787992ull -> 9274703563551556229ull [H-THIN] kSuppressedTail_hashes[5]: 16947117433974038ull -> 4437548837203718331ull [H-THIN] kSuppressedTail_hashes[6]: 423735939073054907ull -> 9447280110204500336ull [H-THIN] kSuppressedTail_hashes[7]: 18056612604689961163ull -> 644150700710845644ull [H-THIN] kSuppressedTail_hashes[8]: 12445011709498043361ull -> 12856503234686701952ull [H-THIN] kSuppressedTail_hashes[9]: 16382730376298748267ull -> 8228843622305023308ull [H-THIN] kSuppressedTail_hashes[10]: 12718112296054108390ull -> 11723584407685805665ull [H-THIN] kSuppressedTail_hashes[11]: 913929622865489206ull -> 12427688725613086017ull [H-THIN] kSuppressedTail_hashes[12]: 13218761675008378229ull -> 5717024028566739016ull [H-THIN] kSuppressedTail_hashes[13]: 10388057217060506235ull -> 9136288938210272546ull [H-THIN] kSuppressedTail_hashes[14]: 4373273430774112306ull -> 14265105010801500849ull [H-THIN] kSuppressedTail_hashes[15]: 11946915320520331422ull -> 5913118032193444201ull [H-THIN] kSuppressedTail_hashes[16]: 2472677882334572745ull -> 2601700310587933770ull [H-THIN] kSuppressedTail_hashes[17]: 4709222664471492344ull -> 16710877784426826175ull [H-THIN] kSuppressedTail_hashes[18]: 5785035152764109816ull -> 12222101465241554407ull [H-THIN] kSuppressedTail_hashes[19]: 15834701310167426825ull -> 1466897115669479138ull [H-THIN] kSuppressedTail_hashes[20]: 18416054639860847807ull -> 8818039625616895170ull [H-THIN] kSuppressedTail_hashes[21]: 9654818568811123148ull -> 17382764399684682567ull [H-THIN] kSuppressedTail_hashes[22]: 7514399512975361670ull -> 5569220102658162789ull [H-THIN] kSuppressedTail_hashes[23]: 7096719386424781198ull -> 8558103254181104877ull [H-THIN] test_adapter_recording_hash_witness: 267 lines kSubmit_rows[0]: 8933562200260968840ull -> 8019365971493599225ull [H-THIN] kSubmit_rows[1]: 16496088491414191334ull -> 12084928604454138979ull [H-THIN] kSubmit_rows[2]: 17745030456223078154ull -> 7685642069387429171ull [H-THIN] kSubmit_rows[3]: 15976454321452418827ull -> 3048458165749351592ull [H-THIN] kSubmit_rows[4]: 13867778238676275167ull -> 1016691552216785494ull [H-THIN] kSubmit_rows[5]: 10620212436062486555ull -> 13664491885780078814ull [H-THIN] kSubmit_rows[6]: 16387014991078439144ull -> 9587023827432894395ull [H-THIN] kSubmit_rows[7]: 5372310558802353946ull -> 14524900109868328621ull [H-THIN] kSubmit_rows[8]: 8117928017652874474ull -> 9306730010360799799ull [H-THIN] kSubmit_rows[9]: 12227480580368604280ull -> 10975560481515534245ull [H-THIN] kSubmit_rows[10]: 11671431865371234655ull -> 6175700847996002068ull [H-THIN] kSubmit_rows[11]: 15852753888607827401ull -> 17944617882018938484ull [H-THIN] kSubmit_rows[12]: 4564599765874863441ull -> 89066156438191170ull [H-THIN] kSubmit_rows[13]: 12753515022268993432ull -> 7853223159133263199ull [H-THIN] kSubmit_rows[14]: 16693007526387479527ull -> 16829337964629633742ull [H-THIN] kSubmit_rows[15]: 9553609781489681132ull -> 14048972284943154657ull [H-THIN] kSubmit_rows[16]: 9723295982254899748ull -> 10772403295793355383ull [H-THIN] kSubmit_rows[17]: 9596448656160987273ull -> 12849842554292919416ull [H-THIN] kSubmit_rows[18]: 14172117111020274407ull -> 11523841829240280476ull [H-THIN] kSubmit_rows[19]: 1811404642983946404ull -> 14633701048963800807ull [H-THIN] kSubmit_rows[20]: 6600438489637577460ull -> 13106625599621486697ull [H-THIN] kSubmit_rows[21]: 1823325663201613856ull -> 11578248645351712284ull [H-THIN] kSubmit_rows[22]: 671600216917334430ull -> 12923518206466935124ull [H-THIN] kSubmit_rows[23]: 14450998569007950837ull -> 11664001542269768811ull [H-THIN] kSubmit_rows[24]: 18123601071981623076ull -> 4353270054101958588ull [H-THIN] kSubmit_rows[25]: 5788029478896071429ull -> 12509431446261158271ull [H-THIN] kSubmit_rows[26]: 5791781074908951980ull -> 6827814975693211880ull [H-THIN] kSubmit_rows[27]: 8180458203668078357ull -> 13768158546548443068ull [H-THIN] kSubmit_rows[28]: 10842259428116090863ull -> 6634357882333049268ull [H-THIN] kSubmit_rows[29]: 7789401651194802796ull -> 8521102792149394239ull [H-THIN] kSubmit_rows[30]: 16644199069697802031ull -> 15674750399051881772ull [H-THIN] kSubmit_rows[31]: 13033963886965614456ull -> 7695240294002266835ull [H-THIN] kSubmit_reads[0]: 5783964658104710927ull -> 18234150623076098218ull [H-THIN] kSubmit_reads[1]: 9790324176931117308ull -> 7528636846310271579ull [H-THIN] kSubmit_reads[2]: 8640216615515382983ull -> 6832312803948695488ull [H-THIN] kSubmit_reads[3]: 18016456059351763749ull -> 11582188679677093386ull [H-THIN] kSubmit_reads[4]: 6161691521401770042ull -> 13475769493769981375ull [H-THIN] kSubmit_reads[5]: 11652988695302303084ull -> 10681991124354674742ull [H-THIN] kSubmit_final[0]: 13033963886965614456ull -> 7695240294002266835ull [H-THIN] kReplace_rows[0]: 8933562200260968840ull -> 8019365971493599225ull [H-THIN] kReplace_rows[1]: 3907924323473264241ull -> 10913970432357026228ull [H-THIN] kReplace_rows[2]: 14461884941578386867ull -> 16718022224308212524ull [H-THIN] kReplace_rows[3]: 16511157315480471622ull -> 11959962526167016269ull [H-THIN] kReplace_rows[4]: 3959552939271361991ull -> 13682103540524835918ull [H-THIN] kReplace_rows[5]: 2555551956738490898ull -> 14243367393985337845ull [H-THIN] kReplace_rows[6]: 10502649610878884111ull -> 8014600618664019672ull [H-THIN] kReplace_rows[7]: 18306494604560422814ull -> 12889133748772252011ull [H-THIN] kReplace_rows[8]: 17185448195161859406ull -> 360570764097142565ull [H-THIN] kReplace_rows[9]: 15450494133515205524ull -> 11876071708761659347ull [H-THIN] kReplace_rows[10]: 6827766899990563012ull -> 3851347914344196379ull [H-THIN] kReplace_rows[11]: 181096906100466027ull -> 269582453071901974ull [H-THIN] kReplace_rows[12]: 3679474327996900584ull -> 7374511927462938135ull [H-THIN] kReplace_rows[13]: 5841986886798320038ull -> 14141590806976750289ull [H-THIN] kReplace_rows[14]: 1819868140276584460ull -> 10009811972588039873ull [H-THIN] kReplace_rows[15]: 4420902119403666462ull -> 6680845581363570511ull [H-THIN] kReplace_rows[16]: 13480284355483224649ull -> 14019640910801526228ull [H-THIN] kReplace_rows[17]: 7175317954498263753ull -> 8435723009555847478ull [H-THIN] kReplace_rows[18]: 17073623445577360118ull -> 11806787700855741945ull [H-THIN] kReplace_rows[19]: 17009991270755579471ull -> 4186867479979939080ull [H-THIN] kReplace_rows[20]: 12090343301005594287ull -> 4211449273197151184ull [H-THIN] kReplace_rows[21]: 13041638478193314581ull -> 2665513011878476638ull [H-THIN] kReplace_rows[22]: 1923525511758336863ull -> 6197194006704225848ull [H-THIN] kReplace_rows[23]: 1774789224381390235ull -> 9112509013863788708ull [H-THIN] kReplace_rows[24]: 15509873019280549637ull -> 8040469185102305882ull [H-THIN] kReplace_rows[25]: 18049355505317406500ull -> 2771278037748002351ull [H-THIN] kReplace_rows[26]: 9113574443316558064ull -> 11922167282207411627ull [H-THIN] kReplace_rows[27]: 52835018303832477ull -> 11390247996551829374ull [H-THIN] kReplace_rows[28]: 17600220618483225632ull -> 5206095883568772047ull [H-THIN] kReplace_rows[29]: 4090055281926467129ull -> 2965585984607700150ull [H-THIN] kReplace_rows[30]: 10973342611545588080ull -> 14433906025888897687ull [H-THIN] kReplace_rows[31]: 17124982032285163865ull -> 8339014417427264918ull [H-THIN] kReplace_reads[0]: 2326526227949002076ull -> 18240752413274260441ull [H-THIN] kReplace_reads[1]: 13390056939270062271ull -> 2691016625363928396ull [H-THIN] kReplace_reads[2]: 10021657182331917789ull -> 6810061420294520706ull [H-THIN] kReplace_reads[3]: 14242637685443801194ull -> 9524522377897439163ull [H-THIN] kReplace_reads[4]: 711594472462182014ull -> 7296154269922229361ull [H-THIN] kReplace_reads[5]: 4857350833193818325ull -> 17144192060924016500ull [H-THIN] kReplace_reads[6]: 4317633836685601759ull -> 15841551483731080184ull [H-THIN] kReplace_reads[7]: 1066029825997878730ull -> 3093248295129869043ull [H-THIN] kReplace_final[0]: 17124982032285163865ull -> 8339014417427264918ull [H-THIN] kCancel_rows[0]: 8933562200260968840ull -> 8019365971493599225ull [H-THIN] kCancel_rows[1]: 12907991694751175761ull -> 9755581588236959176ull [H-THIN] kCancel_rows[2]: 15492238351520247563ull -> 8371403224338711542ull [H-THIN] kCancel_rows[3]: 4949954141317803287ull -> 16042211801967482956ull [H-THIN] kCancel_rows[4]: 6869043455093337913ull -> 16802932054993035276ull [H-THIN] kCancel_rows[5]: 16700973384839330835ull -> 13204266665875046502ull [H-THIN] kCancel_rows[6]: 13907977122257783994ull -> 16358615663034135167ull [H-THIN] kCancel_rows[7]: 13690802804532944832ull -> 6541217908632789433ull [H-THIN] kCancel_rows[8]: 6641415404822534272ull -> 16241388624320518101ull [H-THIN] kCancel_rows[9]: 17617700982422257653ull -> 3812797098998110562ull [H-THIN] kCancel_rows[10]: 8957395621418722884ull -> 18060365737908723675ull [H-THIN] kCancel_rows[11]: 15533748013016881700ull -> 1812188758081721615ull [H-THIN] kCancel_rows[12]: 10113927666003718936ull -> 5200369397060719481ull [H-THIN] kCancel_rows[13]: 10748025209891027257ull -> 14528435239011413150ull [H-THIN] kCancel_rows[14]: 7405357053482683011ull -> 16366047349482157266ull [H-THIN] kCancel_rows[15]: 14210999101428183707ull -> 14462275783608418760ull [H-THIN] kCancel_rows[16]: 5089886391592128332ull -> 15465277701067203145ull [H-THIN] kCancel_rows[17]: 1890214210532379459ull -> 10822246540794760664ull [H-THIN] kCancel_rows[18]: 15715071324930605218ull -> 5153708099721087119ull [H-THIN] kCancel_rows[19]: 12760333961958415553ull -> 10351223234449594086ull [H-THIN] kCancel_rows[20]: 1349906610605744215ull -> 17418701808057553558ull [H-THIN] kCancel_rows[21]: 11872699637369758186ull -> 8964634856941185769ull [H-THIN] kCancel_rows[22]: 6278706775858866003ull -> 8066351423005290858ull [H-THIN] kCancel_rows[23]: 8489616688255836383ull -> 17339397096778631582ull [H-THIN] kCancel_rows[24]: 8131243489711240877ull -> 10167217103912949158ull [H-THIN] kCancel_rows[25]: 73524659065323965ull -> 7835551358700961936ull [H-THIN] kCancel_rows[26]: 13120948974577109893ull -> 3057432646794745428ull [H-THIN] kCancel_rows[27]: 13036340438400450584ull -> 3633706464540692363ull [H-THIN] kCancel_rows[28]: 17320509982992261848ull -> 17115428998086447629ull [H-THIN] kCancel_rows[29]: 16001552197493914473ull -> 6180321870109069012ull [H-THIN] kCancel_rows[30]: 12375923388653592921ull -> 13623488446145227464ull [H-THIN] kCancel_rows[31]: 17283281126178883990ull -> 7433824449803764975ull [H-THIN] kCancel_reads[0]: 5066383747043332732ull -> 16017138806894645973ull [H-THIN] kCancel_reads[1]: 14642164039413495816ull -> 14395189169447138845ull [H-THIN] kCancel_reads[2]: 1826995519720684135ull -> 15433550368465758174ull [H-THIN] kCancel_reads[3]: 10600767793424807315ull -> 15319181961208510706ull [H-THIN] kCancel_reads[4]: 18413531801076714261ull -> 7116101502612005176ull [H-THIN] kCancel_reads[5]: 7162595064832704878ull -> 5624598517067648741ull [H-THIN] kCancel_reads[6]: 8771487694918498925ull -> 15626337877376063418ull [H-THIN] kCancel_reads[7]: 3727599241559000403ull -> 12303175751506120794ull [H-THIN] kCancel_reads[8]: 2036989596869214901ull -> 16768996190976851778ull [H-THIN] kCancel_reads[9]: 15857207205534857561ull -> 9166644978234881348ull [H-THIN] kCancel_reads[10]: 5509984618602967315ull -> 18107466278445992216ull [H-THIN] kCancel_reads[11]: 227260307711721321ull -> 512994913100055200ull [H-THIN] kCancel_reads[12]: 14543777879962894095ull -> 9443988914314833904ull [H-THIN] kCancel_reads[13]: 2123158589903514105ull -> 15301609864008799700ull [H-THIN] kCancel_reads[14]: 11488931589024505587ull -> 18376839094291697164ull [H-THIN] kCancel_reads[15]: 6035902334985171665ull -> 3352239734550209384ull [H-THIN] kCancel_reads[16]: 6330997617635358220ull -> 9556582455608271345ull [H-THIN] kCancel_reads[17]: 7622013860668800144ull -> 4597279093679219283ull [H-THIN] kCancel_reads[18]: 7135414897549443327ull -> 12371316346718768834ull [H-THIN] kCancel_final[0]: 17283281126178883990ull -> 7433824449803764975ull [H-THIN] kOca_rows[0]: 8933562200260968840ull -> 8019365971493599225ull [H-THIN] kOca_rows[1]: 16488569157039504391ull -> 3232936220238691146ull [H-THIN] kOca_rows[2]: 15308668032647221442ull -> 8093402965097552867ull [H-THIN] kOca_rows[3]: 11025020666800289870ull -> 16342776694917511399ull [H-THIN] kOca_rows[4]: 17376859899643293076ull -> 1176409931075103109ull [H-THIN] kOca_rows[5]: 18031995174781967338ull -> 14796994139400300899ull [H-THIN] kOca_rows[6]: 9135991105341529701ull -> 4041329064345231230ull [H-THIN] kOca_rows[7]: 12521501026388244289ull -> 18203940660042607242ull [H-THIN] kOca_rows[8]: 17387999442892602416ull -> 11789920335390250025ull [H-THIN] kOca_rows[9]: 17523447275006301316ull -> 12162719525992402073ull [H-THIN] kOca_rows[10]: 700544763663270973ull -> 10376658214325705912ull [H-THIN] kOca_rows[11]: 6419555011776275829ull -> 985387310022141304ull [H-THIN] kOca_rows[12]: 9501522493702417732ull -> 11563487340980545165ull [H-THIN] kOca_rows[13]: 2198037920276336707ull -> 17750573796976151050ull [H-THIN] kOca_rows[14]: 3899965171878021222ull -> 8782861391928203675ull [H-THIN] kOca_rows[15]: 17400108061693215989ull -> 148042859240194154ull [H-THIN] kOca_rows[16]: 6268695127500094535ull -> 2639689295090791304ull [H-THIN] kOca_rows[17]: 15386469441856039635ull -> 10798952018967388820ull [H-THIN] kOca_rows[18]: 1827308857775093108ull -> 13015585966305206737ull [H-THIN] kOca_rows[19]: 9002315039531479122ull -> 619093614529828615ull [H-THIN] kOca_rows[20]: 4336820592134659556ull -> 16432223468538835037ull [H-THIN] kOca_rows[21]: 1568941454279137340ull -> 5709266478406133937ull [H-THIN] kOca_rows[22]: 3586528324041147724ull -> 11031153128114419525ull [H-THIN] kOca_rows[23]: 9921466732535290906ull -> 17759043185727148801ull [H-THIN] kOca_rows[24]: 2210251816763789972ull -> 778153452226410841ull [H-THIN] kOca_rows[25]: 12540125623729258183ull -> 6929057138192640130ull [H-THIN] kOca_rows[26]: 4922139678029842511ull -> 1014477579839442850ull [H-THIN] kOca_rows[27]: 14880777043208119420ull -> 14398065235087572465ull [H-THIN] kOca_rows[28]: 766085567512147215ull -> 10116121127485637382ull [H-THIN] kOca_rows[29]: 12667708910662510744ull -> 9104659107751229673ull [H-THIN] kOca_rows[30]: 17116188563837257134ull -> 12444616329691358185ull [H-THIN] kOca_rows[31]: 18394579596403092954ull -> 14761567357674824267ull [H-THIN] kOca_reads[0]: 3105752370505168555ull -> 15189525287878030606ull [H-THIN] kOca_reads[1]: 13318576850138903270ull -> 12039483643790541963ull [H-THIN] kOca_reads[2]: 4943429593008048082ull -> 16889317257125145965ull [H-THIN] kOca_reads[3]: 7900987300136689534ull -> 14061180146549677313ull [H-THIN] kOca_reads[4]: 16889527581053605563ull -> 3457726981161275090ull [H-THIN] kOca_reads[5]: 8561664608116353912ull -> 15838028967223682041ull [H-THIN] kOca_reads[6]: 10377670066927504148ull -> 11019583341156392335ull [H-THIN] kOca_reads[7]: 14341448323725690491ull -> 15037407705806185768ull [H-THIN] kOca_reads[8]: 9080486838152177794ull -> 17212813844302948315ull [H-THIN] kOca_reads[9]: 3016108360062111013ull -> 8321492686618048122ull [H-THIN] kOca_reads[10]: 14134368882450491401ull -> 12537836709650543420ull [H-THIN] kOca_reads[11]: 12755516166577050941ull -> 5737128231549608782ull [H-THIN] kOca_final[0]: 18394579596403092954ull -> 14761567357674824267ull [H-THIN] kReissue_rows[0]: 11371589716169913082ull -> 5794085833535179433ull [H-THIN] kReissue_rows[1]: 11912421295804525448ull -> 10206102349587901947ull [H-THIN] kReissue_rows[2]: 16162904808422149189ull -> 16740711338258568218ull [H-THIN] kReissue_rows[3]: 1884050390405525809ull -> 157145938453825824ull [H-THIN] kReissue_rows[4]: 1630941943368694258ull -> 11549520461694658189ull [H-THIN] kReissue_rows[5]: 1646315788396378413ull -> 375980444520009614ull [H-THIN] kReissue_rows[6]: 3202536020749475123ull -> 1364356711154384662ull [H-THIN] kReissue_rows[7]: 16739146824408715591ull -> 9267951354912006062ull [H-THIN] kReissue_rows[8]: 12465965272475823462ull -> 18013247113906258621ull [H-THIN] kReissue_rows[9]: 12416019024705234276ull -> 15135278226663906553ull [H-THIN] kReissue_rows[10]: 8019931705879186711ull -> 6395419845332150882ull [H-THIN] kReissue_rows[11]: 10511748891040354060ull -> 2608642583221907887ull [H-THIN] kReissue_rows[12]: 7873647981647748428ull -> 1439014296977039245ull [H-THIN] kReissue_rows[13]: 13583506357494126788ull -> 4897379921696966153ull [H-THIN] kReissue_rows[14]: 6827456748487052757ull -> 13502194017489163244ull [H-THIN] kReissue_rows[15]: 4860432548138722281ull -> 8254807225619272358ull [H-THIN] kReissue_rows[16]: 10318257384229209728ull -> 4041173354564745731ull [H-THIN] kReissue_rows[17]: 13270746490627165070ull -> 5823046275575919511ull [H-THIN] kReissue_rows[18]: 15647754736958033867ull -> 11150667084723241358ull [H-THIN] kReissue_rows[19]: 15257413490644512113ull -> 7066649565605271382ull [H-THIN] kReissue_rows[20]: 3422115173668721064ull -> 9147117666804548605ull [H-THIN] kReissue_rows[21]: 11954333827210587376ull -> 2943969262508479583ull [H-THIN] kReissue_rows[22]: 16717809947662223375ull -> 2242050686395926078ull [H-THIN] kReissue_rows[23]: 16431286081784348505ull -> 18297932585058907168ull [H-THIN] kReissue_rows[24]: 14674774925924029409ull -> 7328278135585233712ull [H-THIN] kReissue_rows[25]: 8888789072620710620ull -> 4496911868369281609ull [H-THIN] kReissue_rows[26]: 7677274857978420999ull -> 4092312770444862044ull [H-THIN] kReissue_rows[27]: 8389711543599766598ull -> 3103138085614071329ull [H-THIN] kReissue_rows[28]: 11866583660361571056ull -> 1640897772442143723ull [H-THIN] kReissue_rows[29]: 8782474388336378853ull -> 11067585398331222410ull [H-THIN] kReissue_rows[30]: 12830062157342008889ull -> 358041756474409844ull [H-THIN] kReissue_rows[31]: 11660101618206053704ull -> 8735900797794446825ull [H-THIN] kReissue_reads[0]: 9397758857253294369ull -> 15035498073180564574ull [H-THIN] kReissue_reads[1]: 14952922796737447431ull -> 6351174026430191720ull [H-THIN] kReissue_reads[2]: 350142526787915315ull -> 17029808830897663162ull [H-THIN] kReissue_reads[3]: 9287618455889763760ull -> 1960056261526077029ull [H-THIN] kReissue_reads[4]: 8049051401679342672ull -> 9359327854260104443ull [H-THIN] kReissue_reads[5]: 7334056035772970331ull -> 9193727488393939674ull [H-THIN] kReissue_reads[6]: 16777030856659722344ull -> 2562235961360585845ull [H-THIN] kReissue_reads[7]: 12861194332908904879ull -> 15091373470769183170ull [H-THIN] kReissue_reads[8]: 7839065249474270945ull -> 9066532592892510086ull [H-THIN] kReissue_reads[9]: 2385401613699651006ull -> 15993610558080257077ull [H-THIN] kReissue_reads[10]: 3151001040546167918ull -> 14590383768983463647ull [H-THIN] kReissue_reads[11]: 6768266663339224688ull -> 14847391113272722757ull [H-THIN] kReissue_reads[12]: 211423388548179255ull -> 9991682699480081944ull [H-THIN] kReissue_reads[13]: 10932414881536382884ull -> 7606589495236393781ull [H-THIN] kReissue_reads[14]: 15509429580401113050ull -> 16170401377191376471ull [H-THIN] kReissue_reads[15]: 272542696402235168ull -> 8660956339548506257ull [H-THIN] kReissue_reads[16]: 12650608718358639686ull -> 17474680937306256469ull [H-THIN] kReissue_reads[17]: 16954864996066484000ull -> 14583099152188767967ull [H-THIN] kReissue_reads[18]: 2072712234614561342ull -> 1043496941119894989ull [H-THIN] kReissue_reads[19]: 15829080338314944658ull -> 13855326176576914141ull [H-THIN] kReissue_reads[20]: 2272828286921366934ull -> 10342641769074306343ull [H-THIN] kReissue_reads[21]: 16041091954465179468ull -> 11480238464600989237ull [H-THIN] kReissue_final[0]: 11660101618206053704ull -> 8735900797794446825ull [H-THIN] kTrail_rows[0]: 11371589716169913082ull -> 5794085833535179433ull [H-THIN] kTrail_rows[1]: 15827480248058921916ull -> 2370845579388248975ull [H-THIN] kTrail_rows[2]: 9179483745920742590ull -> 14433248242147310045ull [H-THIN] kTrail_rows[3]: 6778316597629456467ull -> 6746208057179709300ull [H-THIN] kTrail_rows[4]: 5172328729500611739ull -> 5141474228715449514ull [H-THIN] kTrail_rows[5]: 538732652451172654ull -> 11607255782218151389ull [H-THIN] kTrail_rows[6]: 6878958150024727018ull -> 15859825369433213587ull [H-THIN] kTrail_rows[7]: 8673040670875734808ull -> 14000636333325119541ull [H-THIN] kTrail_rows[8]: 1224102971077480171ull -> 8235513201704559620ull [H-THIN] kTrail_rows[9]: 4557268190807619538ull -> 896476467432039189ull [H-THIN] kTrail_rows[10]: 9136468491734931429ull -> 8318862700865263670ull [H-THIN] kTrail_rows[11]: 6184955078515723301ull -> 7174198796690634212ull [H-THIN] kTrail_rows[12]: 12362884735948053375ull -> 14047279371931353558ull [H-THIN] kTrail_rows[13]: 15431697961609933942ull -> 10303898847732478779ull [H-THIN] kTrail_rows[14]: 1411625524442821644ull -> 10919087129373839423ull [H-THIN] kTrail_rows[15]: 12832334111417447482ull -> 2392471918185386717ull [H-THIN] kTrail_rows[16]: 5598316292658291609ull -> 13975657224456405630ull [H-THIN] kTrail_rows[17]: 1094550630882239090ull -> 5371073164686480833ull [H-THIN] kTrail_rows[18]: 9032605003773573902ull -> 5788193476615953481ull [H-THIN] kTrail_rows[19]: 655750707938363819ull -> 742735857153964210ull [H-THIN] kTrail_rows[20]: 11787846682663979157ull -> 9511387276849347064ull [H-THIN] kTrail_rows[21]: 4271553443345200153ull -> 8387023980895655564ull [H-THIN] kTrail_rows[22]: 13505617900634614461ull -> 12129447825700318114ull [H-THIN] kTrail_rows[23]: 14516777496449283573ull -> 6819473365388645714ull [H-THIN] kTrail_rows[24]: 8741802295233492767ull -> 177891932096105070ull [H-THIN] kTrail_rows[25]: 7191771264792402823ull -> 12347931419050467504ull [H-THIN] kTrail_rows[26]: 407930561309081398ull -> 11096564370558447981ull [H-THIN] kTrail_rows[27]: 10139811205313313301ull -> 5083341745670325276ull [H-THIN] kTrail_rows[28]: 9692372728761498854ull -> 15979402051208842361ull [H-THIN] kTrail_rows[29]: 17629340520421506187ull -> 10031744998823385352ull [H-THIN] kTrail_rows[30]: 12781001967037913295ull -> 2405952843925529234ull [H-THIN] kTrail_rows[31]: 4953750954703410960ull -> 15933138127006431029ull [H-THIN] kTrail_reads[0]: 9397758857253294369ull -> 15035498073180564574ull [H-THIN] kTrail_reads[1]: 8570208265068282686ull -> 2064235671204825645ull [H-THIN] kTrail_final[0]: 4953750954703410960ull -> 15933138127006431029ull [H-THIN] test_adapter_continuation_view: 169 lines k_chart[1]: 17778912298194596574ull -> 2540222023230692795ull [H-THIN] k_chart[3]: 8941018855024971093ull -> 7277266162097189602ull [H-THIN] k_chart[4]: 2898399952537902865ull -> 13929697350787785222ull [H-THIN] k_chart[6]: 2898399952537902865ull -> 13929697350787785222ull [H-THIN] k_magnifier[1]: 14738067743722080804ull -> 10187142414804555211ull [H-THIN] k_magnifier[3]: 2917741806179085516ull -> 13316113226488762715ull [H-THIN] k_magnifier[4]: 13581436790443683042ull -> 489358674406674971ull [H-THIN] k_magnifier[6]: 13581436790443683042ull -> 489358674406674971ull [H-THIN] k_aggregated[1]: 13031512803108236715ull -> 12703961494282243636ull [H-THIN] k_aggregated[2]: 6906214955679447270ull -> 4636245846172056503ull [H-THIN] k_aggregated[4]: 6906214955679447270ull -> 4636245846172056503ull [H-THIN] k_aggregated_magnifier[1]: 7363580706773703120ull -> 6822321611223800121ull [H-THIN] k_aggregated_magnifier[2]: 5380311451522306220ull -> 1968062868302397607ull [H-THIN] k_aggregated_magnifier[4]: 5380311451522306220ull -> 1968062868302397607ull [H-THIN] k_coof[3]: 1856715264914641232ull -> 17725898018428294595ull [H-THIN] k_coof[5]: 9115044940243703863ull -> 8234685298969568092ull [PAR-ORDERS-2, H-THIN] k_coof[6]: 9198850018143016781ull -> 13907186164657073030ull [H-THIN] k_coof[7]: 784137025905909089ull -> 5915100548425272856ull [H-THIN] k_coof[9]: 784137025905909089ull -> 5915100548425272856ull [H-THIN] k_recording[1]: 16309388099732889217ull -> 13909724611150684752ull [H-THIN] k_recording[3]: 14697449118452563264ull -> 6058006111773907271ull [H-THIN] k_recording[4]: 2898399952537902865ull -> 13929697350787785222ull [H-THIN] k_recording[6]: 2898399952537902865ull -> 13929697350787785222ull [H-THIN] k_recording[7]: 11912929120179152844ull -> 14971695095207148331ull [H-THIN] k_recording[8]: 7111499071038382551ull -> 10535748930949530126ull [H-THIN] k_recording[9]: 14395892391241930212ull -> 12991008539092747157ull [H-THIN] k_recording[10]: 9034635987304089269ull -> 15956762843095239782ull [H-THIN] k_recording[11]: 7745035282290493167ull -> 2607245150229644476ull [H-THIN] k_recording[12]: 9653978480792136139ull -> 2184445209065093070ull [H-THIN] k_recording[13]: 6575784925310671623ull -> 2897664079743275822ull [H-THIN] k_recording[14]: 16756306850270906652ull -> 4157996150621490761ull [H-THIN] k_recording[15]: 6791844355462743637ull -> 10537503143714109070ull [H-THIN] k_recording[16]: 9752175874850418240ull -> 13754546407945448507ull [H-THIN] k_recording[17]: 5581794927458177019ull -> 16440431375654315850ull [H-THIN] k_recording[18]: 16367531041072987671ull -> 7003661191077414576ull [H-THIN] k_recording[19]: 5499206076623092635ull -> 11353048097065184288ull [H-THIN] k_recording[20]: 16891823664600062531ull -> 12848736740973846400ull [H-THIN] k_recording[21]: 15306266440153708710ull -> 3161842879106046797ull [H-THIN] k_recording[22]: 8382791724770590634ull -> 10847056918133263985ull [H-THIN] k_recording[23]: 4926336483410460532ull -> 8350647617751486789ull [H-THIN] k_recording[24]: 12823090459845782834ull -> 1856794670313281419ull [H-THIN] k_recording[25]: 10217177223946990874ull -> 3189033237966511301ull [H-THIN] k_recording[26]: 12680312482263936655ull -> 9556919139953515754ull [H-THIN] k_recording[27]: 15032085204869760060ull -> 5681429441061062241ull [H-THIN] k_recording[28]: 7696336452231268012ull -> 10425853480784439841ull [H-THIN] k_recording[29]: 6819477884530924764ull -> 12097190747565164239ull [H-THIN] k_recording[30]: 13932522791731114426ull -> 12637836836690018377ull [H-THIN] k_recording[31]: 15842817164955438950ull -> 14424918618138801803ull [H-THIN] k_recording[32]: 4250764768037495411ull -> 17903030095845904958ull [H-THIN] k_recording[33]: 1760469320874157114ull -> 15510962773836543503ull [H-THIN] k_recording[34]: 7565330933885892856ull -> 16031650111081263649ull [H-THIN] k_recording[35]: 3348650657119335616ull -> 15453192344610479715ull [H-THIN] k_recording[36]: 16026642042438818681ull -> 12604736624722151438ull [H-THIN] k_recording[37]: 15336121108426360137ull -> 17308957480242505390ull [H-THIN] k_recording[38]: 12610087835245307199ull -> 452142121530738962ull [H-THIN] k_recording[39]: 3169655936610324835ull -> 6150193546982553982ull [H-THIN] k_recording[40]: 18005345939863936504ull -> 12917335185226580263ull [H-THIN] k_recording[41]: 10374887129263366256ull -> 3249697982624166735ull [H-THIN] k_recording[42]: 3316605285128650350ull -> 3343978410969230181ull [H-THIN] k_recording[43]: 10411230090089985874ull -> 16247831777829745607ull [H-THIN] k_recording[44]: 2321647345879300563ull -> 7514289197254818298ull [H-THIN] k_recording[45]: 9282855342318040804ull -> 17972948283988009341ull [H-THIN] k_recording[46]: 8782939554936041465ull -> 4872902272073809254ull [H-THIN] k_recording[47]: 9270809468617755726ull -> 16118270799549857939ull [H-THIN] k_recording[48]: 9705564861311910669ull -> 14672005653726242528ull [H-THIN] k_recording[49]: 1984027035687621850ull -> 2133921838620278979ull [H-THIN] k_recording[50]: 5309816640440296119ull -> 13677532362142245502ull [H-THIN] k_recording[51]: 61906512444861614ull -> 7883065542401673469ull [H-THIN] k_recording[52]: 18408253739502061131ull -> 7126839422434123000ull [H-THIN] k_recording[53]: 2900636347809437670ull -> 3907128035298066349ull [H-THIN] k_recording[54]: 8590510826456084479ull -> 4948527793100784708ull [H-THIN] k_recording[55]: 16739036358771975930ull -> 142644456983149613ull [H-THIN] k_recording[56]: 3846505090147632664ull -> 14555917326272639085ull [H-THIN] k_recording[57]: 13346518438036570112ull -> 5212018862612370299ull [H-THIN] k_recording[58]: 3939181335635768762ull -> 865072719653853777ull [H-THIN] k_recording[59]: 1996675722330704212ull -> 14621783374817857573ull [H-THIN] k_recording[60]: 271113896427528122ull -> 12663926541947072239ull [H-THIN] k_recording[61]: 1511893874670543988ull -> 7631475035369278017ull [H-THIN] k_recording[62]: 15973548105797383183ull -> 951171557373679674ull [H-THIN] k_recording[63]: 4392899708297163730ull -> 9891512222295021637ull [H-THIN] k_recording[64]: 11467883812317724901ull -> 14067535954397498792ull [H-THIN] k_recording[65]: 11051413718335140252ull -> 14311549026741467081ull [H-THIN] k_recording[66]: 903533060614176790ull -> 775415503723595045ull [H-THIN] k_recording[67]: 14466209299323094098ull -> 2025287021890762573ull [H-THIN] k_recording[68]: 4010960734010157804ull -> 5576052922734185379ull [H-THIN] k_recording[69]: 5172512134487820157ull -> 1575805364117614574ull [H-THIN] k_recording[70]: 2898399952537902865ull -> 13929697350787785222ull [H-THIN] k_stream[0]: 16458853630937199693ull -> 6675987432147888000ull [H-THIN] k_stream[2]: 15852567477617018112ull -> 15316836834528354089ull [H-THIN] k_stream[3]: 8395254103899656747ull -> 122347678355491576ull [H-THIN] k_stream[4]: 9137153746257222586ull -> 17060189170921966279ull [H-THIN] k_stream[5]: 4321243615010347791ull -> 12998233506120003610ull [H-THIN] k_stream[7]: 4335032133758631355ull -> 5790160451626852554ull [H-THIN] k_stream[8]: 3418642895666580588ull -> 8747488118396803171ull [H-THIN] k_stream[10]: 15778603109418817912ull -> 11301555486053984887ull [H-THIN] k_stream[12]: 8082887755714141501ull -> 16019190197976852534ull [H-THIN] k_stream[13]: 18272796975887582060ull -> 14554266247005678391ull [H-THIN] k_stream[14]: 4620349392656757831ull -> 9863308761378043570ull [H-THIN] k_stream[16]: 11681022048666484124ull -> 3981212442403576087ull [H-THIN] k_stream[17]: 1110410394930231379ull -> 16893712934576968012ull [H-THIN] k_stream[18]: 8353289200898942510ull -> 17103134812120181635ull [H-THIN] k_stream[19]: 5464661787014092283ull -> 2716679541586828074ull [H-THIN] k_stream[21]: 17913569146247237990ull -> 2142815117417671925ull [H-THIN] k_stream[22]: 14252084119842247317ull -> 1297101144475427900ull [H-THIN] k_stream[24]: 14984524306684102418ull -> 17860190683404402179ull [H-THIN] k_stream[26]: 8427843384471180011ull -> 12743930332679229528ull [H-THIN] k_stream[27]: 12312569890916686665ull -> 13672064593010625780ull [H-THIN] k_stream[28]: 7806471840435792838ull -> 15322331001617686957ull [H-THIN] k_stream[29]: 10841176947711978698ull -> 2540452936526860029ull [H-THIN] k_stream[31]: 15796138674937755121ull -> 5707160760001918266ull [H-THIN] k_stream[32]: 4930499034900633496ull -> 17256519628932910437ull [H-THIN] k_stream[33]: 11894749826123257324ull -> 1265742856577179629ull [H-THIN] k_stream[35]: 6209022505833017115ull -> 8935675646451046114ull [H-THIN] k_stream[36]: 13004202639896233962ull -> 1995113508368758049ull [H-THIN] k_stream[37]: 1029059189446661931ull -> 12126669220334860080ull [H-THIN] k_stream[39]: 9236479431486641300ull -> 13100650686759242355ull [H-THIN] k_stream[40]: 5209128789544115679ull -> 1320523439935521014ull [H-THIN] k_stream[41]: 5582126543454279956ull -> 10835572324283436971ull [H-THIN] k_stream[43]: 16435999169585034112ull -> 411402308123821837ull [H-THIN] k_stream[44]: 2858404675926398753ull -> 18325644631502656152ull [H-THIN] k_stream[45]: 2863207613356019619ull -> 6533533400826708814ull [H-THIN] k_stream[47]: 1651019462700250297ull -> 11035145778810736216ull [H-THIN] k_stream[48]: 9342015881796079139ull -> 4664106270365911266ull [H-THIN] k_stream[49]: 971242319250035563ull -> 6512545530849110454ull [H-THIN] k_stream[51]: 971242319250035563ull -> 6512545530849110454ull [H-THIN] k_stream[53]: 971242319250035563ull -> 6512545530849110454ull [H-THIN] k_stream_aborted[0]: 16458853630937199693ull -> 6675987432147888000ull [H-THIN] k_stream_aborted[2]: 15852567477617018112ull -> 15316836834528354089ull [H-THIN] k_stream_aborted[3]: 8395254103899656747ull -> 122347678355491576ull [H-THIN] k_stream_aborted[4]: 9137153746257222586ull -> 17060189170921966279ull [H-THIN] k_stream_aborted[5]: 4321243615010347791ull -> 12998233506120003610ull [H-THIN] k_stream_aborted[7]: 4335032133758631355ull -> 5790160451626852554ull [H-THIN] k_stream_aborted[8]: 3418642895666580588ull -> 8747488118396803171ull [H-THIN] k_stream_aborted[10]: 15778603109418817912ull -> 11301555486053984887ull [H-THIN] k_stream_aborted[12]: 8082887755714141501ull -> 16019190197976852534ull [H-THIN] k_stream_aborted[13]: 18272796975887582060ull -> 14554266247005678391ull [H-THIN] k_stream_aborted[14]: 4620349392656757831ull -> 9863308761378043570ull [H-THIN] k_stream_aborted[16]: 11681022048666484124ull -> 3981212442403576087ull [H-THIN] k_stream_aborted[17]: 1110410394930231379ull -> 16893712934576968012ull [H-THIN] k_stream_aborted[18]: 8353289200898942510ull -> 17103134812120181635ull [H-THIN] k_stream_aborted[19]: 5464661787014092283ull -> 2716679541586828074ull [H-THIN] k_stream_aborted[21]: 17913569146247237990ull -> 2142815117417671925ull [H-THIN] k_stream_aborted[22]: 14252084119842247317ull -> 1297101144475427900ull [H-THIN] k_stream_aborted[24]: 14984524306684102418ull -> 17860190683404402179ull [H-THIN] k_stream_aborted[26]: 8427843384471180011ull -> 12743930332679229528ull [H-THIN] k_stream_aborted[27]: 12312569890916686665ull -> 13672064593010625780ull [H-THIN] k_stream_aborted[28]: 7806471840435792838ull -> 15322331001617686957ull [H-THIN] k_stream_aborted[29]: 10841176947711978698ull -> 2540452936526860029ull [H-THIN] k_stream_aborted[31]: 15796138674937755121ull -> 5707160760001918266ull [H-THIN] k_stream_aborted[32]: 4930499034900633496ull -> 17256519628932910437ull [H-THIN] k_stream_aborted[34]: 4930499034900633496ull -> 17256519628932910437ull [H-THIN] k_aborted[1]: 17778912298194596574ull -> 2540222023230692795ull [H-THIN] k_aborted[3]: 12754404695042992156ull -> 8965275888317139301ull [H-THIN] k_aborted[5]: 12754404695042992156ull -> 8965275888317139301ull [H-THIN] k_reused[1]: 17778912298194596574ull -> 2540222023230692795ull [H-THIN] k_reused[3]: 8941018855024971093ull -> 7277266162097189602ull [H-THIN] k_reused[5]: 17778912298194596574ull -> 2540222023230692795ull [H-THIN] k_reused[7]: 8941018855024971093ull -> 7277266162097189602ull [H-THIN] k_reused[8]: 2898399952537902865ull -> 13929697350787785222ull [H-THIN] k_reused[10]: 2898399952537902865ull -> 13929697350787785222ull [H-THIN] k_reused[11]: 4063572268643314481ull -> 3045160088509885646ull [H-THIN] k_reused[12]: 4620349392656757831ull -> 9863308761378043570ull [H-THIN] k_reused[14]: 4620349392656757831ull -> 9863308761378043570ull [H-THIN] k_refused[1]: 2898399952537902865ull -> 13929697350787785222ull [H-THIN] k_refused[3]: 2898399952537902865ull -> 13929697350787785222ull [H-THIN] k_refused[5]: 17778912298194596574ull -> 2540222023230692795ull [H-THIN] k_refused[7]: 8941018855024971093ull -> 7277266162097189602ull [H-THIN] k_refused[8]: 2898399952537902865ull -> 13929697350787785222ull [H-THIN] k_refused[10]: 2898399952537902865ull -> 13929697350787785222ull [H-THIN] test_adapter_receipts_in_place: 334 lines kDeclined_rows[0]: 16367413721785097631ull -> 10078075251929413260ull [H-THIN] kDeclined_rows[1]: 8673411908040166317ull -> 9021013497480718974ull [H-THIN] kDeclined_rows[2]: 711509299982175852ull -> 5211498519342463915ull [H-THIN] kDeclined_rows[3]: 7197939734931235205ull -> 8693678377712780894ull [H-THIN] kDeclined_rows[4]: 7029670217763137463ull -> 3263497715304832018ull [H-THIN] kDeclined_rows[5]: 3635687300847380421ull -> 4887532512140189270ull [H-THIN] kDeclined_rows[6]: 16534761561291369739ull -> 3474228148466484630ull [H-THIN] kDeclined_rows[7]: 10876124900683281275ull -> 8939974732258322358ull [H-THIN] kDeclined_rows[8]: 10020927749345911019ull -> 2229197510643109192ull [H-THIN] kDeclined_rows[9]: 8691527508212156618ull -> 5373392566564228697ull [H-THIN] kDeclined_final[0]: 8691527508212156618ull -> 5373392566564228697ull [H-THIN] kDeclinedMag_rows[0]: 17043975752116642733ull -> 16782433437025871990ull [H-THIN] kDeclinedMag_rows[1]: 10606185829452660874ull -> 1222497984017113931ull [H-THIN] kDeclinedMag_rows[2]: 15448675564357825832ull -> 13459087267571170739ull [H-THIN] kDeclinedMag_rows[3]: 15175044830743125885ull -> 9516889388168741832ull [H-THIN] kDeclinedMag_rows[4]: 6450443484594143151ull -> 7518654345809068380ull [H-THIN] kDeclinedMag_rows[5]: 2398923910778006973ull -> 11893247087917015032ull [H-THIN] kDeclinedMag_rows[6]: 8459031541244945398ull -> 12055886405220663097ull [H-THIN] kDeclinedMag_rows[7]: 1773947867498079483ull -> 10055241910018728642ull [H-THIN] kDeclinedMag_rows[8]: 15294623478765439011ull -> 12275394792888174860ull [H-THIN] kDeclinedMag_rows[9]: 483092226467101870ull -> 9368999989622318001ull [H-THIN] kDeclinedMag_final[0]: 483092226467101870ull -> 9368999989622318001ull [H-THIN] kRevive_rows[0]: 13340112391979823686ull -> 13853830474015623389ull [H-THIN] kRevive_rows[1]: 3080039388426713206ull -> 4728749920725358539ull [H-THIN] kRevive_rows[2]: 7902869859755540775ull -> 4849688479110990770ull [H-THIN] kRevive_rows[3]: 18430037521897822255ull -> 16929648573929730497ull [H-THIN] kRevive_rows[4]: 8604888200074309324ull -> 1041650372773372953ull [H-THIN] kRevive_rows[5]: 10980118139031631968ull -> 5462196043591698097ull [H-THIN] kRevive_rows[6]: 17030525150602589392ull -> 2747991239711285631ull [H-THIN] kRevive_final[0]: 17030525150602589392ull -> 2747991239711285631ull [H-THIN] kReviveMag_rows[0]: 9323387230543354105ull -> 832728159842300632ull [H-THIN] kReviveMag_rows[1]: 17230544106411439877ull -> 10054388176425177056ull [H-THIN] kReviveMag_rows[2]: 8558481171000670347ull -> 18222497838575167482ull [H-THIN] kReviveMag_rows[3]: 11778096795536835427ull -> 7009173660391284590ull [H-THIN] kReviveMag_rows[4]: 6288134072033682736ull -> 13277765918702967861ull [H-THIN] kReviveMag_rows[5]: 5799450267550970898ull -> 6034130187223021955ull [H-THIN] kReviveMag_rows[6]: 16015312550992593014ull -> 9402276759429607197ull [H-THIN] kReviveMag_final[0]: 16015312550992593014ull -> 9402276759429607197ull [H-THIN] kCascade_rows[0]: 551096499902803590ull -> 11665685740161685773ull [H-THIN] kCascade_rows[1]: 6515737401085179265ull -> 6762973723098471536ull [H-THIN] kCascade_rows[2]: 13117702113570278612ull -> 13194180830256499995ull [H-THIN] kCascade_rows[3]: 1257083709566184300ull -> 5915515851064168701ull [H-THIN] kCascade_rows[4]: 1236032771344307983ull -> 14468652264495611008ull [H-THIN] kCascade_rows[5]: 17545089905645669552ull -> 4582123328933630207ull [H-THIN] kCascade_rows[6]: 11022588141404321180ull -> 16894420136553841491ull [H-THIN] kCascade_final[0]: 11022588141404321180ull -> 16894420136553841491ull [H-THIN] kCascadeMag_rows[0]: 15338041165786710953ull -> 10293226467739870528ull [H-THIN] kCascadeMag_rows[1]: 9326370104822563476ull -> 15440279261765505431ull [H-THIN] kCascadeMag_rows[2]: 2546778736934085057ull -> 3033096808895778346ull [H-THIN] kCascadeMag_rows[3]: 15492970885942622778ull -> 8603410363956848503ull [H-THIN] kCascadeMag_rows[4]: 16758361987609330968ull -> 3716382050228218553ull [H-THIN] kCascadeMag_rows[5]: 4624738216223074340ull -> 15049237438778114557ull [H-THIN] kCascadeMag_rows[6]: 2074679355477699332ull -> 3591493413416270605ull [H-THIN] kCascadeMag_final[0]: 2074679355477699332ull -> 3591493413416270605ull [H-THIN] kBrackets_rows[0]: 16217277159719941353ull -> 7500883169712558290ull [H-THIN] kBrackets_rows[1]: 8338318974496761540ull -> 11727648583129089083ull [H-THIN] kBrackets_rows[2]: 4387288109901666172ull -> 6413647876750793739ull [H-THIN] kBrackets_rows[3]: 17194843007024741926ull -> 16308656194518151033ull [H-THIN] kBrackets_rows[4]: 13764914223976981334ull -> 16727135511009534069ull [H-THIN] kBrackets_rows[5]: 5165427215729956474ull -> 6044943424043481425ull [H-THIN] kBrackets_rows[6]: 4634967915219578727ull -> 14841136291072554584ull [H-THIN] kBrackets_rows[7]: 16692219291368299752ull -> 1773096913540911661ull [H-THIN] kBrackets_rows[8]: 14748223541663733754ull -> 17033478288745263781ull [H-THIN] kBrackets_rows[9]: 3456524395963272453ull -> 17415237666140008114ull [H-THIN] kBrackets_rows[10]: 12434621022045497961ull -> 14903371499312557794ull [H-THIN] kBrackets_rows[11]: 16335369385158554819ull -> 1559388622250313888ull [H-THIN] kBrackets_rows[12]: 12991044494042544886ull -> 11339295434477982165ull [H-THIN] kBrackets_rows[13]: 4973527342518025567ull -> 5188816558658073040ull [H-THIN] kBrackets_rows[14]: 17883762509965744260ull -> 10562336006974642565ull [H-THIN] kBrackets_rows[15]: 2061839802394366722ull -> 9944319438386404069ull [H-THIN] kBrackets_rows[16]: 9666386366910181655ull -> 18087204908683637838ull [H-THIN] kBrackets_rows[17]: 4436881865564527894ull -> 6643491299232430087ull [H-THIN] kBrackets_rows[18]: 16715420632714991716ull -> 6148826652091198175ull [H-THIN] kBrackets_rows[19]: 1529353304450412038ull -> 4758323598983849211ull [H-THIN] kBrackets_rows[20]: 1035717575522872886ull -> 2642662310786859897ull [H-THIN] kBrackets_rows[21]: 16225013858806894718ull -> 595312524362424357ull [H-THIN] kBrackets_rows[22]: 8409169559076599494ull -> 14890548070825444249ull [H-THIN] kBrackets_rows[23]: 9707763992756481156ull -> 3533069104010016939ull [H-THIN] kBrackets_rows[24]: 2831826005880704344ull -> 1617991145468681997ull [H-THIN] kBrackets_rows[25]: 17431835404196442687ull -> 10622401576246976900ull [H-THIN] kBrackets_rows[26]: 9788230484951507538ull -> 8817393790173822955ull [H-THIN] kBrackets_rows[27]: 13167836072962096695ull -> 16364625047298717046ull [H-THIN] kBrackets_rows[28]: 9063096009681232646ull -> 14563367105470732551ull [H-THIN] kBrackets_rows[29]: 5410108115990088215ull -> 4971637099003501106ull [H-THIN] kBrackets_rows[30]: 13040426354353828713ull -> 7489452283312605238ull [H-THIN] kBrackets_rows[31]: 14801076939233187415ull -> 12683946654012290214ull [H-THIN] kBrackets_rows[32]: 16815914518686289691ull -> 17971398269388980488ull [H-THIN] kBrackets_rows[33]: 2747558237080166725ull -> 16748341084474510752ull [H-THIN] kBrackets_rows[34]: 8660899489027983548ull -> 824168580839598771ull [H-THIN] kBrackets_rows[35]: 17102303971583026849ull -> 17966172105988324718ull [H-THIN] kBrackets_rows[36]: 15533897202822525630ull -> 12921910347520578453ull [H-THIN] kBrackets_rows[37]: 3654409746117594394ull -> 10997125561879301907ull [H-THIN] kBrackets_rows[38]: 13081935113382965168ull -> 6586432521644155739ull [H-THIN] kBrackets_rows[39]: 10127071882762265462ull -> 5868434502871598073ull [H-THIN] kBrackets_final[0]: 10127071882762265462ull -> 5868434502871598073ull [H-THIN] kBracketsMag_rows[0]: 16815971932027180960ull -> 7509436481287183131ull [H-THIN] kBracketsMag_rows[1]: 8203166635201027950ull -> 9787443206195299241ull [H-THIN] kBracketsMag_rows[2]: 17138741981140426534ull -> 9468051353845057081ull [H-THIN] kBracketsMag_rows[3]: 1925655849137931056ull -> 11617996857175938359ull [H-THIN] kBracketsMag_rows[4]: 14052428529967425108ull -> 17297388508275178983ull [H-THIN] kBracketsMag_rows[5]: 9832333236513193860ull -> 8305808312144255951ull [H-THIN] kBracketsMag_rows[6]: 16191684320998506441ull -> 6113367866531691422ull [H-THIN] kBracketsMag_rows[7]: 8371860663058915477ull -> 9106991683344179872ull [H-THIN] kBracketsMag_rows[8]: 8552593071486288536ull -> 14803910782973314363ull [H-THIN] kBracketsMag_rows[9]: 1454522877014641667ull -> 8372038073428945080ull [H-THIN] kBracketsMag_rows[10]: 2707471427108666620ull -> 7616171107040094915ull [H-THIN] kBracketsMag_rows[11]: 16372300294283464170ull -> 10072793551676710573ull [H-THIN] kBracketsMag_rows[12]: 13159511447411425168ull -> 202129580709274867ull [H-THIN] kBracketsMag_rows[13]: 7564232112284716309ull -> 4132398015409106530ull [H-THIN] kBracketsMag_rows[14]: 1543613811599180298ull -> 4510478381903057991ull [H-THIN] kBracketsMag_rows[15]: 15537720168749660922ull -> 8742602620032897417ull [H-THIN] kBracketsMag_rows[16]: 1622379678095332862ull -> 9123990550757961143ull [H-THIN] kBracketsMag_rows[17]: 12005994129115025487ull -> 11712877018079407950ull [H-THIN] kBracketsMag_rows[18]: 5070356265405608919ull -> 2366334284611481108ull [H-THIN] kBracketsMag_rows[19]: 13665825986379254999ull -> 13240125918553875754ull [H-THIN] kBracketsMag_rows[20]: 5146373771364769642ull -> 9789183794260015895ull [H-THIN] kBracketsMag_rows[21]: 6239145605357339126ull -> 14154778537780493947ull [H-THIN] kBracketsMag_rows[22]: 3824110391792374830ull -> 17674589416348533163ull [H-THIN] kBracketsMag_rows[23]: 6558902854431182828ull -> 12580566125972638241ull [H-THIN] kBracketsMag_rows[24]: 8689860431737549825ull -> 12408515542794035006ull [H-THIN] kBracketsMag_rows[25]: 7395806610126361265ull -> 12236372317329550998ull [H-THIN] kBracketsMag_rows[26]: 4003508704365535789ull -> 18175585318787098632ull [H-THIN] kBracketsMag_rows[27]: 15338849660478731120ull -> 10989358971982145541ull [H-THIN] kBracketsMag_rows[28]: 12122897657346658873ull -> 12151142627304375048ull [H-THIN] kBracketsMag_rows[29]: 14082864916763771424ull -> 7079152462663640793ull [H-THIN] kBracketsMag_rows[30]: 4695176159045771576ull -> 9430854713541920393ull [H-THIN] kBracketsMag_rows[31]: 11640860124898684186ull -> 10120897727742141847ull [H-THIN] kBracketsMag_rows[32]: 7934415857533705714ull -> 6015137363156052521ull [H-THIN] kBracketsMag_rows[33]: 2180712887517991172ull -> 9143335679527440329ull [H-THIN] kBracketsMag_rows[34]: 13696242844024611290ull -> 1965209665081157387ull [H-THIN] kBracketsMag_rows[35]: 4388532892304967463ull -> 16559717930979642786ull [H-THIN] kBracketsMag_rows[36]: 886913764131836035ull -> 6332179897502099272ull [H-THIN] kBracketsMag_rows[37]: 6026087689782884746ull -> 1448475308516474107ull [H-THIN] kBracketsMag_rows[38]: 7992738227861920074ull -> 7560963980189861213ull [H-THIN] kBracketsMag_rows[39]: 2507370523932097816ull -> 16163234093112718807ull [H-THIN] kBracketsMag_final[0]: 2507370523932097816ull -> 16163234093112718807ull [H-THIN] kGroups_rows[0]: 428451890117060597ull -> 16343339797501156050ull [H-THIN] kGroups_rows[1]: 16045090594768170214ull -> 4412441122235541001ull [H-THIN] kGroups_rows[2]: 18166866907386903819ull -> 8796315700613324416ull [H-THIN] kGroups_rows[3]: 6958941312543333314ull -> 7395320384889894535ull [H-THIN] kGroups_rows[4]: 12357763379855329333ull -> 14779897915981054858ull [H-THIN] kGroups_rows[5]: 10319108494843725228ull -> 12902321761157770999ull [H-THIN] kGroups_rows[6]: 8057235825342986589ull -> 13233007585699491660ull [H-THIN] kGroups_rows[7]: 775027953877311050ull -> 4759091960862828143ull [H-THIN] kGroups_rows[8]: 14610483346660466821ull -> 9823824249826547972ull [H-THIN] kGroups_rows[9]: 1198104509264338521ull -> 10222432195471458288ull [H-THIN] kGroups_rows[10]: 15842771393076564009ull -> 12268080935222816256ull [H-THIN] kGroups_rows[11]: 5342656873780671151ull -> 1820590039511046510ull [H-THIN] kGroups_rows[12]: 2165268433984104ull -> 13378031530146436277ull [H-THIN] kGroups_rows[13]: 16270682949794220379ull -> 12953190010614641206ull [H-THIN] kGroups_rows[14]: 14994318600896901474ull -> 1053166407871468103ull [H-THIN] kGroups_rows[15]: 3023615581009694835ull -> 9272374160641534696ull [H-THIN] kGroups_rows[16]: 13496533941019930808ull -> 10364329263136600283ull [H-THIN] kGroups_rows[17]: 6174715221277441970ull -> 4225956051004008949ull [H-THIN] kGroups_rows[18]: 5113739070112146000ull -> 7108588782921604133ull [H-THIN] kGroups_rows[19]: 1772495940269705534ull -> 5489213286228074399ull [H-THIN] kGroups_rows[20]: 10686035606077590379ull -> 10616002336161510798ull [H-THIN] kGroups_rows[21]: 17379645375024748460ull -> 9622546459129830553ull [H-THIN] kGroups_rows[22]: 3810215059026298411ull -> 944039958041978562ull [H-THIN] kGroups_rows[23]: 6980015280280300871ull -> 15116276931277952764ull [H-THIN] kGroups_rows[24]: 17653590613423189804ull -> 10604949906509802561ull [H-THIN] kGroups_rows[25]: 5097055414090822762ull -> 2073356378818895203ull [H-THIN] kGroups_rows[26]: 16040617079938685278ull -> 2582840377395668879ull [H-THIN] kGroups_rows[27]: 7474835689400851509ull -> 2562757827364035110ull [H-THIN] kGroups_rows[28]: 3588654748202396640ull -> 12699965404948142476ull [H-THIN] kGroups_rows[29]: 482256096822295700ull -> 1195332705294901458ull [H-THIN] kGroups_rows[30]: 491618572355040023ull -> 16972648823711764497ull [H-THIN] kGroups_rows[31]: 15447520409861182807ull -> 4481092489104066192ull [H-THIN] kGroups_rows[32]: 13546661642780035054ull -> 7005130885374206459ull [H-THIN] kGroups_rows[33]: 586417508450537570ull -> 6610411880574253007ull [H-THIN] kGroups_rows[34]: 16805284807511709227ull -> 3204572366595249610ull [H-THIN] kGroups_rows[35]: 14582173341406959084ull -> 2120665229545761995ull [H-THIN] kGroups_rows[36]: 4670485499468050406ull -> 6282585683572106081ull [H-THIN] kGroups_rows[37]: 8464039108905730677ull -> 12064630322548111510ull [H-THIN] kGroups_rows[38]: 16757005045255032303ull -> 1478230385172911488ull [H-THIN] kGroups_rows[39]: 14459029476982242904ull -> 11603812731166869657ull [H-THIN] kGroups_final[0]: 14459029476982242904ull -> 11603812731166869657ull [H-THIN] kGroupsMag_rows[0]: 18395077400277767983ull -> 2680312739779497464ull [H-THIN] kGroupsMag_rows[1]: 12064304672483746293ull -> 822058034361535600ull [H-THIN] kGroupsMag_rows[2]: 14523426078186844972ull -> 3962469670734696157ull [H-THIN] kGroupsMag_rows[3]: 8862832225214863179ull -> 17411473840245726690ull [H-THIN] kGroupsMag_rows[4]: 14588621505662414032ull -> 5739216726257722739ull [H-THIN] kGroupsMag_rows[5]: 4447156622707225162ull -> 4823834691276900811ull [H-THIN] kGroupsMag_rows[6]: 4361493804042466119ull -> 14557402943266793758ull [H-THIN] kGroupsMag_rows[7]: 1804450653994911768ull -> 17153420186503897123ull [H-THIN] kGroupsMag_rows[8]: 14783451729418173724ull -> 11456037253658883689ull [H-THIN] kGroupsMag_rows[9]: 8082972508033422197ull -> 2071497720185716146ull [H-THIN] kGroupsMag_rows[10]: 8979544420957768909ull -> 10228359593541992710ull [H-THIN] kGroupsMag_rows[11]: 7493375864971444891ull -> 8164374031973170202ull [H-THIN] kGroupsMag_rows[12]: 2222992351194986144ull -> 14741153401126253421ull [H-THIN] kGroupsMag_rows[13]: 11771896598749089907ull -> 5321723665543660526ull [H-THIN] kGroupsMag_rows[14]: 12560649180545194470ull -> 13847739486469904387ull [H-THIN] kGroupsMag_rows[15]: 14421558832844187772ull -> 5160912244748048563ull [H-THIN] kGroupsMag_rows[16]: 4296761660805133425ull -> 9669498150986227540ull [H-THIN] kGroupsMag_rows[17]: 742061714794648722ull -> 8947635553433824973ull [H-THIN] kGroupsMag_rows[18]: 14109205439874640145ull -> 12178890551572236268ull [H-THIN] kGroupsMag_rows[19]: 6057192239224243194ull -> 5673400076925890495ull [H-THIN] kGroupsMag_rows[20]: 18247263908222071868ull -> 15315129035357191511ull [H-THIN] kGroupsMag_rows[21]: 17174776223039553865ull -> 13281596230891049428ull [H-THIN] kGroupsMag_rows[22]: 13727420337195600730ull -> 4615956555268131813ull [H-THIN] kGroupsMag_rows[23]: 2818312375125287588ull -> 5864244316711809037ull [H-THIN] kGroupsMag_rows[24]: 9915087812300743522ull -> 1805681795225876151ull [H-THIN] kGroupsMag_rows[25]: 7161399393808283031ull -> 3885225909948070556ull [H-THIN] kGroupsMag_rows[26]: 2862680592861432951ull -> 16301392942591338516ull [H-THIN] kGroupsMag_rows[27]: 1307007383947855281ull -> 16771933549340211026ull [H-THIN] kGroupsMag_rows[28]: 18356049985413183826ull -> 1873451560715463970ull [H-THIN] kGroupsMag_rows[29]: 15935937615335005856ull -> 8712225159834374858ull [H-THIN] kGroupsMag_rows[30]: 16415296467281494110ull -> 3729496159990057708ull [H-THIN] kGroupsMag_rows[31]: 16803171265864625638ull -> 1612160720622215895ull [H-THIN] kGroupsMag_rows[32]: 10957401283361829601ull -> 5717348211442227590ull [H-THIN] kGroupsMag_rows[33]: 11252967136817419064ull -> 2472676727577223345ull [H-THIN] kGroupsMag_rows[34]: 4760661672048575327ull -> 8470082562618443630ull [H-THIN] kGroupsMag_rows[35]: 14915144339907298167ull -> 1840587632070777498ull [H-THIN] kGroupsMag_rows[36]: 575503570718308907ull -> 15629226293335228938ull [H-THIN] kGroupsMag_rows[37]: 5741952581690045810ull -> 2022458278016162571ull [H-THIN] kGroupsMag_rows[38]: 1055864288829102030ull -> 13820365368293950247ull [H-THIN] kGroupsMag_rows[39]: 16431710829567235323ull -> 7247005295497143736ull [H-THIN] kGroupsMag_final[0]: 16431710829567235323ull -> 7247005295497143736ull [H-THIN] kStopLimit_rows[0]: 4402211772532359251ull -> 10950004051556324382ull [H-THIN] kStopLimit_rows[1]: 5449557300220864793ull -> 14490347146816688448ull [H-THIN] kStopLimit_rows[2]: 2575787073227669374ull -> 17137431077388642227ull [H-THIN] kStopLimit_rows[3]: 9938418473191636202ull -> 2067378368474728109ull [H-THIN] kStopLimit_rows[4]: 10058127381824367565ull -> 11488424314019716432ull [H-THIN] kStopLimit_rows[5]: 1315470856055524560ull -> 8954327035592863039ull [H-THIN] kStopLimit_rows[6]: 15263698702667167777ull -> 7512665423720105326ull [H-THIN] kStopLimit_rows[7]: 13220310601440668918ull -> 4187947268963235259ull [H-THIN] kStopLimit_rows[8]: 15409637845860903908ull -> 2913839783495075163ull [H-THIN] kStopLimit_rows[9]: 6446798858981198964ull -> 17362053743420220567ull [H-THIN] kStopLimit_rows[10]: 10451839227886399841ull -> 18277069102810285906ull [H-THIN] kStopLimit_rows[11]: 11816001997725235374ull -> 6966128010082440393ull [H-THIN] kStopLimit_rows[12]: 1932657111780796664ull -> 343766021385529931ull [H-THIN] kStopLimit_rows[13]: 5608877402063171472ull -> 14896797560397420035ull [H-THIN] kStopLimit_rows[14]: 4816751693073396948ull -> 17434159485109243471ull [H-THIN] kStopLimit_rows[15]: 406036428804793947ull -> 6133926189978579814ull [H-THIN] kStopLimit_rows[16]: 8411172447822998997ull -> 13535889090349139748ull [H-THIN] kStopLimit_rows[17]: 9566812430047964551ull -> 7154291922301271050ull [H-THIN] kStopLimit_rows[18]: 7047212701932505101ull -> 12262784895722576372ull [H-THIN] kStopLimit_rows[19]: 12944086715960255318ull -> 12762159079513717323ull [H-THIN] kStopLimit_rows[20]: 2932986234293574878ull -> 6211630995163031141ull [H-THIN] kStopLimit_rows[21]: 3851484164654454670ull -> 15698793793400314809ull [H-THIN] kStopLimit_rows[22]: 5793783382051423327ull -> 9224825516139358046ull [H-THIN] kStopLimit_rows[23]: 17080321546688673331ull -> 3897555601472887486ull [H-THIN] kStopLimit_rows[24]: 15357710425425554761ull -> 7742563012562507868ull [H-THIN] kStopLimit_rows[25]: 17224699499285056096ull -> 6030704080576753327ull [H-THIN] kStopLimit_rows[26]: 10136441707663244279ull -> 1172792760249453492ull [H-THIN] kStopLimit_rows[27]: 17829354381425508170ull -> 215530794059409427ull [H-THIN] kStopLimit_rows[28]: 12155373643870828842ull -> 1515537412314014355ull [H-THIN] kStopLimit_rows[29]: 10715131222481220760ull -> 8129234433613561553ull [H-THIN] kStopLimit_rows[30]: 4867968313932752304ull -> 7238089528421731329ull [H-THIN] kStopLimit_rows[31]: 4064453165671217168ull -> 9745351952936386365ull [H-THIN] kStopLimit_rows[32]: 1769313611189948830ull -> 14384312445426600067ull [H-THIN] kStopLimit_rows[33]: 4504345708664910252ull -> 6198847811909310381ull [H-THIN] kStopLimit_rows[34]: 782093603809331885ull -> 11872127846069280170ull [H-THIN] kStopLimit_rows[35]: 18267060294147680041ull -> 6744981219021824364ull [H-THIN] kStopLimit_rows[36]: 2944439646726608949ull -> 6655913456858325688ull [H-THIN] kStopLimit_rows[37]: 2477382922949126500ull -> 17554715975906318531ull [H-THIN] kStopLimit_rows[38]: 9214756763675162553ull -> 542461356767618432ull [H-THIN] kStopLimit_rows[39]: 6336494971372918338ull -> 13852676117274540777ull [H-THIN] kStopLimit_final[0]: 6336494971372918338ull -> 13852676117274540777ull [H-THIN] kStopLimitMag_rows[0]: 11157594904456853627ull -> 12049254652408164286ull [H-THIN] kStopLimitMag_rows[1]: 8401675850175966532ull -> 7170364698800146161ull [H-THIN] kStopLimitMag_rows[2]: 14768083129611579792ull -> 16063150345282688529ull [H-THIN] kStopLimitMag_rows[3]: 2813084369439848034ull -> 2375247605653847437ull [H-THIN] kStopLimitMag_rows[4]: 680081896452532403ull -> 5989703757491448050ull [H-THIN] kStopLimitMag_rows[5]: 3114044039354922134ull -> 7325059507104947563ull [H-THIN] kStopLimitMag_rows[6]: 18101581505709266307ull -> 4110843077158619028ull [H-THIN] kStopLimitMag_rows[7]: 3765585083570211695ull -> 8512043085829177902ull [H-THIN] kStopLimitMag_rows[8]: 5126585860831024376ull -> 11976516818017786813ull [H-THIN] kStopLimitMag_rows[9]: 15762785015022297388ull -> 8000889795845112059ull [H-THIN] kStopLimitMag_rows[10]: 14002286754420941147ull -> 3812031270016354768ull [H-THIN] kStopLimitMag_rows[11]: 9222864100890729823ull -> 13171068499659342452ull [H-THIN] kStopLimitMag_rows[12]: 6646548798123240355ull -> 2834570042813630280ull [H-THIN] kStopLimitMag_rows[13]: 6473759346034722294ull -> 612293012859052373ull [H-THIN] kStopLimitMag_rows[14]: 12710876809005991003ull -> 941008970127083544ull [H-THIN] kStopLimitMag_rows[15]: 3124661257347549740ull -> 12868627571096102339ull [H-THIN] kStopLimitMag_rows[16]: 2631460917473729378ull -> 13282802570212513971ull [H-THIN] kStopLimitMag_rows[17]: 6218399147747637008ull -> 3990088271426726433ull [H-THIN] kStopLimitMag_rows[18]: 7885746142563048312ull -> 15805224658207253271ull [H-THIN] kStopLimitMag_rows[19]: 7465774284363189296ull -> 4861907005886599783ull [H-THIN] kStopLimitMag_rows[20]: 15336532554253228688ull -> 5606595334476947895ull [H-THIN] kStopLimitMag_rows[21]: 3052942228447511665ull -> 16715759077213701098ull [H-THIN] kStopLimitMag_rows[22]: 11131484997342707171ull -> 17047719425359875908ull [H-THIN] kStopLimitMag_rows[23]: 12129492814419792298ull -> 8426786106325009307ull [H-THIN] kStopLimitMag_rows[24]: 16582911731330051275ull -> 15538761259524269350ull [H-THIN] kStopLimitMag_rows[25]: 14696325906442206119ull -> 14720303962652891230ull [H-THIN] kStopLimitMag_rows[26]: 13365743854012499388ull -> 6644795815053845919ull [H-THIN] kStopLimitMag_rows[27]: 271739893287090075ull -> 13567799877554087198ull [H-THIN] kStopLimitMag_rows[28]: 1688196853701772460ull -> 303066552358707847ull [H-THIN] kStopLimitMag_rows[29]: 1042572030152166013ull -> 1144887452290110514ull [H-THIN] kStopLimitMag_rows[30]: 18233864280626554198ull -> 17215798505737563325ull [H-THIN] kStopLimitMag_rows[31]: 14647413247529149725ull -> 13389121371815587728ull [H-THIN] kStopLimitMag_rows[32]: 4738394210991551364ull -> 10590670751038194751ull [H-THIN] kStopLimitMag_rows[33]: 10052202710057429660ull -> 16401520297421927605ull [H-THIN] kStopLimitMag_rows[34]: 3980637583370132169ull -> 1764458890420715860ull [H-THIN] kStopLimitMag_rows[35]: 11984610609265407890ull -> 15454338180826410585ull [H-THIN] kStopLimitMag_rows[36]: 17391162522930315245ull -> 10945801551956678372ull [H-THIN] kStopLimitMag_rows[37]: 17028258441606210396ull -> 13245700179032171137ull [H-THIN] kStopLimitMag_rows[38]: 8328930773731943340ull -> 14239530769682782007ull [H-THIN] kStopLimitMag_rows[39]: 9254963348917194152ull -> 4769056648897025171ull [H-THIN] kStopLimitMag_final[0]: 9254963348917194152ull -> 4769056648897025171ull [H-THIN] kQuiet_rows[0]: 428451890117060597ull -> 16343339797501156050ull [H-THIN] kQuiet_rows[1]: 1939867743749048694ull -> 16937855132455432219ull [H-THIN] kQuiet_rows[2]: 11195157439215676551ull -> 4413074683041463858ull [H-THIN] kQuiet_rows[3]: 13964997473137927097ull -> 1254109408918791568ull [H-THIN] kQuiet_rows[4]: 9212479816748968188ull -> 13031573772438562141ull [H-THIN] kQuiet_rows[5]: 16000164592560197492ull -> 9886236508543006595ull [H-THIN] kQuiet_rows[6]: 8130410293855426317ull -> 7133223670036907210ull [H-THIN] kQuiet_rows[7]: 10300840897873716753ull -> 3481249560441549862ull [H-THIN] kQuiet_rows[8]: 16568174616242417603ull -> 4184701415579817428ull [H-THIN] kQuiet_rows[9]: 17876633525817055002ull -> 17420350975284167145ull [H-THIN] kQuiet_rows[10]: 10659236710582653713ull -> 14713852734592639794ull [H-THIN] kQuiet_rows[11]: 17983194377250571525ull -> 17934243807563394006ull [H-THIN] kQuiet_rows[12]: 2047181318263126988ull -> 4115550806570448239ull [H-THIN] kQuiet_rows[13]: 14672633943864425963ull -> 14750302925653734844ull [H-THIN] kQuiet_rows[14]: 5161029633467225738ull -> 15882033274424875345ull [H-THIN] kQuiet_rows[15]: 7726211904455540872ull -> 10123935771407189591ull [H-THIN] kQuiet_final[0]: 7726211904455540872ull -> 10123935771407189591ull [H-THIN] kQuietMag_rows[0]: 18395077400277767983ull -> 2680312739779497464ull [H-THIN] kQuietMag_rows[1]: 8974918290184464318ull -> 14986120281306231523ull [H-THIN] kQuietMag_rows[2]: 9820360325097478805ull -> 9036345257640757520ull [H-THIN] kQuietMag_rows[3]: 9357979130704319683ull -> 2157695045143891892ull [H-THIN] kQuietMag_rows[4]: 1380600006529331319ull -> 12933046682291176380ull [H-THIN] kQuietMag_rows[5]: 3158417560156675530ull -> 1720445300800871165ull [H-THIN] kQuietMag_rows[6]: 3834533453170743615ull -> 13872138906269598328ull [H-THIN] kQuietMag_rows[7]: 4903111063815600551ull -> 7090820869966325000ull [H-THIN] kQuietMag_rows[8]: 8341995541182971309ull -> 8891708591767162058ull [H-THIN] kQuietMag_rows[9]: 1420418759607814080ull -> 16869781438282071171ull [H-THIN] kQuietMag_rows[10]: 6268521046740087103ull -> 16039329020103217780ull [H-THIN] kQuietMag_rows[11]: 8200284679939302671ull -> 646593827978091276ull [H-THIN] kQuietMag_rows[12]: 4063870167578037858ull -> 33504042054535113ull [H-THIN] kQuietMag_rows[13]: 14848461357115496341ull -> 18256975930975412058ull [H-THIN] kQuietMag_rows[14]: 8190184809671648256ull -> 12491220415833066003ull [H-THIN] kQuietMag_rows[15]: 15117606282418226478ull -> 6982614520627168209ull [H-THIN] kQuietMag_final[0]: 15117606282418226478ull -> 6982614520627168209ull [H-THIN] test_adapter_lookup_index_witness: 948 lines kFlip_rows[0]: 15092016227729062170ull -> 2713710203384152283ull [H-THIN] kFlip_rows[1]: 4449511944144175244ull -> 6223096899160842027ull [H-THIN] kFlip_rows[2]: 14173574221892226517ull -> 8359657276913190150ull [H-THIN] kFlip_rows[3]: 11141868197266883358ull -> 13333415662077313847ull [H-THIN] kFlip_rows[4]: 5770620033420772121ull -> 2339692489009935305ull [H-THIN] kFlip_rows[5]: 3157789451207414333ull -> 13881977959297861087ull [H-THIN] kFlip_rows[6]: 15624036521156382865ull -> 6037739369311942191ull [H-THIN] kFlip_rows[7]: 17987828433123328363ull -> 13360499968018975827ull [H-THIN] kFlip_rows[8]: 3782132892539745074ull -> 3716214627590530178ull [H-THIN] kFlip_rows[9]: 14406538727996760883ull -> 10595879371001202161ull [H-THIN] kFlip_rows[10]: 14845895464439448971ull -> 17310026037100513916ull [H-THIN] kFlip_rows[11]: 14487807311329646997ull -> 12667453818951594687ull [H-THIN] kFlip_rows[12]: 14748183486665371140ull -> 2079604483202772421ull [H-THIN] kFlip_rows[13]: 12768807557967508254ull -> 3113949815737359904ull [H-THIN] kFlip_rows[14]: 9713166925369100573ull -> 3920341595463994402ull [H-THIN] kFlip_rows[15]: 16480235611441211183ull -> 9084177151567397255ull [H-THIN] kFlip_rows[16]: 6732375251738161555ull -> 18359863285932392610ull [H-THIN] kFlip_rows[17]: 9492220356384667994ull -> 17884599320337535118ull [H-THIN] kFlip_rows[18]: 7836886659987736698ull -> 17357906624164980943ull [H-THIN] kFlip_rows[19]: 11041759722035017537ull -> 7307330041717258533ull [H-THIN] kFlip_rows[20]: 1923076241747348592ull -> 17225382223780507170ull [H-THIN] kFlip_rows[21]: 16491371029791645211ull -> 10189175013051411098ull [H-THIN] kFlip_rows[22]: 1800320518007831877ull -> 12861484300776397436ull [H-THIN] kFlip_rows[23]: 571647502003500868ull -> 18158906195875357228ull [H-THIN] kFlip_rows[24]: 2739724503975357253ull -> 668263264598955855ull [H-THIN] kFlip_rows[25]: 9819507431665488276ull -> 12373521030877882901ull [H-THIN] kFlip_rows[26]: 9945110324091627954ull -> 13472266762540203511ull [H-THIN] kFlip_rows[27]: 11949472718108945819ull -> 1520792362654463139ull [H-THIN] kFlip_rows[28]: 9762923116876284513ull -> 8057949129317313833ull [H-THIN] kFlip_rows[29]: 18027851627748731010ull -> 7504246145768874050ull [H-THIN] kFlip_rows[30]: 1366892124494590874ull -> 14499427008266620636ull [H-THIN] kFlip_rows[31]: 14363491720205864892ull -> 12041531785093130716ull [H-THIN] kFlip_rows[32]: 5814793330491118429ull -> 1801834117749396818ull [H-THIN] kFlip_rows[33]: 14054560180567661527ull -> 17865123298655850846ull [H-THIN] kFlip_rows[34]: 7046697962876325595ull -> 1083628892442129306ull [H-THIN] kFlip_rows[35]: 13964478754584930616ull -> 8913360708937979364ull [H-THIN] kFlip_rows[36]: 10828030611661121392ull -> 3270649769713143173ull [H-THIN] kFlip_rows[37]: 16763687418918419771ull -> 2058499959797060144ull [H-THIN] kFlip_rows[38]: 14089257237702634390ull -> 3618662235985025981ull [H-THIN] kFlip_rows[39]: 4126250144527807077ull -> 10418131399291610085ull [H-THIN] kFlip_rows[40]: 18195401520799288599ull -> 5976888785228459955ull [H-THIN] kFlip_rows[41]: 1257461765259766246ull -> 5642573737457759491ull [H-THIN] kFlip_rows[42]: 859001078874563294ull -> 8755896700597066515ull [H-THIN] kFlip_rows[43]: 4425771211202385044ull -> 9228163713756312671ull [H-THIN] kFlip_rows[44]: 11354019857073859374ull -> 16057503475246941040ull [H-THIN] kFlip_rows[45]: 14877394547945152326ull -> 9166486181684479621ull [H-THIN] kFlip_rows[46]: 6393077053177388522ull -> 12196317256432235408ull [H-THIN] kFlip_rows[47]: 7888781154589438239ull -> 8248464056444717792ull [H-THIN] kFlip_rows[48]: 6757741011218066608ull -> 15765659994617767915ull [H-THIN] kFlip_rows[49]: 6658586324753656657ull -> 6741853983312669853ull [H-THIN] kFlip_rows[50]: 4423113805183200923ull -> 10896023542341293900ull [H-THIN] kFlip_rows[51]: 16281985077084422619ull -> 13744950457412991552ull [H-THIN] kFlip_rows[52]: 1045183813508792423ull -> 2675255887306542834ull [H-THIN] kFlip_rows[53]: 9313942597962496343ull -> 5645825323621631733ull [H-THIN] kFlip_rows[54]: 12979192255068283250ull -> 3920811839771533221ull [H-THIN] kFlip_rows[55]: 4141974475510294052ull -> 7755538476974379703ull [H-THIN] kFlip_rows[56]: 7255579447356474699ull -> 13132499480713676622ull [H-THIN] kFlip_rows[57]: 11500737887308332433ull -> 5764644161685668523ull [H-THIN] kFlip_rows[58]: 11396364648984166081ull -> 15355218105878313483ull [H-THIN] kFlip_rows[59]: 17999943307750433945ull -> 5253835154794121854ull [H-THIN] kFlip_rows[60]: 1207486754606428343ull -> 16356433441734928501ull [H-THIN] kFlip_rows[61]: 13698332948978760389ull -> 9284859115909895525ull [H-THIN] kFlip_rows[62]: 11589549934838942681ull -> 10869385438384466871ull [H-THIN] kFlip_rows[63]: 9964006416615122225ull -> 2981556363023708914ull [H-THIN] kFlip_rows[64]: 16845154769434317988ull -> 18170305916189617187ull [H-THIN] kFlip_rows[65]: 7334258627544164572ull -> 4703464358355960681ull [H-THIN] kFlip_rows[66]: 4908786545179256930ull -> 580923567071968203ull [H-THIN] kFlip_rows[67]: 3299406129086178611ull -> 13542563271263616250ull [H-THIN] kFlip_rows[68]: 5312427811860831678ull -> 1241688105587480446ull [H-THIN] kFlip_rows[69]: 11839292439534512009ull -> 18195118200181923889ull [H-THIN] kFlip_rows[70]: 10875737639895929077ull -> 18177172509460860789ull [H-THIN] kFlip_rows[71]: 11779092014895227520ull -> 15332939137911226852ull [H-THIN] kFlip_rows[72]: 4793765047901110384ull -> 12867779665010719103ull [H-THIN] kFlip_rows[73]: 1012289705290011692ull -> 6631355797698381202ull [H-THIN] kFlip_rows[74]: 1667267280248270342ull -> 6751318601181665851ull [H-THIN] kFlip_rows[75]: 8455992061434657085ull -> 15964807147277222246ull [H-THIN] kFlip_rows[76]: 15957521919212433920ull -> 410299832240558536ull [H-THIN] kFlip_rows[77]: 10969413931083416374ull -> 4534261760619620647ull [H-THIN] kFlip_rows[78]: 7170250270476017946ull -> 12405100549638781593ull [H-THIN] kFlip_rows[79]: 15006273935975173912ull -> 1594024600982885699ull [H-THIN] kFlip_rows[80]: 11184968259335349501ull -> 5379847191493293482ull [H-THIN] kFlip_rows[81]: 6276009473983766703ull -> 16387988109738828311ull [H-THIN] kFlip_rows[82]: 1757274001997493069ull -> 8185426235741655660ull [H-THIN] kFlip_rows[83]: 7772193038207188421ull -> 3218474949250087435ull [H-THIN] kFlip_rows[84]: 15686643752958926138ull -> 15335138187047848164ull [H-THIN] kFlip_rows[85]: 17800105557473441299ull -> 15994853274085433009ull [H-THIN] kFlip_rows[86]: 5863137483546792400ull -> 15487434336049414427ull [H-THIN] kFlip_rows[87]: 7816936454583767258ull -> 1769663901820159206ull [H-THIN] kFlip_rows[88]: 6161705640611834793ull -> 15148139558826312238ull [H-THIN] kFlip_rows[89]: 7780124240324826458ull -> 8665254425768362077ull [H-THIN] kFlip_rows[90]: 13353384012423321986ull -> 10696940446584417573ull [H-THIN] kFlip_rows[91]: 9198310857862468197ull -> 6408081173200491198ull [H-THIN] kFlip_rows[92]: 388071097687117133ull -> 325415082935288696ull [H-THIN] kFlip_rows[93]: 17288887437003769330ull -> 16992578023938135234ull [H-THIN] kFlip_rows[94]: 3196584109952517226ull -> 1363812141911607255ull [H-THIN] kFlip_rows[95]: 9440871734801450516ull -> 7381600230201732590ull [H-THIN] kFlip_reads[0]: 10304825573213448968ull -> 3114684496819061489ull [H-THIN] kFlip_reads[1]: 14774003860189224946ull -> 6956326645770273433ull [H-THIN] kFlip_reads[2]: 11801395281262233716ull -> 6687820258270235301ull [H-THIN] kFlip_reads[3]: 8061934143473682151ull -> 12696019809107805010ull [H-THIN] kFlip_reads[4]: 15983140295339383862ull -> 6271839642900128638ull [H-THIN] kFlip_reads[5]: 5166653331022736661ull -> 17758335370804468308ull [H-THIN] kFlip_reads[6]: 5250563911700661104ull -> 6246269702984932325ull [H-THIN] kFlip_reads[7]: 7793104124436293557ull -> 756407504012262357ull [H-THIN] kFlip_reads[8]: 1336784375798636377ull -> 10752289111609390933ull [H-THIN] kFlip_reads[9]: 6339830337021579012ull -> 10702669286541029164ull [H-THIN] kFlip_reads[10]: 16252798169114118083ull -> 3961413467939854492ull [H-THIN] kFlip_reads[11]: 10937184901662388644ull -> 13925604324515645251ull [H-THIN] kFlip_reads[12]: 18064999594731702480ull -> 11175435615622050442ull [H-THIN] kFlip_reads[13]: 3546952347196282172ull -> 6776561790919030493ull [H-THIN] kFlip_reads[14]: 13395340388338915323ull -> 577659037159878982ull [H-THIN] kFlip_reads[15]: 8229077577636098658ull -> 10163490242105190374ull [H-THIN] kFlip_reads[16]: 6541813106524409698ull -> 11276159262602451941ull [H-THIN] kFlip_reads[17]: 25242829173903041ull -> 3717944350664878703ull [H-THIN] kFlip_reads[18]: 8874377228534856759ull -> 17786578140867347109ull [H-THIN] kFlip_reads[19]: 13648938798136741164ull -> 253867406630618996ull [H-THIN] kFlip_reads[20]: 11200265295650397519ull -> 3554686122410655613ull [H-THIN] kFlip_reads[21]: 11420272068366920678ull -> 17187792775761839863ull [H-THIN] kFlip_reads[22]: 3753005678972800967ull -> 15223748191501073730ull [H-THIN] kFlip_reads[23]: 4896870620618706776ull -> 4622823325308959265ull [H-THIN] kFlip_reads[24]: 6895518920767105217ull -> 3947596021646594531ull [H-THIN] kFlip_reads[25]: 7636324161499144341ull -> 18103100887606856772ull [H-THIN] kFlip_reads[26]: 8785945007507579830ull -> 3330742851831023775ull [H-THIN] kFlip_reads[27]: 5297575674933281534ull -> 12522884863273911920ull [H-THIN] kFlip_reads[28]: 13537048838690550187ull -> 11078204221518180355ull [H-THIN] kFlip_reads[29]: 16051941383930194236ull -> 7792565789695194874ull [H-THIN] kFlip_reads[30]: 10552551845124524652ull -> 5837249087395420066ull [H-THIN] kFlip_reads[31]: 15672638477538649241ull -> 15061096079220184405ull [H-THIN] kFlip_reads[32]: 14086945637463962641ull -> 10949413619028940858ull [H-THIN] kFlip_reads[33]: 10134206932178793443ull -> 3927414729140235852ull [H-THIN] kFlip_reads[34]: 11512602377523606119ull -> 10786374148287235402ull [H-THIN] kFlip_reads[35]: 2958147687598254910ull -> 13739564127065017562ull [H-THIN] kFlip_reads[36]: 6458026266382303356ull -> 6792673025550256385ull [H-THIN] kFlip_reads[37]: 1094964303920914155ull -> 9120117456385770000ull [H-THIN] kFlip_reads[38]: 6484787112426264738ull -> 2702365990173828030ull [H-THIN] kFlip_reads[39]: 12388725274527404683ull -> 14552218038038255731ull [H-THIN] kFlip_reads[40]: 17043242768143307178ull -> 15974235985971979302ull [H-THIN] kFlip_reads[41]: 5476835457134592009ull -> 5859075624711984363ull [H-THIN] kFlip_reads[42]: 2025850526353442102ull -> 6107800350680289807ull [H-THIN] kFlip_reads[43]: 1451154197906290714ull -> 6809837422484044317ull [H-THIN] kFlip_reads[44]: 6635868355983485389ull -> 4235871646442648099ull [H-THIN] kFlip_reads[45]: 7018381114706383243ull -> 2112314171124100852ull [H-THIN] kFlip_reads[46]: 13248420308395735018ull -> 16908957968796171152ull [H-THIN] kFlip_reads[47]: 774767130641043275ull -> 16595556766713987481ull [H-THIN] kFlip_reads[48]: 13252007359081704531ull -> 17393639219555656528ull [H-THIN] kFlip_reads[49]: 6750492291307654391ull -> 258880825276626691ull [H-THIN] kFlip_reads[50]: 2031163009273496765ull -> 6502647070460437768ull [H-THIN] kFlip_reads[51]: 9769809183116681670ull -> 9521652940428922473ull [H-THIN] kFlip_reads[52]: 14493685479901302050ull -> 12532170261876847654ull [H-THIN] kFlip_reads[53]: 4876420665985364975ull -> 7965261589232878523ull [H-THIN] kFlip_reads[54]: 12501338337948389822ull -> 14891396965396161229ull [H-THIN] kFlip_reads[55]: 14207736975056792147ull -> 8954363812786394023ull [H-THIN] kFlip_reads[56]: 10604319388773724637ull -> 16427886737643839568ull [H-THIN] kFlip_reads[57]: 1181294512016461575ull -> 14570810525240019901ull [H-THIN] kFlip_reads[58]: 5062201717018553710ull -> 18385114195862288592ull [H-THIN] kFlip_reads[59]: 5819082412660383228ull -> 356400710711660674ull [H-THIN] kFlip_reads[60]: 4925793200130354182ull -> 6131866860079521894ull [H-THIN] kFlip_reads[61]: 14181407321277195773ull -> 5473687205525959309ull [H-THIN] kFlip_reads[62]: 18275617508396411315ull -> 1368982430503028604ull [H-THIN] kFlip_reads[63]: 4666214838307866879ull -> 12966841479814647796ull [H-THIN] kFlip_reads[64]: 13997906523391047231ull -> 11709399142050353312ull [H-THIN] kFlip_reads[65]: 11459677085401802248ull -> 5078259748833979006ull [H-THIN] kFlip_reads[66]: 5810149534385663204ull -> 12253705469518588621ull [H-THIN] kFlip_reads[67]: 8487172727633256810ull -> 14449800309393374267ull [H-THIN] kFlip_reads[68]: 7417382166705753312ull -> 11263108227153639840ull [H-THIN] kFlip_reads[69]: 9462378120791609816ull -> 4291291545838288200ull [H-THIN] kFlip_reads[70]: 12680325011617368327ull -> 10391124708572243222ull [H-THIN] kFlip_reads[71]: 7441435328919647574ull -> 11045512779463427838ull [H-THIN] kFlip_reads[72]: 13407951422463106989ull -> 14252271658091688198ull [H-THIN] kFlip_reads[73]: 9986964831043767237ull -> 5864385547990959743ull [H-THIN] kFlip_reads[74]: 14464180164790259585ull -> 10060761729093637760ull [H-THIN] kFlip_reads[75]: 5220370176240914239ull -> 6012435413813366588ull [H-THIN] kFlip_reads[76]: 3518323198598619449ull -> 4863606450928885329ull [H-THIN] kFlip_reads[77]: 2133255899590215934ull -> 13441348122273594418ull [H-THIN] kFlip_reads[78]: 3011009182271535838ull -> 2704170472265346213ull [H-THIN] kFlip_reads[79]: 14836384915404578807ull -> 12941384362597894872ull [H-THIN] kFlip_reads[80]: 9555956714003340276ull -> 10030988591835912271ull [H-THIN] kFlip_reads[81]: 11964636267214528571ull -> 7895088774892271145ull [H-THIN] kFlip_reads[82]: 16921397335982440924ull -> 8891008075855122089ull [H-THIN] kFlip_reads[83]: 10586397832022517650ull -> 9575328812735763041ull [H-THIN] kFlip_reads[84]: 18096821421422867234ull -> 15722680013241790924ull [H-THIN] kFlip_reads[85]: 14108486707933080521ull -> 14330185982019726263ull [H-THIN] kFlip_reads[86]: 2822797701958308088ull -> 3361192221763941904ull [H-THIN] kFlip_reads[87]: 8446901108100253910ull -> 12098417980386469814ull [H-THIN] kFlip_reads[88]: 9675426367378755701ull -> 5014147383869998166ull [H-THIN] kFlip_reads[89]: 4615585398233598887ull -> 4177883635838955668ull [H-THIN] kFlip_reads[90]: 4951618999197916175ull -> 7150992680430954116ull [H-THIN] kFlip_reads[91]: 13485186398181682580ull -> 18395049852860007119ull [H-THIN] kFlip_reads[92]: 2264266327693067886ull -> 17907122275511118531ull [H-THIN] kFlip_reads[93]: 7513550480889044889ull -> 10212301139308990009ull [H-THIN] kFlip_reads[94]: 14297335183847666490ull -> 17920870463788384967ull [H-THIN] kFlip_reads[95]: 3496786386085197052ull -> 2787743584436740230ull [H-THIN] kFlip_final[0]: 9440871734801450516ull -> 7381600230201732590ull [H-THIN] kBrackets_rows[0]: 910134550716034036ull -> 7661078636126692219ull [H-THIN] kBrackets_rows[1]: 10119054608448654679ull -> 9096500876121352440ull [H-THIN] kBrackets_rows[2]: 5666477157831623024ull -> 14112738771422015627ull [H-THIN] kBrackets_rows[3]: 202804637143337815ull -> 3424342483834498990ull [H-THIN] kBrackets_rows[4]: 4829290339771875162ull -> 4773449085765542223ull [H-THIN] kBrackets_rows[5]: 10813129365163527157ull -> 11446110045063788208ull [H-THIN] kBrackets_rows[6]: 2873082008237019980ull -> 10433831526790963875ull [H-THIN] kBrackets_rows[7]: 17093419766211306788ull -> 13032183593567110055ull [H-THIN] kBrackets_rows[8]: 17114718258109977185ull -> 6742781461778894496ull [H-THIN] kBrackets_rows[9]: 13157751279945694777ull -> 15715452229856127120ull [H-THIN] kBrackets_rows[10]: 218728992015652860ull -> 8358119382546858485ull [H-THIN] kBrackets_rows[11]: 14366067783818258415ull -> 2290319528819275924ull [H-THIN] kBrackets_rows[12]: 2053665803239548226ull -> 6782857020890029813ull [H-THIN] kBrackets_rows[13]: 16605355454280111952ull -> 17953535504978118895ull [H-THIN] kBrackets_rows[14]: 12012249101044242847ull -> 4063231953652586830ull [H-THIN] kBrackets_rows[15]: 6571732001567235539ull -> 1777830241952955486ull [H-THIN] kBrackets_rows[16]: 1030372738842403124ull -> 9409772117090094177ull [H-THIN] kBrackets_rows[17]: 5009615712759518098ull -> 18086159773284292787ull [H-THIN] kBrackets_rows[18]: 11188453092947462355ull -> 8322892009219716946ull [H-THIN] kBrackets_rows[19]: 7315481592762531637ull -> 5534653117672349686ull [H-THIN] kBrackets_rows[20]: 15172698683941778544ull -> 15974950853372725031ull [H-THIN] kBrackets_rows[21]: 13076695872824005863ull -> 17858881224485069236ull [H-THIN] kBrackets_rows[22]: 8630952403774920278ull -> 14177855542759430887ull [H-THIN] kBrackets_rows[23]: 5446218045192577731ull -> 11337212344183599690ull [H-THIN] kBrackets_rows[24]: 8868444455935332378ull -> 2175468754819186737ull [H-THIN] kBrackets_rows[25]: 11935211056196573296ull -> 4612364077073358813ull [H-THIN] kBrackets_rows[26]: 10851985789766247663ull -> 12868865454125389666ull [H-THIN] kBrackets_rows[27]: 13162717003573213214ull -> 13883346286574059481ull [H-THIN] kBrackets_rows[28]: 3878781687326319893ull -> 10942687616239908444ull [H-THIN] kBrackets_rows[29]: 17299684057647085908ull -> 16439500824616424713ull [H-THIN] kBrackets_rows[30]: 3804735542114435005ull -> 13534132873070141390ull [H-THIN] kBrackets_rows[31]: 10757881489481288273ull -> 1657001366552425314ull [H-THIN] kBrackets_rows[32]: 7304910952741449329ull -> 2790121310768896680ull [H-THIN] kBrackets_rows[33]: 6446837817330552219ull -> 493357323053936518ull [H-THIN] kBrackets_rows[34]: 517463869567802231ull -> 1703129217351950834ull [H-THIN] kBrackets_rows[35]: 16486081082077568694ull -> 820240013594959273ull [H-THIN] kBrackets_rows[36]: 8342654310771400857ull -> 12701410792546982706ull [H-THIN] kBrackets_rows[37]: 18315162927804124892ull -> 12841347796780112451ull [H-THIN] kBrackets_rows[38]: 11131921289741383191ull -> 6762571077584465114ull [H-THIN] kBrackets_rows[39]: 4272804311264028152ull -> 9839274063428681485ull [H-THIN] kBrackets_rows[40]: 17329153790653310123ull -> 1142671265814790566ull [H-THIN] kBrackets_rows[41]: 2805380709705245281ull -> 7977023869714740224ull [H-THIN] kBrackets_rows[42]: 11779111360073316823ull -> 12196334454371478488ull [H-THIN] kBrackets_rows[43]: 169194399111162280ull -> 10719326225194309969ull [H-THIN] kBrackets_rows[44]: 11895438524630435024ull -> 2933196093229072601ull [H-THIN] kBrackets_rows[45]: 11609073112680438102ull -> 11873952510683230649ull [H-THIN] kBrackets_rows[46]: 13966776254915757287ull -> 11807266724158176848ull [H-THIN] kBrackets_rows[47]: 429534206234234885ull -> 4367492907737103676ull [H-THIN] kBrackets_rows[48]: 15196669054502239260ull -> 8270195260561269801ull [H-THIN] kBrackets_rows[49]: 11931512644904705052ull -> 8207582755124985801ull [H-THIN] kBrackets_rows[50]: 11456573762602716761ull -> 11179776345437501518ull [H-THIN] kBrackets_rows[51]: 15718773719268670624ull -> 15415488505702541339ull [H-THIN] kBrackets_rows[52]: 10989825173285828350ull -> 11371155962071199053ull [H-THIN] kBrackets_rows[53]: 11346633789448934891ull -> 514527900501657286ull [H-THIN] kBrackets_rows[54]: 1699367467603584576ull -> 16506041037325274955ull [H-THIN] kBrackets_rows[55]: 9509380442542362055ull -> 13369657622015869344ull [H-THIN] kBrackets_rows[56]: 5849765746586438263ull -> 9255829141470398292ull [H-THIN] kBrackets_rows[57]: 15258883296401804974ull -> 15142394148477901141ull [H-THIN] kBrackets_rows[58]: 12668399090838414326ull -> 5111689152684702997ull [H-THIN] kBrackets_rows[59]: 17428702340345096816ull -> 11236186855000699047ull [H-THIN] kBrackets_rows[60]: 16044980621491544275ull -> 3566968125769434750ull [H-THIN] kBrackets_rows[61]: 7858613838309440451ull -> 10566985719991230638ull [H-THIN] kBrackets_rows[62]: 4973377506472252675ull -> 8566448383289752548ull [H-THIN] kBrackets_rows[63]: 15666991206354451962ull -> 8352146082968021413ull [H-THIN] kBrackets_rows[64]: 427471937805578063ull -> 8514065980832786824ull [H-THIN] kBrackets_rows[65]: 13475650653372852055ull -> 4245601606500227800ull [H-THIN] kBrackets_rows[66]: 8639057947851950159ull -> 8540602924026434500ull [H-THIN] kBrackets_rows[67]: 5938463183864513700ull -> 11642724078551580301ull [H-THIN] kBrackets_rows[68]: 5648745424618098437ull -> 4082406208929632142ull [H-THIN] kBrackets_rows[69]: 447259644375350934ull -> 16999316433304684901ull [H-THIN] kBrackets_rows[70]: 16648371541827537609ull -> 12630189843512310176ull [H-THIN] kBrackets_rows[71]: 16429366851237249424ull -> 13269378032529487365ull [H-THIN] kBrackets_rows[72]: 279187655991346264ull -> 4089528504669692435ull [H-THIN] kBrackets_rows[73]: 15953137644069010440ull -> 13468147224454604453ull [H-THIN] kBrackets_rows[74]: 7431687286361549413ull -> 2050641591772190580ull [H-THIN] kBrackets_rows[75]: 5023789027380833817ull -> 13884969392006403536ull [H-THIN] kBrackets_rows[76]: 10248295502726720638ull -> 18342784319159857231ull [H-THIN] kBrackets_rows[77]: 640594980859648944ull -> 6373895992970415839ull [H-THIN] kBrackets_rows[78]: 448592334131119277ull -> 6052197144968057496ull [H-THIN] kBrackets_rows[79]: 16276383010288949834ull -> 4516024341018020553ull [H-THIN] kBrackets_rows[80]: 442889274262864391ull -> 16151109736050180518ull [H-THIN] kBrackets_rows[81]: 4333712493194412434ull -> 3962683434800121797ull [H-THIN] kBrackets_rows[82]: 14568797647822043048ull -> 10101286877014092167ull [H-THIN] kBrackets_rows[83]: 2692075964144184930ull -> 3322622454096003149ull [H-THIN] kBrackets_rows[84]: 13961322833977115428ull -> 12861272284809283343ull [H-THIN] kBrackets_rows[85]: 15127421684109569508ull -> 4487614500502847957ull [H-THIN] kBrackets_rows[86]: 14509829626384044961ull -> 11763183347924504138ull [H-THIN] kBrackets_rows[87]: 5027884074442838157ull -> 4022970112297852778ull [H-THIN] kBrackets_rows[88]: 9918179728360057649ull -> 4113353824470960398ull [H-THIN] kBrackets_rows[89]: 3559274869490871147ull -> 4712660953944041544ull [H-THIN] kBrackets_rows[90]: 2619065441802592089ull -> 620536408398626286ull [H-THIN] kBrackets_rows[91]: 9017534287281633674ull -> 13279868472225083865ull [H-THIN] kBrackets_rows[92]: 10142753397763051047ull -> 15776436722626106676ull [H-THIN] kBrackets_rows[93]: 1825167456880839822ull -> 2688432314125484999ull [H-THIN] kBrackets_rows[94]: 12808870162637299498ull -> 5425066327213429727ull [H-THIN] kBrackets_rows[95]: 16833369335393909673ull -> 17655979472884703908ull [H-THIN] kBrackets_rows[96]: 18259761263316881488ull -> 7599015585915916887ull [H-THIN] kBrackets_rows[97]: 2751903624558806694ull -> 14084462871382252093ull [H-THIN] kBrackets_rows[98]: 12840688134865061533ull -> 5327406007590004902ull [H-THIN] kBrackets_rows[99]: 17213452481114196577ull -> 1327260407587226834ull [H-THIN] kBrackets_rows[100]: 110464992498916575ull -> 9419238100865204704ull [H-THIN] kBrackets_rows[101]: 2694244057601629716ull -> 7851171160816794259ull [H-THIN] kBrackets_rows[102]: 4590220084927802863ull -> 14694071302479188878ull [H-THIN] kBrackets_rows[103]: 2437199406152424049ull -> 9265024402745593748ull [H-THIN] kBrackets_rows[104]: 8159801410563329851ull -> 16604561282375327072ull [H-THIN] kBrackets_rows[105]: 14735699852171413911ull -> 14329385415067150240ull [H-THIN] kBrackets_rows[106]: 9157222081581125695ull -> 6772020752524046436ull [H-THIN] kBrackets_rows[107]: 1085291491204167422ull -> 4903877568298110599ull [H-THIN] kBrackets_rows[108]: 6346057658078798747ull -> 125984323879217730ull [H-THIN] kBrackets_rows[109]: 9775349377282567210ull -> 13515966623569247839ull [H-THIN] kBrackets_rows[110]: 18290376404771660045ull -> 14623667090670085180ull [H-THIN] kBrackets_rows[111]: 14109635784428120043ull -> 8396494711544938138ull [H-THIN] kBrackets_rows[112]: 6793532772339349305ull -> 14666598984172760326ull [H-THIN] kBrackets_rows[113]: 12315454366145997871ull -> 13546459695976942876ull [H-THIN] kBrackets_rows[114]: 3340912588114346141ull -> 17394619144074825006ull [H-THIN] kBrackets_rows[115]: 10626957428750553111ull -> 4698093564093878598ull [H-THIN] kBrackets_rows[116]: 16133891667902416811ull -> 18182954524309548546ull [H-THIN] kBrackets_rows[117]: 18129147092791966950ull -> 2493468370943321295ull [H-THIN] kBrackets_rows[118]: 12096596197056575067ull -> 746152301302218096ull [H-THIN] kBrackets_rows[119]: 12419458164604031287ull -> 6645823410963443848ull [H-THIN] kBrackets_reads[0]: 174544879155911710ull -> 6447178462726161489ull [H-THIN] kBrackets_reads[1]: 6847890701157152189ull -> 7282051271575243766ull [H-THIN] kBrackets_reads[2]: 14236720572403425120ull -> 3204765037450491575ull [H-THIN] kBrackets_reads[3]: 9323947307018159523ull -> 18267580849453675956ull [H-THIN] kBrackets_reads[4]: 6706848899178691109ull -> 12480191273474156080ull [H-THIN] kBrackets_reads[5]: 6612524546470024288ull -> 13176315927737097143ull [H-THIN] kBrackets_reads[6]: 10668883385252112854ull -> 2613978327321436125ull [H-THIN] kBrackets_reads[7]: 231581670271637154ull -> 6672818892360288129ull [H-THIN] kBrackets_reads[8]: 13242222575440997023ull -> 11742746030453123914ull [H-THIN] kBrackets_reads[9]: 17290172295419987875ull -> 10755408511157905806ull [H-THIN] kBrackets_reads[10]: 3265116288063283204ull -> 5043690746894668881ull [H-THIN] kBrackets_reads[11]: 8205670759236763993ull -> 9940769973078973490ull [H-THIN] kBrackets_reads[12]: 112561924048020734ull -> 13766311310658315637ull [H-THIN] kBrackets_reads[13]: 15428098460808809263ull -> 17445549063740998476ull [H-THIN] kBrackets_reads[14]: 17567572508977123509ull -> 11888716919086895768ull [H-THIN] kBrackets_reads[15]: 11229049955366294166ull -> 2021557805667300987ull [H-THIN] kBrackets_reads[16]: 18321533072536116520ull -> 17925926338552567217ull [H-THIN] kBrackets_reads[17]: 16105561550759864220ull -> 477557840133476497ull [H-THIN] kBrackets_reads[18]: 483522463541730617ull -> 5775694257083602140ull [H-THIN] kBrackets_reads[19]: 9570911977664884728ull -> 16386436657763025955ull [H-THIN] kBrackets_reads[20]: 7580047313596786995ull -> 3123738760236770072ull [H-THIN] kBrackets_reads[21]: 4665256402668508715ull -> 14223494751084252108ull [H-THIN] kBrackets_reads[22]: 421497521705913896ull -> 18354245747609319561ull [H-THIN] kBrackets_reads[23]: 17087607459663445442ull -> 8349411476214893155ull [H-THIN] kBrackets_reads[24]: 12527499930243019525ull -> 3923626943346498186ull [H-THIN] kBrackets_reads[25]: 2971437309355039777ull -> 6383384587798256840ull [H-THIN] kBrackets_reads[26]: 9863935661287706359ull -> 12975754508323072986ull [H-THIN] kBrackets_reads[27]: 6851589387112729438ull -> 1647783811313703689ull [H-THIN] kBrackets_reads[28]: 10761885404547615008ull -> 4455718202356565173ull [H-THIN] kBrackets_reads[29]: 5179210173701881257ull -> 8665836061694240420ull [H-THIN] kBrackets_reads[30]: 12700378635574144447ull -> 11408952070111923920ull [H-THIN] kBrackets_reads[31]: 12732163138534186016ull -> 7614293002298548875ull [H-THIN] kBrackets_reads[32]: 14016674439498449031ull -> 5629517015204608062ull [H-THIN] kBrackets_reads[33]: 11327403139305040686ull -> 4158951807679315351ull [H-THIN] kBrackets_reads[34]: 5747816375553482093ull -> 11024401079205504452ull [H-THIN] kBrackets_reads[35]: 12386497247962690388ull -> 2438635214836639347ull [H-THIN] kBrackets_reads[36]: 16040095552360636577ull -> 3497662992807996570ull [H-THIN] kBrackets_reads[37]: 17340479382499593300ull -> 16474642763659850795ull [H-THIN] kBrackets_reads[38]: 12046954179175030003ull -> 16735708978364955206ull [H-THIN] kBrackets_reads[39]: 3330771664215515798ull -> 3252272045281708895ull [H-THIN] kBrackets_reads[40]: 2985817778317811366ull -> 15474076080136309379ull [H-THIN] kBrackets_reads[41]: 3949828394086364361ull -> 13848019505242045158ull [H-THIN] kBrackets_reads[42]: 16198687903297035507ull -> 3476711314567542992ull [H-THIN] kBrackets_reads[43]: 11881556093292430173ull -> 4854017566382016708ull [H-THIN] kBrackets_reads[44]: 10043888482375429571ull -> 14076054934784788294ull [H-THIN] kBrackets_reads[45]: 7435285615030433029ull -> 17570234807141684014ull [H-THIN] kBrackets_reads[46]: 9041137245577600492ull -> 12112735759254556435ull [H-THIN] kBrackets_reads[47]: 993655137088931422ull -> 14613241064154119463ull [H-THIN] kBrackets_reads[48]: 16182190595469348839ull -> 18349864350883260054ull [H-THIN] kBrackets_reads[49]: 5762190984609537746ull -> 15417110870088006819ull [H-THIN] kBrackets_reads[50]: 87663964338170542ull -> 17128247651204467173ull [H-THIN] kBrackets_reads[51]: 15322366287499145950ull -> 10807796678683691685ull [H-THIN] kBrackets_reads[52]: 13403031039768262916ull -> 6278785944056007979ull [H-THIN] kBrackets_reads[53]: 13795061532521471462ull -> 8049557354080931463ull [H-THIN] kBrackets_reads[54]: 6407369734919200583ull -> 17791516491718511800ull [H-THIN] kBrackets_reads[55]: 8575404273882652022ull -> 11305578665809417121ull [H-THIN] kBrackets_reads[56]: 962815066506496169ull -> 13230018917786094638ull [H-THIN] kBrackets_reads[57]: 13267308122545089892ull -> 1502537382570686975ull [H-THIN] kBrackets_reads[58]: 11888142924425674761ull -> 14318816668076449218ull [H-THIN] kBrackets_reads[59]: 12543244761273968195ull -> 15331500228353786720ull [H-THIN] kBrackets_reads[60]: 743116436140857884ull -> 17346729228089869405ull [H-THIN] kBrackets_reads[61]: 13428981689322201921ull -> 1561708263858075840ull [H-THIN] kBrackets_reads[62]: 3888528505659186179ull -> 229263709144259356ull [H-THIN] kBrackets_reads[63]: 16920970929338735704ull -> 17327472028118720615ull [H-THIN] kBrackets_reads[64]: 11486836585396753956ull -> 12182983198162567319ull [H-THIN] kBrackets_reads[65]: 3912352931341674340ull -> 8439356152210307823ull [H-THIN] kBrackets_reads[66]: 488391584166124984ull -> 11858668009824522697ull [H-THIN] kBrackets_reads[67]: 594818243880175417ull -> 10098763073045529668ull [H-THIN] kBrackets_reads[68]: 5897410099435466572ull -> 178845652746320211ull [H-THIN] kBrackets_reads[69]: 6292341051554398594ull -> 2011898511618741601ull [H-THIN] kBrackets_reads[70]: 5652341650549197297ull -> 10212557729617550900ull [H-THIN] kBrackets_reads[71]: 15500575164592052866ull -> 17051694765221730851ull [H-THIN] kBrackets_reads[72]: 6616463239552538973ull -> 9868130284962937766ull [H-THIN] kBrackets_reads[73]: 6924796937138718592ull -> 18032334360967082277ull [H-THIN] kBrackets_reads[74]: 12199564954470416935ull -> 14727145473477122778ull [H-THIN] kBrackets_reads[75]: 1262923056330410530ull -> 16259529515940238053ull [H-THIN] kBrackets_reads[76]: 14580867601298688079ull -> 16828614615166433170ull [H-THIN] kBrackets_reads[77]: 695300185954652101ull -> 8468897962405976228ull [H-THIN] kBrackets_reads[78]: 4911858693999487313ull -> 5207571868172488684ull [H-THIN] kBrackets_reads[79]: 4052266239546809097ull -> 7657491728824699978ull [H-THIN] kBrackets_reads[80]: 16261647227390403486ull -> 7536338060220416859ull [H-THIN] kBrackets_reads[81]: 9269996099404471907ull -> 16443584424917468468ull [H-THIN] kBrackets_reads[82]: 17364765293714899346ull -> 5422665264074241505ull [H-THIN] kBrackets_reads[83]: 9253740477123675482ull -> 8096893673298686321ull [H-THIN] kBrackets_reads[84]: 3598536848022417347ull -> 10330127297609212492ull [H-THIN] kBrackets_reads[85]: 15772584097012446157ull -> 3143371143401203384ull [H-THIN] kBrackets_reads[86]: 13220139554987575832ull -> 17744960029344161943ull [H-THIN] kBrackets_reads[87]: 5696897036054960153ull -> 743800041325432758ull [H-THIN] kBrackets_reads[88]: 16985841889497997093ull -> 576351043472799110ull [H-THIN] kBrackets_reads[89]: 10415231377040584138ull -> 15626777699481064933ull [H-THIN] kBrackets_reads[90]: 12984613912057588504ull -> 25613991977989083ull [H-THIN] kBrackets_reads[91]: 598945118998222943ull -> 423738522170636072ull [H-THIN] kBrackets_reads[92]: 6912671141973928854ull -> 2370489562900647761ull [H-THIN] kBrackets_reads[93]: 18429543982731833536ull -> 5138368878345319471ull [H-THIN] kBrackets_reads[94]: 5917122860046364322ull -> 4753877877775442691ull [H-THIN] kBrackets_reads[95]: 14977518267164775357ull -> 17962273710729275004ull [H-THIN] kBrackets_reads[96]: 15294004932482913795ull -> 17588425223806176188ull [H-THIN] kBrackets_reads[97]: 14861726070560837386ull -> 17355922851713147397ull [H-THIN] kBrackets_reads[98]: 14467806706499675692ull -> 5559047461962127399ull [H-THIN] kBrackets_reads[99]: 6260501953792293914ull -> 15923451304054496993ull [H-THIN] kBrackets_reads[100]: 8227642000683598159ull -> 1395104389648954276ull [H-THIN] kBrackets_reads[101]: 16328367771257271624ull -> 12594813164804610403ull [H-THIN] kBrackets_reads[102]: 2080811162938952899ull -> 8499202279082002358ull [H-THIN] kBrackets_reads[103]: 4230200989955191115ull -> 3976288064231381934ull [H-THIN] kBrackets_reads[104]: 2243443394831501937ull -> 6053679002470499114ull [H-THIN] kBrackets_reads[105]: 14575945386085730904ull -> 16698836563482206307ull [H-THIN] kBrackets_reads[106]: 7236032703621894143ull -> 9330221417640539684ull [H-THIN] kBrackets_reads[107]: 11053348144569293517ull -> 11686916916826934224ull [H-THIN] kBrackets_reads[108]: 12741152113770246445ull -> 1635100190140340380ull [H-THIN] kBrackets_reads[109]: 6345919259715423413ull -> 14429355558491266072ull [H-THIN] kBrackets_reads[110]: 4116643839905048431ull -> 2638545206399705470ull [H-THIN] kBrackets_reads[111]: 13283796175120077278ull -> 9334315816579956891ull [H-THIN] kBrackets_reads[112]: 14167185253504234566ull -> 4285818535841220481ull [H-THIN] kBrackets_reads[113]: 11430800879610209506ull -> 6571870152547998309ull [H-THIN] kBrackets_reads[114]: 14033714145752220328ull -> 5503577911173824511ull [H-THIN] kBrackets_reads[115]: 739675710812434647ull -> 8243465699060733690ull [H-THIN] kBrackets_reads[116]: 11662106361049752258ull -> 418090560305378255ull [H-THIN] kBrackets_reads[117]: 8075508688952706341ull -> 17066859890889434988ull [H-THIN] kBrackets_reads[118]: 3795209660336936451ull -> 888971320708135628ull [H-THIN] kBrackets_reads[119]: 10247852932638933229ull -> 5198182708464165974ull [H-THIN] kBrackets_final[0]: 12419458164604031287ull -> 6645823410963443848ull [H-THIN] kChains_rows[0]: 18068305618654701499ull -> 17216550202785438742ull [H-THIN] kChains_rows[1]: 7631310091482109571ull -> 888925525783100684ull [H-THIN] kChains_rows[2]: 3548794957611195047ull -> 14971086878838481907ull [H-THIN] kChains_rows[3]: 9238067768069886424ull -> 1294272531176600360ull [H-THIN] kChains_rows[4]: 15801514563772350680ull -> 1886455716578836766ull [H-THIN] kChains_rows[5]: 17906063143479564342ull -> 11637635059042252090ull [H-THIN] kChains_rows[6]: 14314488034311577333ull -> 17515667699609373101ull [H-THIN] kChains_rows[7]: 10547448244184431820ull -> 7000083051162580056ull [H-THIN] kChains_rows[8]: 7123489397519609406ull -> 9300983128218736014ull [H-THIN] kChains_rows[9]: 1602213910524840811ull -> 9958894908037006139ull [H-THIN] kChains_rows[10]: 9787597445052736151ull -> 1635532543853096379ull [H-THIN] kChains_rows[11]: 17601291968173311470ull -> 16017861459144616409ull [H-THIN] kChains_rows[12]: 5776057093989319106ull -> 17338287424821881179ull [H-THIN] kChains_rows[13]: 5050622455696579782ull -> 1983186311692138031ull [H-THIN] kChains_rows[14]: 14615448368462347221ull -> 9090376350238380314ull [H-THIN] kChains_rows[15]: 17881255140791172527ull -> 12881684737752619620ull [H-THIN] kChains_rows[16]: 12765402170990768512ull -> 15135573007448004119ull [H-THIN] kChains_rows[17]: 17731227218325476252ull -> 9716971973878048845ull [H-THIN] kChains_rows[18]: 1751899773874851993ull -> 1434832193067712479ull [H-THIN] kChains_rows[19]: 17508832565167325456ull -> 13391945418166012328ull [H-THIN] kChains_rows[20]: 9700834346346522071ull -> 4222942739049503617ull [H-THIN] kChains_rows[21]: 1282406435677068885ull -> 14263738008833632447ull [H-THIN] kChains_rows[22]: 3633697705136355869ull -> 13268772213335834263ull [H-THIN] kChains_rows[23]: 7615819126236245163ull -> 5997455233863395761ull [H-THIN] kChains_rows[24]: 483990815715194534ull -> 1068503963899776944ull [H-THIN] kChains_rows[25]: 17161117397723763954ull -> 17141612644617594564ull [H-THIN] kChains_rows[26]: 13706192373597017281ull -> 9871144343592514033ull [H-THIN] kChains_rows[27]: 8349535569627227390ull -> 5276840031980785269ull [H-THIN] kChains_rows[28]: 1043431932614584276ull -> 15355133066636605819ull [H-THIN] kChains_rows[29]: 14601237308263653315ull -> 2728946953552743916ull [H-THIN] kChains_rows[30]: 5546360134897835890ull -> 15136838873153049319ull [H-THIN] kChains_rows[31]: 6847183254477845044ull -> 5645466354956180049ull [H-THIN] kChains_rows[32]: 780618388197566036ull -> 18409791973237644079ull [H-THIN] kChains_rows[33]: 1869891317481091735ull -> 14278855388951708356ull [H-THIN] kChains_rows[34]: 14666026974162495080ull -> 17266360427613746801ull [H-THIN] kChains_rows[35]: 2462504664095454997ull -> 18261188449347737966ull [H-THIN] kChains_rows[36]: 9050713444551814707ull -> 10896461346494157758ull [H-THIN] kChains_rows[37]: 18273004200638640909ull -> 16041849805505875804ull [H-THIN] kChains_rows[38]: 2005853044969644190ull -> 10185251185371103195ull [H-THIN] kChains_rows[39]: 3149925508439969851ull -> 1646822496082916590ull [H-THIN] kChains_rows[40]: 5854844698006220443ull -> 7947291941730160766ull [H-THIN] kChains_rows[41]: 9381219014372881502ull -> 8578518959639923799ull [H-THIN] kChains_rows[42]: 18324992465300580137ull -> 18335246465501349816ull [H-THIN] kChains_rows[43]: 1461894063981629002ull -> 9718231080356630143ull [H-THIN] kChains_rows[44]: 17437165102333019839ull -> 16219609048603415226ull [H-THIN] kChains_rows[45]: 8044544612955041611ull -> 10959371403202421450ull [H-THIN] kChains_rows[46]: 16351394341610307986ull -> 17602634145882978471ull [H-THIN] kChains_rows[47]: 14412883952315230739ull -> 8425686225706397430ull [H-THIN] kChains_rows[48]: 10721912884113213397ull -> 12708049332763053506ull [H-THIN] kChains_rows[49]: 12992048657322263867ull -> 7627006390785584896ull [H-THIN] kChains_rows[50]: 3339483349076882774ull -> 8859726068956568565ull [H-THIN] kChains_rows[51]: 12589462032708666350ull -> 3362393005477244925ull [H-THIN] kChains_rows[52]: 4157120851447613547ull -> 5181063662459076181ull [H-THIN] kChains_rows[53]: 14533664878044930220ull -> 11384647369363252140ull [H-THIN] kChains_rows[54]: 10713585920480808169ull -> 14541944896990249375ull [H-THIN] kChains_rows[55]: 2002537927364435313ull -> 2314324140859013727ull [H-THIN] kChains_rows[56]: 10170703978995689194ull -> 13411671664513209746ull [H-THIN] kChains_rows[57]: 3860537231187349238ull -> 5868706585110359110ull [H-THIN] kChains_rows[58]: 13713491075625798656ull -> 10711939723010575336ull [H-THIN] kChains_rows[59]: 608981548879404887ull -> 3729675833838621372ull [H-THIN] kChains_rows[60]: 13504708270136522593ull -> 9243393460869700874ull [H-THIN] kChains_rows[61]: 6427407450358014981ull -> 17371699057649433318ull [H-THIN] kChains_rows[62]: 4929862301177442641ull -> 3997723996730063192ull [H-THIN] kChains_rows[63]: 2268853989619820532ull -> 11433050125327522239ull [H-THIN] kChains_rows[64]: 6103887638466893787ull -> 12541125813245406968ull [H-THIN] kChains_rows[65]: 11509749389073642609ull -> 9039672968915681536ull [H-THIN] kChains_rows[66]: 18133726165290830677ull -> 15608011746644681292ull [H-THIN] kChains_rows[67]: 15915101668393006094ull -> 5054687594698147473ull [H-THIN] kChains_rows[68]: 5109293539399095562ull -> 3477709473812172570ull [H-THIN] kChains_rows[69]: 4762639120474267940ull -> 10152311901395946216ull [H-THIN] kChains_rows[70]: 16578121073498307040ull -> 8891349751852823074ull [H-THIN] kChains_rows[71]: 12298653987130833045ull -> 16144546084129249441ull [H-THIN] kChains_rows[72]: 14615479905553429955ull -> 13685016483773539513ull [H-THIN] kChains_rows[73]: 8498021699752483664ull -> 13522218457313117986ull [H-THIN] kChains_rows[74]: 2237995378251169439ull -> 4995503363249481535ull [H-THIN] kChains_rows[75]: 3400521619940694194ull -> 16748306406958785033ull [H-THIN] kChains_rows[76]: 13553756526528636558ull -> 9311515972501057953ull [H-THIN] kChains_rows[77]: 2109463370083716881ull -> 7365920725454078940ull [H-THIN] kChains_rows[78]: 14168593821002454122ull -> 8173149860273300269ull [H-THIN] kChains_rows[79]: 16341261596541620307ull -> 3129205185712362172ull [H-THIN] kChains_rows[80]: 10041235417076058114ull -> 10311992612364215267ull [H-THIN] kChains_rows[81]: 7909486330303743747ull -> 6430081021816923700ull [H-THIN] kChains_rows[82]: 11640249140605321865ull -> 3812940485274326802ull [H-THIN] kChains_rows[83]: 1871933448816404727ull -> 12258588714698561684ull [H-THIN] kChains_rows[84]: 10857133212749050668ull -> 16977065476345962723ull [H-THIN] kChains_rows[85]: 11914116902843745018ull -> 16840034821380621151ull [H-THIN] kChains_rows[86]: 8338386234307664155ull -> 196921697771132750ull [H-THIN] kChains_rows[87]: 7693563294105136383ull -> 1532115488081461896ull [H-THIN] kChains_rows[88]: 4393637264257635043ull -> 11299803424734145910ull [H-THIN] kChains_rows[89]: 10817114663804369156ull -> 843050419706294665ull [H-THIN] kChains_rows[90]: 777552857201851974ull -> 3060973841661433401ull [H-THIN] kChains_rows[91]: 4318860778166919945ull -> 1061493805797114660ull [H-THIN] kChains_rows[92]: 3440765181079071375ull -> 11945392687849806220ull [H-THIN] kChains_rows[93]: 18150517180267406702ull -> 2644275880755387635ull [H-THIN] kChains_rows[94]: 3822860841058540379ull -> 18268997097508906988ull [H-THIN] kChains_rows[95]: 18408233930596919548ull -> 10440378040220682715ull [H-THIN] kChains_rows[96]: 15483250115370548957ull -> 5460631613847058284ull [H-THIN] kChains_rows[97]: 12187081253452044158ull -> 90541039206026679ull [H-THIN] kChains_rows[98]: 15096704468692058143ull -> 1525179726704156361ull [H-THIN] kChains_rows[99]: 351395931644162269ull -> 4568196733230345761ull [H-THIN] kChains_rows[100]: 7681099562665395463ull -> 4900564848107415469ull [H-THIN] kChains_rows[101]: 621673586916640985ull -> 905839454594031045ull [H-THIN] kChains_rows[102]: 1548670563434988819ull -> 5600806767076858089ull [H-THIN] kChains_rows[103]: 15018421197641490508ull -> 15853491948794316602ull [H-THIN] kChains_rows[104]: 14843102830690661585ull -> 4436926389284220729ull [H-THIN] kChains_rows[105]: 18078373819427574057ull -> 12484426071009033621ull [H-THIN] kChains_rows[106]: 2156517456339179906ull -> 14565732008483823358ull [H-THIN] kChains_rows[107]: 1550063316561336971ull -> 5421750991390559718ull [H-THIN] kChains_rows[108]: 682120972313074028ull -> 16654253476033026771ull [H-THIN] kChains_rows[109]: 13222505441996051255ull -> 4914934475284686956ull [H-THIN] kChains_rows[110]: 894762928574935768ull -> 11353492294670403361ull [H-THIN] kChains_rows[111]: 6921956189824964594ull -> 2952007191267046267ull [H-THIN] kChains_rows[112]: 1470489317032329184ull -> 4137400735535674785ull [H-THIN] kChains_rows[113]: 3757845431780800368ull -> 13313570887318196803ull [H-THIN] kChains_rows[114]: 4291423602824827155ull -> 16333832917038083335ull [H-THIN] kChains_rows[115]: 11888096106209469513ull -> 15983016049430097831ull [H-THIN] kChains_rows[116]: 3227573574798329590ull -> 16516441775145748186ull [H-THIN] kChains_rows[117]: 13089038313828989382ull -> 9980463146813617446ull [H-THIN] kChains_rows[118]: 7508021719959037954ull -> 14892416906114576210ull [H-THIN] kChains_rows[119]: 2308535366701241976ull -> 6560664027287701548ull [H-THIN] kChains_rows[120]: 1246288425489315813ull -> 11333039054045879633ull [H-THIN] kChains_rows[121]: 11433845872149110256ull -> 10624127326473164730ull [H-THIN] kChains_rows[122]: 10295145752395079770ull -> 10585896247857052348ull [H-THIN] kChains_rows[123]: 2890366903333661801ull -> 9715815904094644978ull [H-THIN] kChains_rows[124]: 9960659940131424438ull -> 12043177962550059801ull [H-THIN] kChains_rows[125]: 14272369748982538569ull -> 6319376608257543486ull [H-THIN] kChains_rows[126]: 17076452457032941051ull -> 16904749238070947842ull [H-THIN] kChains_rows[127]: 3424791957006073618ull -> 6095240370596913147ull [H-THIN] kChains_rows[128]: 1811248193508923525ull -> 4788859803642606588ull [H-THIN] kChains_rows[129]: 14588001261256884000ull -> 6996529386998857627ull [H-THIN] kChains_rows[130]: 13923059957978311005ull -> 4078288008260849702ull [H-THIN] kChains_rows[131]: 16983695715066873498ull -> 4804551383567784305ull [H-THIN] kChains_rows[132]: 608375160627328848ull -> 9950943025990090361ull [H-THIN] kChains_rows[133]: 8684084180111108347ull -> 884570081640732608ull [H-THIN] kChains_rows[134]: 15679623628995063704ull -> 626856908236235819ull [H-THIN] kChains_rows[135]: 8464528085679884071ull -> 6383254879440463492ull [H-THIN] kChains_rows[136]: 14932854744042668023ull -> 17222449793445921158ull [H-THIN] kChains_rows[137]: 569982362832841031ull -> 12876151944089409278ull [H-THIN] kChains_rows[138]: 7496415600731368183ull -> 664057026045247402ull [H-THIN] kChains_rows[139]: 6021406431901867511ull -> 14473992423923157020ull [H-THIN] kChains_rows[140]: 14375074717677642639ull -> 16134349783577291380ull [H-THIN] kChains_rows[141]: 15805523719787038613ull -> 17824754404789806578ull [H-THIN] kChains_rows[142]: 12126947588968916209ull -> 11922334275858293940ull [H-THIN] kChains_rows[143]: 11019915590207223465ull -> 12324529413121065212ull [H-THIN] kChains_rows[144]: 12152802468277950710ull -> 12586161209074725511ull [H-THIN] kChains_rows[145]: 9974141365018685887ull -> 17841333238070299986ull [H-THIN] kChains_rows[146]: 7966238851102234546ull -> 4939054549376478538ull [H-THIN] kChains_rows[147]: 9747060662483912123ull -> 12653974792798479453ull [H-THIN] kChains_rows[148]: 1717605098271755502ull -> 168169012948749036ull [H-THIN] kChains_rows[149]: 9342227818610348684ull -> 1600379719717813316ull [H-THIN] kChains_rows[150]: 2114030550145565950ull -> 9597243467462794310ull [H-THIN] kChains_rows[151]: 418023209795270565ull -> 746366577121939789ull [H-THIN] kChains_rows[152]: 15770927766697271034ull -> 4939870284827214452ull [H-THIN] kChains_rows[153]: 12606063420003019757ull -> 14763601424926594359ull [H-THIN] kChains_rows[154]: 1303915722987961013ull -> 14442078822824496949ull [H-THIN] kChains_rows[155]: 11281002575808296045ull -> 10722539175905485350ull [H-THIN] kChains_rows[156]: 5386973648178682980ull -> 1668204868435100495ull [H-THIN] kChains_rows[157]: 7792848154392720004ull -> 3125339228212761319ull [H-THIN] kChains_rows[158]: 65916750986234223ull -> 12970829842683432868ull [H-THIN] kChains_rows[159]: 13869421282421271494ull -> 14563213551732184257ull [H-THIN] kChains_reads[0]: 9570007664485953953ull -> 10110664357585261028ull [H-THIN] kChains_reads[1]: 1562834205308840657ull -> 5809989452861246834ull [H-THIN] kChains_reads[2]: 9925417414736888460ull -> 8219150533614246984ull [H-THIN] kChains_reads[3]: 16797489091140150745ull -> 9160394247003716585ull [H-THIN] kChains_reads[4]: 8864771617498668987ull -> 8016378449870904253ull [H-THIN] kChains_reads[5]: 10813620960703012830ull -> 9413734555269737202ull [H-THIN] kChains_reads[6]: 9917769484038902883ull -> 10408793874733580811ull [H-THIN] kChains_reads[7]: 11399566133788975554ull -> 2285942971403387014ull [H-THIN] kChains_reads[8]: 13065799061037800748ull -> 7421889705292008284ull [H-THIN] kChains_reads[9]: 10473842100614025729ull -> 17682777079617910737ull [H-THIN] kChains_reads[10]: 10356315431649319903ull -> 4481707948696649371ull [H-THIN] kChains_reads[11]: 6718697686702398031ull -> 7827336907453999164ull [H-THIN] kChains_reads[12]: 3085131610026641094ull -> 14974364552734208895ull [H-THIN] kChains_reads[13]: 7920913286497161545ull -> 13128401401658011940ull [H-THIN] kChains_reads[14]: 6965112488735434783ull -> 17827869807111927604ull [H-THIN] kChains_reads[15]: 17824918572352876790ull -> 9719840719700136041ull [H-THIN] kChains_reads[16]: 431137757590538768ull -> 2665859956187061075ull [H-THIN] kChains_reads[17]: 5096278457151630514ull -> 9254561921243745847ull [H-THIN] kChains_reads[18]: 16629172470762388288ull -> 2360259598999100592ull [H-THIN] kChains_reads[19]: 11652117991390508417ull -> 12205711579178689313ull [H-THIN] kChains_reads[20]: 2618729299425190784ull -> 14232976407316743690ull [H-THIN] kChains_reads[21]: 212332156515024145ull -> 5965032367641075215ull [H-THIN] kChains_reads[22]: 12246593551859292419ull -> 14573502756353459737ull [H-THIN] kChains_reads[23]: 12545038087661601474ull -> 15400971490512734092ull [H-THIN] kChains_reads[24]: 1460766259544383584ull -> 4098087993772423194ull [H-THIN] kChains_reads[25]: 10861778317578249995ull -> 6112934256622287033ull [H-THIN] kChains_reads[26]: 20932238954851345ull -> 15862596503276202833ull [H-THIN] kChains_reads[27]: 18286529377979065696ull -> 13670568930995334427ull [H-THIN] kChains_reads[28]: 5666326454301174821ull -> 9788130664428183490ull [H-THIN] kChains_reads[29]: 6228635006760178710ull -> 16335392583796627193ull [H-THIN] kChains_reads[30]: 9906791842642905532ull -> 4410190916080756281ull [H-THIN] kChains_reads[31]: 2721687798792728445ull -> 12834531680111998344ull [H-THIN] kChains_reads[32]: 16653381899846261400ull -> 14109653350817882099ull [H-THIN] kChains_reads[33]: 16503363491014194836ull -> 7442946405223661239ull [H-THIN] kChains_reads[34]: 5987209830733009584ull -> 4479757411535168501ull [H-THIN] kChains_reads[35]: 11879508822085901633ull -> 1056910844796346930ull [H-THIN] kChains_reads[36]: 4428380683177019699ull -> 10866647835240895022ull [H-THIN] kChains_reads[37]: 6702580677339138469ull -> 8626263364100578116ull [H-THIN] kChains_reads[38]: 12470873918402201930ull -> 5095382084298433023ull [H-THIN] kChains_reads[39]: 7755601604835325521ull -> 2483955861156609744ull [H-THIN] kChains_reads[40]: 17100550686359839522ull -> 10313301897794116551ull [H-THIN] kChains_reads[41]: 9982417146672614117ull -> 8562911020376654396ull [H-THIN] kChains_reads[42]: 16219053929893216833ull -> 8063731069482460736ull [H-THIN] kChains_reads[43]: 8335500495109512692ull -> 11518609544766773477ull [H-THIN] kChains_reads[44]: 17499407126158105512ull -> 5810028710378801057ull [H-THIN] kChains_reads[45]: 8520742716124176357ull -> 61058730138114860ull [H-THIN] kChains_reads[46]: 6969237881725548098ull -> 2100804384988582495ull [H-THIN] kChains_reads[47]: 6167898056600340972ull -> 7209163061824649505ull [H-THIN] kChains_reads[48]: 10796080984723706778ull -> 4878378036811209549ull [H-THIN] kChains_reads[49]: 2659129401831175873ull -> 4404719757894447566ull [H-THIN] kChains_reads[50]: 4113136533365713821ull -> 4720502478547523234ull [H-THIN] kChains_reads[51]: 16959859587150714548ull -> 1753398606656246895ull [H-THIN] kChains_reads[52]: 11977596023171988133ull -> 16246112928648551635ull [H-THIN] kChains_reads[53]: 3997121899712306414ull -> 10657174667652890190ull [H-THIN] kChains_reads[54]: 302851387997930277ull -> 14743154045868530737ull [H-THIN] kChains_reads[55]: 7004784366449869436ull -> 6268694762545713086ull [H-THIN] kChains_reads[56]: 17731084291599622060ull -> 16976140361266671940ull [H-THIN] kChains_reads[57]: 2201184572610804096ull -> 10119949631905980432ull [H-THIN] kChains_reads[58]: 15700916709431740066ull -> 11203266787989620834ull [H-THIN] kChains_reads[59]: 4158286781895941687ull -> 17512206047802952924ull [H-THIN] kChains_reads[60]: 5992524740679929691ull -> 13293769686508049256ull [H-THIN] kChains_reads[61]: 1449426275268650261ull -> 8545075081067204330ull [H-THIN] kChains_reads[62]: 18162824504997853425ull -> 12091509383956641936ull [H-THIN] kChains_reads[63]: 14899520499454266758ull -> 9675593530383755541ull [H-THIN] kChains_reads[64]: 13393817499128108248ull -> 8212271365170382599ull [H-THIN] kChains_reads[65]: 10711743217016099970ull -> 16962447560998690087ull [H-THIN] kChains_reads[66]: 2691444242389935931ull -> 1232604283450205942ull [H-THIN] kChains_reads[67]: 8294060884410393919ull -> 2707154508237646712ull [H-THIN] kChains_reads[68]: 12352805812450672776ull -> 11313850582244987544ull [H-THIN] kChains_reads[69]: 696110624400704576ull -> 14369487380029217132ull [H-THIN] kChains_reads[70]: 10843628086540515552ull -> 11921066809935672874ull [H-THIN] kChains_reads[71]: 8971856522667982633ull -> 14007992411558869707ull [H-THIN] kChains_reads[72]: 11210433033614009510ull -> 6598319271298697372ull [H-THIN] kChains_reads[73]: 8384649526502906040ull -> 5417331013900840530ull [H-THIN] kChains_reads[74]: 2393883974530973309ull -> 13345232768434677145ull [H-THIN] kChains_reads[75]: 4665626305905607638ull -> 1935249115057575465ull [H-THIN] kChains_reads[76]: 8867654732330596907ull -> 13220457444093362632ull [H-THIN] kChains_reads[77]: 4416041359039881293ull -> 13358973226079502740ull [H-THIN] kChains_reads[78]: 13719154741412155838ull -> 2889830699990711661ull [H-THIN] kChains_reads[79]: 7098510039070530028ull -> 1211171047890347763ull [H-THIN] kChains_reads[80]: 10831403092179419039ull -> 18014776936753683306ull [H-THIN] kChains_reads[81]: 8578164481500217838ull -> 14390427333071081721ull [H-THIN] kChains_reads[82]: 14156042948797799583ull -> 7011925151867020864ull [H-THIN] kChains_reads[83]: 18079113731505369907ull -> 3169302683092777636ull [H-THIN] kChains_reads[84]: 11217579006112077760ull -> 8884683607532542707ull [H-THIN] kChains_reads[85]: 578274504487938291ull -> 1220390021601311746ull [H-THIN] kChains_reads[86]: 12858268482564452648ull -> 4563751557195908857ull [H-THIN] kChains_reads[87]: 13457093904989238219ull -> 9371430556201687572ull [H-THIN] kChains_reads[88]: 7535108913864740263ull -> 8755857123799478610ull [H-THIN] kChains_reads[89]: 4941944547036948489ull -> 11347889350981525264ull [H-THIN] kChains_reads[90]: 15143761260194159051ull -> 2948337259249410352ull [H-THIN] kChains_reads[91]: 3169992748722096748ull -> 16392540589482694957ull [H-THIN] kChains_reads[92]: 2935645212104261694ull -> 13084006063425074665ull [H-THIN] kChains_reads[93]: 1503156579032907045ull -> 13166369752734976728ull [H-THIN] kChains_reads[94]: 12825108615254555255ull -> 751990060505666340ull [H-THIN] kChains_reads[95]: 11152144432285654023ull -> 5970943418956605684ull [H-THIN] kChains_reads[96]: 14374425704408319362ull -> 10521883552184644999ull [H-THIN] kChains_reads[97]: 13181125330469706258ull -> 16052318444984852327ull [H-THIN] kChains_reads[98]: 8173826051536434519ull -> 11187570176199389997ull [H-THIN] kChains_reads[99]: 3445997924383545490ull -> 15460763283392291862ull [H-THIN] kChains_reads[100]: 6587016227990886659ull -> 10338663625416646013ull [H-THIN] kChains_reads[101]: 4779228536467194685ull -> 4881396789477267769ull [H-THIN] kChains_reads[102]: 13151647749864074127ull -> 4212938202001384025ull [H-THIN] kChains_reads[103]: 9173722582032837076ull -> 14988355991036891650ull [H-THIN] kChains_reads[104]: 14434113364655604703ull -> 17837779332496626471ull [H-THIN] kChains_reads[105]: 749323963540978962ull -> 11682505807010385846ull [H-THIN] kChains_reads[106]: 9886586197280394206ull -> 17870499886893847586ull [H-THIN] kChains_reads[107]: 17593161773407266268ull -> 679199023029947373ull [H-THIN] kChains_reads[108]: 16651394056260196842ull -> 3970199140902816509ull [H-THIN] kChains_reads[109]: 5523306718191251504ull -> 17401534532171766295ull [H-THIN] kChains_reads[110]: 12137483147531194660ull -> 775181258658200153ull [H-THIN] kChains_reads[111]: 1385603333851191898ull -> 16283811115647142047ull [H-THIN] kChains_reads[112]: 4496586139598675537ull -> 1589955952552870168ull [H-THIN] kChains_reads[113]: 8942420997722419200ull -> 1739626929334374295ull [H-THIN] kChains_reads[114]: 16186100995544900609ull -> 14787527140441919317ull [H-THIN] kChains_reads[115]: 10571664598865577953ull -> 9655802592323111739ull [H-THIN] kChains_reads[116]: 9783208691725612411ull -> 5654513164848938839ull [H-THIN] kChains_reads[117]: 17727619977969867138ull -> 10659820542718417314ull [H-THIN] kChains_reads[118]: 12308607250191287290ull -> 7500865454428219602ull [H-THIN] kChains_reads[119]: 13136936254003785978ull -> 14944030626459508318ull [H-THIN] kChains_reads[120]: 4799706175857105538ull -> 8358528417073095116ull [H-THIN] kChains_reads[121]: 15342824296389601806ull -> 12655664015256093170ull [H-THIN] kChains_reads[122]: 10278727004657844890ull -> 13209887606846213484ull [H-THIN] kChains_reads[123]: 40192321974480982ull -> 6069560199676759525ull [H-THIN] kChains_reads[124]: 9783890578221359931ull -> 6696082621071745728ull [H-THIN] kChains_reads[125]: 11215532213811366070ull -> 757716102432190821ull [H-THIN] kChains_reads[126]: 14480169342549825019ull -> 15487397720156294866ull [H-THIN] kChains_reads[127]: 15414792187977659182ull -> 884148487135854651ull [H-THIN] kChains_reads[128]: 8493082597081956684ull -> 3880366816499564477ull [H-THIN] kChains_reads[129]: 8564012831232873370ull -> 15047621949732128129ull [H-THIN] kChains_reads[130]: 5491576330870414255ull -> 9322903483901568796ull [H-THIN] kChains_reads[131]: 14022487705176382100ull -> 687002260497979135ull [H-THIN] kChains_reads[132]: 4124805857279107720ull -> 9491707471648859049ull [H-THIN] kChains_reads[133]: 15467889468829316615ull -> 8185964782213665656ull [H-THIN] kChains_reads[134]: 7295497316328640107ull -> 1858930161053607088ull [H-THIN] kChains_reads[135]: 15082002607429366135ull -> 1233846040989093404ull [H-THIN] kChains_reads[136]: 12499158975010305465ull -> 6120927370816337024ull [H-THIN] kChains_reads[137]: 3208794190730501637ull -> 4886803687938088384ull [H-THIN] kChains_reads[138]: 15064216799612457293ull -> 16477410875523784452ull [H-THIN] kChains_reads[139]: 16974691537614575812ull -> 4392674394804262923ull [H-THIN] kChains_reads[140]: 1426687806059238508ull -> 5769985102691339939ull [H-THIN] kChains_reads[141]: 17375680129214845717ull -> 11070202918036379862ull [H-THIN] kChains_reads[142]: 12487972796056251866ull -> 18160274419371647815ull [H-THIN] kChains_reads[143]: 8080964710494461719ull -> 8829636490162765266ull [H-THIN] kChains_reads[144]: 1415692574356635417ull -> 6601601345405395420ull [H-THIN] kChains_reads[145]: 9772128015242412363ull -> 2898341262682614174ull [H-THIN] kChains_reads[146]: 18331800931612105183ull -> 16796631727010079735ull [H-THIN] kChains_reads[147]: 15912555856562133268ull -> 15187481886734015430ull [H-THIN] kChains_reads[148]: 16674262793511329036ull -> 18259141933065442106ull [H-THIN] kChains_reads[149]: 12709672117837958972ull -> 7779036192932713702ull [H-THIN] kChains_reads[150]: 533937075939181997ull -> 16368000199937753609ull [H-THIN] kChains_reads[151]: 13997036310862179778ull -> 6523761455165153646ull [H-THIN] kChains_reads[152]: 2303353295064390890ull -> 7426464792584462270ull [H-THIN] kChains_reads[153]: 13136856999282398145ull -> 5887376615432929807ull [H-THIN] kChains_reads[154]: 9661353626625839895ull -> 6126763591804873111ull [H-THIN] kChains_reads[155]: 7809505236198215515ull -> 7982613224094165644ull [H-THIN] kChains_reads[156]: 9689360779147915768ull -> 3510856804336884675ull [H-THIN] kChains_reads[157]: 7546140302872708152ull -> 14582091139041748587ull [H-THIN] kChains_reads[158]: 11057158687385367268ull -> 1120801175490593191ull [H-THIN] kChains_reads[159]: 930681743930080351ull -> 2552028032359677900ull [H-THIN] kChains_final[0]: 13869421282421271494ull -> 14563213551732184257ull [H-THIN] kMargin_rows[0]: 9541482286738450374ull -> 15705115564979405549ull [H-THIN] kMargin_rows[1]: 10604897191185230729ull -> 2899650352150822234ull [H-THIN] kMargin_rows[2]: 10430957227712956259ull -> 7276138441216849650ull [H-THIN] kMargin_rows[3]: 17038118833282111132ull -> 10214260769585446445ull [H-THIN] kMargin_rows[4]: 6880506736019270193ull -> 12785408784277752740ull [H-THIN] kMargin_rows[5]: 2574896616226754364ull -> 1444456431648962701ull [H-THIN] kMargin_rows[6]: 14467806575653580994ull -> 10436554947470791241ull [H-THIN] kMargin_rows[7]: 13935231533587438999ull -> 2225406636414512896ull [H-THIN] kMargin_rows[8]: 1848048834540881643ull -> 15643906313119105108ull [H-THIN] kMargin_rows[9]: 18329579972079141513ull -> 9422865728073085008ull [H-THIN] kMargin_rows[10]: 12355688239988236079ull -> 3330174464320359216ull [H-THIN] kMargin_rows[11]: 10991245773765040855ull -> 6235550293720256144ull [H-THIN] kMargin_rows[12]: 5685214256158276764ull -> 2776878453603156051ull [H-THIN] kMargin_rows[13]: 17160692497390069334ull -> 10067815237117653281ull [H-THIN] kMargin_rows[14]: 4009833773659380105ull -> 13418795566150795168ull [H-THIN] kMargin_rows[15]: 13777884366772416026ull -> 16965503610107780005ull [H-THIN] kMargin_rows[16]: 8958056086241362995ull -> 15857294378993443508ull [H-THIN] kMargin_rows[17]: 12207636356642772400ull -> 14981195424080413801ull [H-THIN] kMargin_rows[18]: 3459076114230420156ull -> 4436574227947160453ull [H-THIN] kMargin_rows[19]: 14921426272357766472ull -> 3461714965409448097ull [H-THIN] kMargin_rows[20]: 2345813046963814958ull -> 17713706949036740901ull [H-THIN] kMargin_rows[21]: 285001323442461930ull -> 11924616437869767087ull [H-THIN] kMargin_rows[22]: 16917342746817816499ull -> 10848033720136413570ull [H-THIN] kMargin_rows[23]: 18052258256818146447ull -> 2042183915997550400ull [H-THIN] kMargin_rows[24]: 4781946747460841696ull -> 2178193614679359335ull [H-THIN] kMargin_rows[25]: 8888939313581979724ull -> 17907135566382286865ull [H-THIN] kMargin_rows[26]: 17314806764712940533ull -> 3689997758302326782ull [H-THIN] kMargin_rows[27]: 4508121597259689478ull -> 15415188862026868395ull [H-THIN] kMargin_rows[28]: 11060315425557244321ull -> 2445419420491494904ull [H-THIN] kMargin_rows[29]: 14574236380430121079ull -> 5012856126461730798ull [H-THIN] kMargin_rows[30]: 12392320350472046194ull -> 8792557501267046029ull [H-THIN] kMargin_rows[31]: 12250313226238695007ull -> 16418141106963966556ull [H-THIN] kMargin_rows[32]: 11116092899196726058ull -> 4540186183786023695ull [H-THIN] kMargin_rows[33]: 3440822300002062498ull -> 4565832655366886747ull [H-THIN] kMargin_rows[34]: 6172478543095956935ull -> 16628078784743176294ull [H-THIN] kMargin_rows[35]: 11864943854655903053ull -> 5822402081127036958ull [H-THIN] kMargin_rows[36]: 7350714231396417061ull -> 15419312725777085722ull [H-THIN] kMargin_rows[37]: 18236477525793941949ull -> 2939233122312242652ull [H-THIN] kMargin_rows[38]: 18289535579996812515ull -> 6678780917008540002ull [H-THIN] kMargin_rows[39]: 15597457755908201189ull -> 16097482011424548464ull [H-THIN] kMargin_rows[40]: 11910235219191833807ull -> 30760358683421794ull [H-THIN] kMargin_rows[41]: 9451964279490960683ull -> 14579653650355389700ull [H-THIN] kMargin_rows[42]: 4710825118275752110ull -> 10818557034142368397ull [H-THIN] kMargin_rows[43]: 12851808306320095878ull -> 5905237345792316559ull [H-THIN] kMargin_rows[44]: 8820052433058937619ull -> 10793423326720317090ull [H-THIN] kMargin_rows[45]: 15533164234013109494ull -> 880325936979180181ull [H-THIN] kMargin_rows[46]: 16547449805254710983ull -> 15185473436805269358ull [H-THIN] kMargin_rows[47]: 2596701226294842163ull -> 15710388520607918584ull [H-THIN] kMargin_rows[48]: 17704360989892006448ull -> 3850576270060304957ull [H-THIN] kMargin_rows[49]: 5312178085029999460ull -> 1178190546578254837ull [H-THIN] kMargin_rows[50]: 681706230469294026ull -> 13171343461724749261ull [H-THIN] kMargin_rows[51]: 11208354125027713564ull -> 11067978420491083409ull [H-THIN] kMargin_rows[52]: 13848046050944922829ull -> 11720479467561892020ull [H-THIN] kMargin_rows[53]: 16629321510727683252ull -> 15138731064688668601ull [H-THIN] kMargin_rows[54]: 489654745066395968ull -> 8511910500217359553ull [H-THIN] kMargin_rows[55]: 17259859321488242835ull -> 8297079889818538858ull [H-THIN] kMargin_rows[56]: 15549072595885790096ull -> 11644861686947971857ull [H-THIN] kMargin_rows[57]: 1505011103482109555ull -> 17004698771171900722ull [H-THIN] kMargin_rows[58]: 15289097285747984347ull -> 18287878227163962874ull [H-THIN] kMargin_rows[59]: 1572915281639359538ull -> 894010446558868231ull [H-THIN] kMargin_rows[60]: 11273305892586396472ull -> 17837072773009990909ull [H-THIN] kMargin_rows[61]: 13328838480036624025ull -> 8467609174260254948ull [H-THIN] kMargin_rows[62]: 13633704569586434331ull -> 2211154383026824986ull [H-THIN] kMargin_rows[63]: 17431948770960738100ull -> 9489292208093108441ull [H-THIN] kMargin_rows[64]: 12155616429848711764ull -> 7841002755427896445ull [H-THIN] kMargin_rows[65]: 14869620479028624382ull -> 12404306619029088135ull [H-THIN] kMargin_rows[66]: 6849688694600040229ull -> 12933743527246336464ull [H-THIN] kMargin_rows[67]: 11485893754569323922ull -> 8414768416822022107ull [H-THIN] kMargin_rows[68]: 8360410264576159919ull -> 2802251700686834430ull [H-THIN] kMargin_rows[69]: 461812385935867019ull -> 15879102663736672598ull [H-THIN] kMargin_rows[70]: 2682358547448593291ull -> 5173606717929774458ull [H-THIN] kMargin_rows[71]: 9291455346649087450ull -> 6649901201650792327ull [H-THIN] kMargin_rows[72]: 13216043467298716443ull -> 10171970172678289686ull [H-THIN] kMargin_rows[73]: 359821378104950531ull -> 7545775745369484286ull [H-THIN] kMargin_rows[74]: 16017291574242025244ull -> 13536131498902788541ull [H-THIN] kMargin_rows[75]: 18391126290698081198ull -> 16093469128393328519ull [H-THIN] kMargin_rows[76]: 9855977842531731938ull -> 13705061326497871495ull [H-THIN] kMargin_rows[77]: 5556828515909880503ull -> 12212848506533549122ull [H-THIN] kMargin_rows[78]: 2022314578154758976ull -> 15857272799701280033ull [H-THIN] kMargin_rows[79]: 9416710584143900557ull -> 17560839342008353738ull [H-THIN] kMargin_rows[80]: 6983558299012049585ull -> 7667506666186671346ull [H-THIN] kMargin_rows[81]: 5499696029215209460ull -> 4473219031176272397ull [H-THIN] kMargin_rows[82]: 10875028956400699532ull -> 8077639951555082143ull [H-THIN] kMargin_rows[83]: 2974831859594208707ull -> 14996152118360177682ull [H-THIN] kMargin_rows[84]: 9582163290358494515ull -> 16878410184721456110ull [H-THIN] kMargin_rows[85]: 9192322421103943129ull -> 18147635800513569660ull [H-THIN] kMargin_rows[86]: 11542997064059872401ull -> 2980679299199661728ull [H-THIN] kMargin_rows[87]: 1957400908434967464ull -> 17470380748409866759ull [H-THIN] kMargin_rows[88]: 12038816259661624215ull -> 166277892836680540ull [H-THIN] kMargin_rows[89]: 14854368408513559808ull -> 17231939775446510753ull [H-THIN] kMargin_rows[90]: 4246614380518911616ull -> 42816067293212027ull [H-THIN] kMargin_rows[91]: 2355707666069181745ull -> 3846323461282028320ull [H-THIN] kMargin_rows[92]: 5599822345970336136ull -> 8768462657599765139ull [H-THIN] kMargin_rows[93]: 2471724750358094554ull -> 17231142656948881527ull [H-THIN] kMargin_rows[94]: 11160534888434597406ull -> 10746568987715196899ull [H-THIN] kMargin_rows[95]: 10114708526056156918ull -> 15724008703225232301ull [H-THIN] kMargin_reads[0]: 11431893337342736571ull -> 8953842992177039580ull [H-THIN] kMargin_reads[1]: 10771769035459469695ull -> 5012328506798440776ull [H-THIN] kMargin_reads[2]: 1401088416158264207ull -> 9514401216297696498ull [H-THIN] kMargin_reads[3]: 5990666036994939969ull -> 15149183047650585612ull [H-THIN] kMargin_reads[4]: 10588539850905902429ull -> 11881899715350987928ull [H-THIN] kMargin_reads[5]: 9641395323288135458ull -> 1425710761276111275ull [H-THIN] kMargin_reads[6]: 16415676128453702962ull -> 13395351882243880097ull [H-THIN] kMargin_reads[7]: 3214841672955981307ull -> 8260038522419186580ull [H-THIN] kMargin_reads[8]: 9140481207117864186ull -> 10260307603270775333ull [H-THIN] kMargin_reads[9]: 6758874244345350527ull -> 12973871011726024370ull [H-THIN] kMargin_reads[10]: 7448761481640431279ull -> 10839401170472448604ull [H-THIN] kMargin_reads[11]: 11899269473854959745ull -> 10533296012194166894ull [H-THIN] kMargin_reads[12]: 17623756564591150437ull -> 5742434260673778054ull [H-THIN] kMargin_reads[13]: 3283115927808803070ull -> 4811301513092974085ull [H-THIN] kMargin_reads[14]: 18313686748628355321ull -> 6154676715145880256ull [H-THIN] kMargin_reads[15]: 10517443878246423516ull -> 11718623329127978039ull [H-THIN] kMargin_reads[16]: 14391554715371743723ull -> 6950038654804808040ull [H-THIN] kMargin_reads[17]: 7815903094041248998ull -> 13805316801444424371ull [H-THIN] kMargin_reads[18]: 11128615458197398484ull -> 414578511773924817ull [H-THIN] kMargin_reads[19]: 16868017582914550674ull -> 9193643862650800159ull [H-THIN] kMargin_reads[20]: 17318292280187111457ull -> 12190847399375331806ull [H-THIN] kMargin_reads[21]: 236539664791149342ull -> 3867367786885611091ull [H-THIN] kMargin_reads[22]: 12561323379654259441ull -> 6847694618925606604ull [H-THIN] kMargin_reads[23]: 17901584105989623670ull -> 6840674894831935501ull [H-THIN] kMargin_reads[24]: 1819495894130837698ull -> 4507552530758797165ull [H-THIN] kMargin_reads[25]: 12913364709271178836ull -> 3571654135135270865ull [H-THIN] kMargin_reads[26]: 4003529127479307250ull -> 17242678907144422297ull [H-THIN] kMargin_reads[27]: 17850722142819534182ull -> 14611540648022352171ull [H-THIN] kMargin_reads[28]: 10755120431723778073ull -> 5835653396943151368ull [H-THIN] kMargin_reads[29]: 1731312764757835608ull -> 3378973123396229197ull [H-THIN] kMargin_reads[30]: 13748410692460811819ull -> 2553190776568274428ull [H-THIN] kMargin_reads[31]: 12896133474356421192ull -> 13290865301483451699ull [H-THIN] kMargin_reads[32]: 9031531055096256852ull -> 5500621162831131901ull [H-THIN] kMargin_reads[33]: 8322755136826696671ull -> 15654765185370989222ull [H-THIN] kMargin_reads[34]: 8233453449656235425ull -> 15762424456997007744ull [H-THIN] kMargin_reads[35]: 18274892380474204343ull -> 14487448570790088836ull [H-THIN] kMargin_reads[36]: 9098889810842207669ull -> 4035614041046029754ull [H-THIN] kMargin_reads[37]: 11981003970545376949ull -> 15812215809727058388ull [H-THIN] kMargin_reads[38]: 5208192149483445791ull -> 16715990192788820494ull [H-THIN] kMargin_reads[39]: 13826614122871749667ull -> 37321428960284898ull [H-THIN] kMargin_reads[40]: 16775818024782332415ull -> 12666941559515237858ull [H-THIN] kMargin_reads[41]: 15803353690065673036ull -> 1010686153406847467ull [H-THIN] kMargin_reads[42]: 8607451910366293802ull -> 7352920967322298933ull [H-THIN] kMargin_reads[43]: 15510545365186449286ull -> 15055048281012200503ull [H-THIN] kMargin_reads[44]: 5406260086936373546ull -> 5206129815205501175ull [H-THIN] kMargin_reads[45]: 2510786366313163875ull -> 10700009383247120472ull [H-THIN] kMargin_reads[46]: 15538776584588985979ull -> 4162766575253458174ull [H-THIN] kMargin_reads[47]: 12229521714344207135ull -> 9893952837417789690ull [H-THIN] kMargin_reads[48]: 2212599425252585470ull -> 58686700216684675ull [H-THIN] kMargin_reads[49]: 10390126991709196687ull -> 8699555160482580650ull [H-THIN] kMargin_reads[50]: 13200005853939750424ull -> 8130419440091896455ull [H-THIN] kMargin_reads[51]: 6847492913241704581ull -> 4851947396057958324ull [H-THIN] kMargin_reads[52]: 15796662482477301615ull -> 16202831428428662870ull [H-THIN] kMargin_reads[53]: 5118863315631314158ull -> 5121408789427822267ull [H-THIN] kMargin_reads[54]: 4129944798799153557ull -> 11023335017679833516ull [H-THIN] kMargin_reads[55]: 11319393888042736100ull -> 18170820144655294041ull [H-THIN] kMargin_reads[56]: 14187452542143074307ull -> 15954646464954318706ull [H-THIN] kMargin_reads[57]: 1074727836785604464ull -> 18242003564237332097ull [H-THIN] kMargin_reads[58]: 17195817214176484787ull -> 17443602250907707410ull [H-THIN] kMargin_reads[59]: 1825647699469508785ull -> 17983462397568604996ull [H-THIN] kMargin_reads[60]: 4268279429926179501ull -> 7802923710060199472ull [H-THIN] kMargin_reads[61]: 16957209788610236342ull -> 12722038537362695175ull [H-THIN] kMargin_reads[62]: 3395896204678737755ull -> 13698029093670433558ull [H-THIN] kMargin_reads[63]: 3101221902009845683ull -> 13959755018533544898ull [H-THIN] kMargin_reads[64]: 14178655193129027029ull -> 7345845768217414220ull [H-THIN] kMargin_reads[65]: 11497559614257964788ull -> 1775914585091981109ull [H-THIN] kMargin_reads[66]: 2301625628626628663ull -> 11374578665796930850ull [H-THIN] kMargin_reads[67]: 16134456750164415938ull -> 4643448624875098475ull [H-THIN] kMargin_reads[68]: 7927341186129459600ull -> 7678477935539233885ull [H-THIN] kMargin_reads[69]: 11762885871454101194ull -> 7822693233579776275ull [H-THIN] kMargin_reads[70]: 3662730897532267179ull -> 17668989095822341662ull [H-THIN] kMargin_reads[71]: 5485569566800147480ull -> 4575423088079055321ull [H-THIN] kMargin_reads[72]: 11083493162941664231ull -> 1679134401260872502ull [H-THIN] kMargin_reads[73]: 4553141082613530877ull -> 13201168357824320508ull [H-THIN] kMargin_reads[74]: 9461970734864272828ull -> 18041290181318436105ull [H-THIN] kMargin_reads[75]: 14341766711532273518ull -> 4516335812849986987ull [H-THIN] kMargin_reads[76]: 11087165755233401455ull -> 6621116709511450086ull [H-THIN] kMargin_reads[77]: 5001440655061293740ull -> 5258952806381761949ull [H-THIN] kMargin_reads[78]: 15047261415908188989ull -> 6941350654943584452ull [H-THIN] kMargin_reads[79]: 12156664521037383764ull -> 5840696783726564867ull [H-THIN] kMargin_reads[80]: 8898477763532118925ull -> 13717359001813774118ull [H-THIN] kMargin_reads[81]: 17667654592610722150ull -> 17092887286647179443ull [H-THIN] kMargin_reads[82]: 6706429179434019306ull -> 10689502582681467121ull [H-THIN] kMargin_reads[83]: 1344134952986164358ull -> 15622799582419499315ull [H-THIN] kMargin_reads[84]: 7134564545056141913ull -> 1793938846847329952ull [H-THIN] kMargin_reads[85]: 755502931104769977ull -> 8001896748571570600ull [H-THIN] kMargin_reads[86]: 18177522707802576037ull -> 7347984521559791464ull [H-THIN] kMargin_reads[87]: 1635613291101838741ull -> 3770456150226756214ull [H-THIN] kMargin_reads[88]: 15997917115278936545ull -> 10283041979086499618ull [H-THIN] kMargin_reads[89]: 17554126746769759592ull -> 11525649179628871169ull [H-THIN] kMargin_reads[90]: 3806995878355289449ull -> 15518453503092621102ull [H-THIN] kMargin_reads[91]: 15405908675426402054ull -> 4288437931908199143ull [H-THIN] kMargin_reads[92]: 1441245154396427777ull -> 3161816617208773754ull [H-THIN] kMargin_reads[93]: 14397269933515551863ull -> 7892138492494137338ull [H-THIN] kMargin_reads[94]: 12219341892567703629ull -> 8517423913411631616ull [H-THIN] kMargin_reads[95]: 2752726173101561857ull -> 10266993678345749518ull [H-THIN] kMargin_final[0]: 10114708526056156918ull -> 15724008703225232301ull [H-THIN] test_adapter_quiet_bar: 272 lines Quiet/0/1: boundary 0x1c2b2e37a44f6877ull -> 0xcd8d3ecb4d2c740aull, rows 0x81799b58f0e052b1ull -> 0xe6da4342a82c460cull, final_hash 0xe0aa4c0240939bcfull -> 0x458f1ecf00ef1d5cull [H-THIN] Quiet/0/0: boundary 0x1c2b2e37a44f6877ull -> 0xcd8d3ecb4d2c740aull, final_hash 0xe0aa4c0240939bcfull -> 0x458f1ecf00ef1d5cull [H-THIN] Quiet/1/1: boundary 0x0a542471817c0616ull -> 0x2a102759d268d84dull, rows 0x4210b9ca5a7d77e6ull -> 0x923a578d640d7fdbull, final_hash 0x2e91db500a83cbe9ull -> 0x6ca064a309be6bfaull [H-THIN] Quiet/1/0: boundary 0x0a542471817c0616ull -> 0x2a102759d268d84dull, final_hash 0x2e91db500a83cbe9ull -> 0x6ca064a309be6bfaull [H-THIN] HoldLongFull/0/1: boundary 0xbb82b8cd85ffaae1ull -> 0x2be6111eb08aff44ull, rows 0xae96abef4356f11full -> 0x37135fdf24860586ull, final_hash 0xbe87239166611559ull -> 0x6caa6506b43c5228ull [H-THIN] HoldLongFull/0/0: boundary 0xbb82b8cd85ffaae1ull -> 0x2be6111eb08aff44ull, final_hash 0xbe87239166611559ull -> 0x6caa6506b43c5228ull [H-THIN] HoldLongFull/1/1: boundary 0x83f82e6a0b0a5677ull -> 0x6da42e25a0445e57ull, rows 0x7700c6fdca38234full -> 0xa90a9669249de5f2ull, final_hash 0xb6f097b6a6318731ull -> 0x1fdb374b13f0dd9cull [H-THIN] HoldLongFull/1/0: boundary 0x83f82e6a0b0a5677ull -> 0x6da42e25a0445e57ull, final_hash 0xb6f097b6a6318731ull -> 0x1fdb374b13f0dd9cull [H-THIN] MarginCalls/0/1: boundary 0x3633b5fd46e3ee1eull -> 0x9ffada7afff423a5ull, rows 0x35e47773037c0f4dull -> 0xf1451bf046aea2aeull, final_hash 0x64d4054219e25f2full -> 0xd58658d95c54acbeull [H-THIN] MarginCalls/0/0: boundary 0x3633b5fd46e3ee1eull -> 0x9ffada7afff423a5ull, final_hash 0x64d4054219e25f2full -> 0xd58658d95c54acbeull [H-THIN] MarginCalls/1/1: boundary 0x96ca6ef83164c685ull -> 0x548dd443b38f74b0ull, rows 0x4b1bc134021b04cbull -> 0xfdd852b37b07d408ull, final_hash 0xc10c1fc9e88e7c23ull -> 0x76f99b20ecd7e68cull [H-THIN] MarginCalls/1/0: boundary 0x96ca6ef83164c685ull -> 0x548dd443b38f74b0ull, final_hash 0xc10c1fc9e88e7c23ull -> 0x76f99b20ecd7e68cull [H-THIN] MarginCallCycles/0/1: boundary 0x38656d5e12929625ull -> 0x53630c222ae9b1a8ull, rows 0x5c4aa443e8e76ef9ull -> 0x26e80eef46f3b822ull, final_hash 0x55f80f5340e670b7ull -> 0x9dea3ea1d3cbc012ull [H-THIN] MarginCallCycles/0/0: boundary 0x38656d5e12929625ull -> 0x53630c222ae9b1a8ull, final_hash 0x55f80f5340e670b7ull -> 0x9dea3ea1d3cbc012ull [H-THIN] MarginCallCycles/1/1: boundary 0xc969906a8f7b57bbull -> 0xd66af1875ed2e747ull, rows 0x67e6e63eb71a661cull -> 0xffa2470471097d1dull, final_hash 0x8e9e930063b52698ull -> 0x22ec1fab2742fa75ull [H-THIN] MarginCallCycles/1/0: boundary 0xc969906a8f7b57bbull -> 0xd66af1875ed2e747ull, final_hash 0x8e9e930063b52698ull -> 0x22ec1fab2742fa75ull [H-THIN] Bracket/0/1: boundary 0x5ceffdf162a625baull -> 0xaeddc0d29a30f3f7ull, rows 0x2c0b9b25e51c3426ull -> 0xd324958e0a8a53deull, final_hash 0xb62be9f335a690fbull -> 0xef163ce5ad81ccceull [H-THIN] Bracket/0/0: boundary 0x5ceffdf162a625baull -> 0xaeddc0d29a30f3f7ull, final_hash 0xb62be9f335a690fbull -> 0xef163ce5ad81ccceull [H-THIN] Bracket/1/1: boundary 0x53093fd52be58e6aull -> 0xdb519033b2a6887bull, rows 0x26c6fc455e7a9e3full -> 0xb2b3be51c614016dull, final_hash 0x2fbb7d2d1228c959ull -> 0x488b102fb534087cull [H-THIN] Bracket/1/0: boundary 0x53093fd52be58e6aull -> 0xdb519033b2a6887bull, final_hash 0x2fbb7d2d1228c959ull -> 0x488b102fb534087cull [H-THIN] Trail/0/1: boundary 0xa09d959161ab83b5ull -> 0xa901a158196e8e90ull, rows 0x6db293f4d9ba407eull -> 0x3bf9ee1972d44776ull, final_hash 0xf9e526fb6a079950ull -> 0xf1d8827b0b6f8fcfull [H-THIN] Trail/0/0: boundary 0xa09d959161ab83b5ull -> 0xa901a158196e8e90ull, final_hash 0xf9e526fb6a079950ull -> 0xf1d8827b0b6f8fcfull [H-THIN] Trail/1/1: boundary 0x2a39cc8c834a5287ull -> 0xb23147b5951428e2ull, rows 0xe23d175bdbe6d254ull -> 0x4f409e92b7a0d98aull, final_hash 0xffac0df8a2dbc19eull -> 0x2716f35c358b7145ull [H-THIN] Trail/1/0: boundary 0x2a39cc8c834a5287ull -> 0xb23147b5951428e2ull, final_hash 0xffac0df8a2dbc19eull -> 0x2716f35c358b7145ull [H-THIN] DualStops/0/1: boundary 0xf53e1aa110098577ull -> 0x29701bba6d7182b5ull, rows 0xa45979a387e09947ull -> 0xf109235082c1860eull, final_hash 0x9d20a3a0e3c4b991ull -> 0x34452d46a85f96c6ull [H-THIN] DualStops/0/0: boundary 0xf53e1aa110098577ull -> 0x29701bba6d7182b5ull, final_hash 0x9d20a3a0e3c4b991ull -> 0x34452d46a85f96c6ull [H-THIN] DualStops/1/1: boundary 0xd56451041c2c2c4full -> 0x86f9654dc6f8ed22ull, rows 0x3525fdbfa98d36bcull -> 0x4393ede3dede0436ull, final_hash 0x0e2c1cdfb96b6301ull -> 0x1bfce4a56d0314f2ull [H-THIN] DualStops/1/0: boundary 0xd56451041c2c2c4full -> 0x86f9654dc6f8ed22ull, final_hash 0x0e2c1cdfb96b6301ull -> 0x1bfce4a56d0314f2ull [H-THIN] InvertedStops/0/1: boundary 0xa3daa491b43f22e9ull -> 0xeb8c3dfbd9d273a2ull, rows 0x054c4b8ef1709dc3ull -> 0xb7ecf902b2ff0befull, final_hash 0x056329caffb5a22eull -> 0x1993f824c35e154dull [H-THIN] InvertedStops/0/0: boundary 0xa3daa491b43f22e9ull -> 0xeb8c3dfbd9d273a2ull, final_hash 0x056329caffb5a22eull -> 0x1993f824c35e154dull [H-THIN] InvertedStops/1/1: boundary 0x9b50fac734e90657ull -> 0x976cbd72b16fcb6full, rows 0x2d65179a434eebf0ull -> 0xe49f3d6c9fe39f5bull, final_hash 0xab655e08a941e123ull -> 0x69e468caa11329ccull [H-THIN] InvertedStops/1/0: boundary 0x9b50fac734e90657ull -> 0x976cbd72b16fcb6full, final_hash 0xab655e08a941e123ull -> 0x69e468caa11329ccull [H-THIN] PendingParent/0/1: boundary 0x47212775031c61c2ull -> 0xc2b9247123313aceull, rows 0xfd5dbde93e8d520aull -> 0xa8f42cb39e1aeff3ull, final_hash 0xe74974df53ef097aull -> 0xfbf54246d7baccdaull [H-THIN] PendingParent/0/0: boundary 0x47212775031c61c2ull -> 0xc2b9247123313aceull, final_hash 0xe74974df53ef097aull -> 0xfbf54246d7baccdaull [H-THIN] PendingParent/1/1: boundary 0x08f4fa130f86a0daull -> 0xcb795828e625815full, rows 0xbd725b4c1d8be787ull -> 0x808c94697e2231bdull, final_hash 0x27cdc3dedee10a56ull -> 0x0f9d2bb1d6492d5aull [H-THIN] PendingParent/1/0: boundary 0x08f4fa130f86a0daull -> 0xcb795828e625815full, final_hash 0x27cdc3dedee10a56ull -> 0x0f9d2bb1d6492d5aull [H-THIN] GappedAdd/0/1: boundary 0x8bd71987fffc51e9ull -> 0x2aa05060cf893d51ull, rows 0x42f9dd5f352155d8ull -> 0xca15382fd8d71504ull, final_hash 0xaa98c069e5a019a7ull -> 0x53a5ed59712f1502ull [H-THIN] GappedAdd/0/0: boundary 0x8bd71987fffc51e9ull -> 0x2aa05060cf893d51ull, final_hash 0xaa98c069e5a019a7ull -> 0x53a5ed59712f1502ull [H-THIN] GappedAdd/1/1: boundary 0x7b93202bd79c1393ull -> 0x1de2b53c2e69ae04ull, rows 0xa4421baf09234dd7ull -> 0xb77b870b1ef756a3ull, final_hash 0xe086e35f4f128b46ull -> 0xc471e2ce0ce5b5b7ull [H-THIN] GappedAdd/1/0: boundary 0x7b93202bd79c1393ull -> 0x1de2b53c2e69ae04ull, final_hash 0xe086e35f4f128b46ull -> 0xc471e2ce0ce5b5b7ull [H-THIN] DeclinedReversal/0/1: boundary 0xfcb9145b39ec7b5aull -> 0x5dc519d25cd8e456ull, rows 0xbbddf0514f17aef7ull -> 0xcd770d65bdeb5e28ull, final_hash 0x764450acb0596bfcull -> 0xacfb010f8ef2c1dbull [H-THIN] DeclinedReversal/0/0: boundary 0xfcb9145b39ec7b5aull -> 0x5dc519d25cd8e456ull, final_hash 0x764450acb0596bfcull -> 0xacfb010f8ef2c1dbull [H-THIN] DeclinedReversal/1/1: boundary 0x4a1378eba012ef40ull -> 0x506c2023d62a4e6full, rows 0x16ea86de895d5b40ull -> 0xe0eb2dae405533f4ull, final_hash 0x14734f0e113371e4ull -> 0x980a839868ba5b97ull [H-THIN] DeclinedReversal/1/0: boundary 0x4a1378eba012ef40ull -> 0x506c2023d62a4e6full, final_hash 0x14734f0e113371e4ull -> 0x980a839868ba5b97ull [H-THIN] CalcOnFills/0/1: boundary 0x7641907936993779ull -> 0xbd9feba11a355c84ull, rows 0x3777fe6ece242880ull -> 0xbd62a54cd2c27075ull, final_hash 0x2583ce2bc6a3c1d9ull -> 0x8e716ca55979f5e4ull [PAR-ORDERS-2, H-THIN] CalcOnFills/0/0: boundary 0x7641907936993779ull -> 0xbd9feba11a355c84ull, final_hash 0x2583ce2bc6a3c1d9ull -> 0x8e716ca55979f5e4ull [PAR-ORDERS-2, H-THIN] CalcOnFills/1/1: boundary 0xd120e8fefb0dc236ull -> 0x0e2860d938b1d36dull, rows 0xdbb28de374d30bc3ull -> 0xfe826221acbd42c3ull, final_hash 0xad6e26cf69199431ull -> 0x1ae91d50d6762fd0ull [PAR-ORDERS-2, H-THIN] CalcOnFills/1/0: boundary 0xd120e8fefb0dc236ull -> 0x0e2860d938b1d36dull, final_hash 0xad6e26cf69199431ull -> 0x1ae91d50d6762fd0ull [PAR-ORDERS-2, H-THIN] CloseOrders/0/1: boundary 0x78ff3352f7085436ull -> 0xb27e9cfcbfee41d3ull, rows 0x896bcdcf15c8a4c6ull -> 0x0033c4a6a644f0ddull, final_hash 0x8e67de695bfb2c60ull -> 0xbedbc1183b7526a1ull [H-THIN] CloseOrders/0/0: boundary 0x78ff3352f7085436ull -> 0xb27e9cfcbfee41d3ull, final_hash 0x8e67de695bfb2c60ull -> 0xbedbc1183b7526a1ull [H-THIN] CloseOrders/1/1: boundary 0xe00ae8be3caf3315ull -> 0xff96b9591f72f309ull, rows 0x2584350c36f68309ull -> 0x5aaa3b2f562a501dull, final_hash 0xa998448999ff9b1eull -> 0xc542c7fb8bb5559dull [H-THIN] CloseOrders/1/0: boundary 0xe00ae8be3caf3315ull -> 0xff96b9591f72f309ull, final_hash 0xa998448999ff9b1eull -> 0xc542c7fb8bb5559dull [H-THIN] CloseOrdersSlipped/0/1: boundary 0x58e9160429c03bcaull -> 0xc2834ef9bd74b55full, rows 0xd3632e32e35ef626ull -> 0xb1042b50a92de9a0ull, final_hash 0x13f82da0279b5836ull -> 0x6c12422a13b3f077ull [H-THIN] CloseOrdersSlipped/0/0: boundary 0x58e9160429c03bcaull -> 0xc2834ef9bd74b55full, final_hash 0x13f82da0279b5836ull -> 0x6c12422a13b3f077ull [H-THIN] CloseOrdersSlipped/1/1: boundary 0x3507072a3e435b83ull -> 0x71906fcd647a169full, rows 0xc5d77a9b36fdec85ull -> 0xa2710dbb1c8a57edull, final_hash 0xc7b132391c547427ull -> 0x0330185649c756c2ull [H-THIN] CloseOrdersSlipped/1/0: boundary 0x3507072a3e435b83ull -> 0x71906fcd647a169full, final_hash 0xc7b132391c547427ull -> 0x0330185649c756c2ull [H-THIN] IntradayLoss/0/1: boundary 0x644e48072a025ba8ull -> 0x8a9bd5dd1429352eull, rows 0x0421711a90fdf429ull -> 0x616b96966292e8adull, final_hash 0xecca5625486ab2e1ull -> 0xc14ffe9c379fbee2ull [H-THIN] IntradayLoss/0/0: boundary 0x644e48072a025ba8ull -> 0x8a9bd5dd1429352eull, final_hash 0xecca5625486ab2e1ull -> 0xc14ffe9c379fbee2ull [H-THIN] IntradayLoss/1/1: boundary 0xce3032bd544d7840ull -> 0x1a92d1a03b233c6cull, rows 0x4e9d2d333bd5f470ull -> 0x3382648892a694c4ull, final_hash 0x4e071c66b57dfeaeull -> 0x66f4f2fe8b455019ull [H-THIN] IntradayLoss/1/0: boundary 0xce3032bd544d7840ull -> 0x1a92d1a03b233c6cull, final_hash 0x4e071c66b57dfeaeull -> 0x66f4f2fe8b455019ull [H-THIN] IntradayCapDeferred/0/1: boundary 0x93a814e5f6e8feb7ull -> 0xde19edf4087b930dull, rows 0xbbbb2712dbbd9ac8ull -> 0x560de539b138ea41ull, final_hash 0x77c142bb11eab624ull -> 0x7730e7fe703663edull [H-THIN] IntradayCapDeferred/0/0: boundary 0x93a814e5f6e8feb7ull -> 0xde19edf4087b930dull, final_hash 0x77c142bb11eab624ull -> 0x7730e7fe703663edull [H-THIN] IntradayCapDeferred/1/1: boundary 0xe23e69831f88d404ull -> 0x293bdcc491ae22d7ull, rows 0x63469b00b6747db8ull -> 0xd165d92830916a1cull, final_hash 0x72de8fef40c4c89bull -> 0x7ba4fe8defa15d4aull [H-THIN] IntradayCapDeferred/1/0: boundary 0xe23e69831f88d404ull -> 0x293bdcc491ae22d7ull, final_hash 0x72de8fef40c4c89bull -> 0x7ba4fe8defa15d4aull [H-THIN] PreopenSlice/0/1: boundary 0x9f401876dd941c1full -> 0xcbc8fb1ba9d83481ull, rows 0xbbce87ebf0d3f638ull -> 0xbe57d9275ae9e2bbull, final_hash 0xf73c6812e8dd0f92ull -> 0x62f97adf60eb373full [H-THIN] PreopenSlice/0/0: boundary 0x9f401876dd941c1full -> 0xcbc8fb1ba9d83481ull, final_hash 0xf73c6812e8dd0f92ull -> 0x62f97adf60eb373full [H-THIN] PreopenSlice/1/1: boundary 0x98345cf423b84f80ull -> 0xadcfe58d853cedfcull, rows 0x36db489e43fd8218ull -> 0xecc971145edced41ull, final_hash 0xa5916c1cffd7bae0ull -> 0x94fff9b38c7f1bd5ull [H-THIN] PreopenSlice/1/0: boundary 0x98345cf423b84f80ull -> 0xadcfe58d853cedfcull, final_hash 0xa5916c1cffd7bae0ull -> 0x94fff9b38c7f1bd5ull [H-THIN] SameBarBatch/0/1: boundary 0xcfa0ce2dfed4b2e4ull -> 0x85b57cf2c67fbdd6ull, rows 0x7de7b618b505f51eull -> 0xdf711eb77d489011ull, final_hash 0x0bb34499121582afull -> 0x77819f662ca36048ull [H-THIN] SameBarBatch/0/0: boundary 0xcfa0ce2dfed4b2e4ull -> 0x85b57cf2c67fbdd6ull, final_hash 0x0bb34499121582afull -> 0x77819f662ca36048ull [H-THIN] SameBarBatch/1/1: boundary 0x4557f9683348bf23ull -> 0x5d280dcf47a7af5bull, rows 0x79d43d1295ddc801ull -> 0x987f9a54f93d934eull, final_hash 0x26a5e7a6d5104dbcull -> 0x6a0ac704a36bef57ull [H-THIN] SameBarBatch/1/0: boundary 0x4557f9683348bf23ull -> 0x5d280dcf47a7af5bull, final_hash 0x26a5e7a6d5104dbcull -> 0x6a0ac704a36bef57ull [H-THIN] CommissionedShort/0/1: boundary 0x18b476bd2a3db96dull -> 0xc73e33b30bf09c29ull, rows 0x54b6ba2e7a2e11c2ull -> 0x26a3e6d04263ad27ull, final_hash 0x48a9193b2c1c1118ull -> 0xeb0d9ea9f0bdfd4bull [H-THIN] CommissionedShort/0/0: boundary 0x18b476bd2a3db96dull -> 0xc73e33b30bf09c29ull, final_hash 0x48a9193b2c1c1118ull -> 0xeb0d9ea9f0bdfd4bull [H-THIN] CommissionedShort/1/1: boundary 0xb24ff7e17f80c347ull -> 0x350b092698bdf8fdull, rows 0x20608b72a057ada4ull -> 0x3fe25a6001c6c4feull, final_hash 0x3ef1eaec89e6430dull -> 0xd73d8e053dbf5cd2ull [H-THIN] CommissionedShort/1/0: boundary 0xb24ff7e17f80c347ull -> 0x350b092698bdf8fdull, final_hash 0x3ef1eaec89e6430dull -> 0xd73d8e053dbf5cd2ull [H-THIN] AccountFx/0/1: boundary 0xaeec7af5859fe6b3ull -> 0xd27340484425ff52ull, rows 0x37039a16e8c77a7bull -> 0x4e596d61c552597eull, final_hash 0x2a28b4e61cd5cbbcull -> 0xea884b09a27ea73bull [H-THIN] AccountFx/0/0: boundary 0xaeec7af5859fe6b3ull -> 0xd27340484425ff52ull, final_hash 0x2a28b4e61cd5cbbcull -> 0xea884b09a27ea73bull [H-THIN] Stream/0/1: boundary 0x49574a22705f52c0ull -> 0x4f42bff80af50865ull, rows 0x3cade1c0a6b65b75ull -> 0xca31c77c5a63ae01ull, final_hash 0x8c9bb935bf0c32daull -> 0xeb75594695696b2full [H-THIN] Stream/0/0: boundary 0x49574a22705f52c0ull -> 0x4f42bff80af50865ull, final_hash 0x8c9bb935bf0c32daull -> 0xeb75594695696b2full [H-THIN] Random01/0/1: boundary 0xfa3864e9f5f3a619ull -> 0xe7fdfddc42a2ae75ull, rows 0x22da84303f34b121ull -> 0x0b38785d82bc8fe6ull, final_hash 0xa142013345fb25ddull -> 0x16fe00b5a88e8ff2ull [H-THIN] Random01/0/0: boundary 0xfa3864e9f5f3a619ull -> 0xe7fdfddc42a2ae75ull, final_hash 0xa142013345fb25ddull -> 0x16fe00b5a88e8ff2ull [H-THIN] Random01/1/1: boundary 0xfdafd368c22a3bb4ull -> 0x64e7087d99c04c46ull, rows 0x30ace005c5ea2e1bull -> 0x1c0e119f02f64ddeull, final_hash 0xb44a05ed46f5109dull -> 0xb0114372fd93a01cull [H-THIN] Random01/1/0: boundary 0xfdafd368c22a3bb4ull -> 0x64e7087d99c04c46ull, final_hash 0xb44a05ed46f5109dull -> 0xb0114372fd93a01cull [H-THIN] Random02/0/1: boundary 0xf933c58464603966ull -> 0x8a2b805556d22659ull, rows 0x8549ba7d7e735482ull -> 0xce086fbf3b8296e5ull, final_hash 0xd9870babe5d18626ull -> 0x5221fce916020c33ull [PAR-ORDERS-2, H-THIN] Random02/0/0: boundary 0xf933c58464603966ull -> 0x8a2b805556d22659ull, final_hash 0xd9870babe5d18626ull -> 0x5221fce916020c33ull [PAR-ORDERS-2, H-THIN] Random02/1/1: boundary 0x25ba5f1040df3b56ull -> 0x844819f3aaccd232ull, rows 0x1593a8e96a0c90a9ull -> 0x74fc5a6e6a256e36ull, final_hash 0x0fb50af38c25c54dull -> 0x089086fe02b53049ull [PAR-ORDERS-2, H-THIN] Random02/1/0: boundary 0x25ba5f1040df3b56ull -> 0x844819f3aaccd232ull, final_hash 0x0fb50af38c25c54dull -> 0x089086fe02b53049ull [PAR-ORDERS-2, H-THIN] Random03/0/1: boundary 0xb5a3f885ba0143fdull -> 0x2dad9d24e763c932ull, rows 0xd91b7712cc62209bull -> 0xa3f6fa59cf96ba52ull, final_hash 0xd93ef819aff693f3ull -> 0xeb202b23d902f8b8ull [H-THIN] Random03/0/0: boundary 0xb5a3f885ba0143fdull -> 0x2dad9d24e763c932ull, final_hash 0xd93ef819aff693f3ull -> 0xeb202b23d902f8b8ull [H-THIN] Random03/1/1: boundary 0x36230659e7bf570eull -> 0x7196371dde7bb130ull, rows 0xdb83075a5690e996ull -> 0xbdf7be966e72b156ull, final_hash 0x23acbc0041308125ull -> 0x92f95865ffb213eeull [H-THIN] Random03/1/0: boundary 0x36230659e7bf570eull -> 0x7196371dde7bb130ull, final_hash 0x23acbc0041308125ull -> 0x92f95865ffb213eeull [H-THIN] Random04/0/1: boundary 0x0c41be29e4497a7cull -> 0x9d74676f534d8b76ull, rows 0x1fdaabb2a1c312cdull -> 0xdb1c5eb0cb9f6c32ull, final_hash 0x542f1b01ef32d535ull -> 0x82a419ac37909af3ull [H-THIN] Random04/0/0: boundary 0x0c41be29e4497a7cull -> 0x9d74676f534d8b76ull, final_hash 0x542f1b01ef32d535ull -> 0x82a419ac37909af3ull [H-THIN] Random04/1/1: boundary 0x93ade030662e0b77ull -> 0x19cdc09e2267df5aull, rows 0x5beecc5dc910feb8ull -> 0x7eca887896ce161bull, final_hash 0x254f4f4933be69e0ull -> 0x350e10b2e19f0b44ull [H-THIN] Random04/1/0: boundary 0x93ade030662e0b77ull -> 0x19cdc09e2267df5aull, final_hash 0x254f4f4933be69e0ull -> 0x350e10b2e19f0b44ull [H-THIN] Random05/0/1: boundary 0x3e8e6495fe2e6dc1ull -> 0xac397a37be0d4789ull, rows 0x2703dbc59d3516a6ull -> 0x64b7bb57706476b6ull, final_hash 0xb72557b7f6894eafull -> 0x85274de8bea42d05ull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] Random05/0/0: boundary 0x3e8e6495fe2e6dc1ull -> 0xac397a37be0d4789ull, final_hash 0xb72557b7f6894eafull -> 0x85274de8bea42d05ull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] Random05/1/1: boundary 0x4be132773d14c0e0ull -> 0xbd96a4fdd0383edbull, rows 0xa6803dead66d3343ull -> 0xdcb784e78fba749eull, final_hash 0xb3561169b361d8bfull -> 0x919116731d265d3dull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] Random05/1/0: boundary 0x4be132773d14c0e0ull -> 0xbd96a4fdd0383edbull, final_hash 0xb3561169b361d8bfull -> 0x919116731d265d3dull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] Random06/0/1: boundary 0xecb44da2cbdb5c42ull -> 0x7deef79bd0217f64ull, rows 0x5485171eedfc86cfull -> 0xd166805cc595f38eull, final_hash 0xff2efe1925e45a9dull -> 0x4c7c821f90e38cccull [H-THIN, PAR-CASHFEE] Random06/0/0: boundary 0xecb44da2cbdb5c42ull -> 0x7deef79bd0217f64ull, final_hash 0xff2efe1925e45a9dull -> 0x4c7c821f90e38cccull [H-THIN, PAR-CASHFEE] Random06/1/1: boundary 0x65cc03efd59ec6b5ull -> 0x8a1bb21a5e64fed9ull, rows 0x2dce2153546130dbull -> 0x7d881d1a93512848ull, final_hash 0x1130940f86f108d2ull -> 0xaa8716ffbdfd11b2ull [H-THIN, PAR-CASHFEE] Random06/1/0: boundary 0x65cc03efd59ec6b5ull -> 0x8a1bb21a5e64fed9ull, final_hash 0x1130940f86f108d2ull -> 0xaa8716ffbdfd11b2ull [H-THIN, PAR-CASHFEE] Random07/0/1: boundary 0x7bf47df91573ab9cull -> 0xe6120913fe5da50dull, rows 0x3c1feb47a4d6d81eull -> 0xe90b62d77e4625b7ull, final_hash 0x438a828933e58c5bull -> 0xbc6f67df5b0f0b2full [H-THIN] Random07/0/0: boundary 0x7bf47df91573ab9cull -> 0xe6120913fe5da50dull, final_hash 0x438a828933e58c5bull -> 0xbc6f67df5b0f0b2full [H-THIN] Random07/1/1: boundary 0x29fcc776be13bdf5ull -> 0xe2c3492a3155bf7dull, rows 0x9a5ab1b2b00efff1ull -> 0x0e5f1aa6cabc3a32ull, final_hash 0x353c5f0e5bc1c22bull -> 0xbad1ae57e202b2d0ull [H-THIN] Random07/1/0: boundary 0x29fcc776be13bdf5ull -> 0xe2c3492a3155bf7dull, final_hash 0x353c5f0e5bc1c22bull -> 0xbad1ae57e202b2d0ull [H-THIN] Random08/0/1: boundary 0xc2f66d9e5f4943c7ull -> 0x0a3af182d33c479aull, rows 0xf4a38750150093e6ull -> 0xc91b237d10b90ecbull, final_hash 0x9ce7eb6ee2b122baull -> 0x1930dc38f4f605fbull [PAR-ORDERS-2, H-THIN] Random08/0/0: boundary 0xc2f66d9e5f4943c7ull -> 0x0a3af182d33c479aull, final_hash 0x9ce7eb6ee2b122baull -> 0x1930dc38f4f605fbull [PAR-ORDERS-2, H-THIN] Random08/1/1: boundary 0xbc7e1d017519ee12ull -> 0xc981400f75b65614ull, rows 0xc18ef29ec0f4fa8eull -> 0xb37d8f41ed349758ull, final_hash 0xbd83191789f76873ull -> 0xe48afc5e8a67e872ull [PAR-ORDERS-2, H-THIN] Random08/1/0: boundary 0xbc7e1d017519ee12ull -> 0xc981400f75b65614ull, final_hash 0xbd83191789f76873ull -> 0xe48afc5e8a67e872ull [PAR-ORDERS-2, H-THIN] Random09/0/1: boundary 0x400c46d70e6e84bcull -> 0xaaff8dcd840f16ceull, rows 0xc58cc1d4b5bd3580ull -> 0x9021538022cf9b9bull, final_hash 0x1f6cdd2e111e016aull -> 0xbbe3c07366b0cbeaull [H-THIN] Random09/0/0: boundary 0x400c46d70e6e84bcull -> 0xaaff8dcd840f16ceull, final_hash 0x1f6cdd2e111e016aull -> 0xbbe3c07366b0cbeaull [H-THIN] Random09/1/1: boundary 0x32d4b35f029bad56ull -> 0x026e829145cad6f5ull, rows 0x447742874dde46e0ull -> 0x32ca6bae685cea43ull, final_hash 0x3d99e7abdd92a773ull -> 0xc55c597c6508d557ull [H-THIN] Random09/1/0: boundary 0x32d4b35f029bad56ull -> 0x026e829145cad6f5ull, final_hash 0x3d99e7abdd92a773ull -> 0xc55c597c6508d557ull [H-THIN] Random10/0/1: boundary 0x998ee74ea73334c6ull -> 0x82933908492fe6aeull, rows 0x5a3104007433abc0ull -> 0x839a10fa6478c692ull, final_hash 0x0a7888fb9b890fb4ull -> 0x6907e89e2f502bd7ull [PAR-ORDERS-2, H-THIN] Random10/0/0: boundary 0x998ee74ea73334c6ull -> 0x82933908492fe6aeull, final_hash 0x0a7888fb9b890fb4ull -> 0x6907e89e2f502bd7ull [PAR-ORDERS-2, H-THIN] Random10/1/1: boundary 0xde7dbd52ac33a7ffull -> 0x3af283057bf553afull, rows 0xaf8f0ca763b06949ull -> 0xd948d5022e691361ull, final_hash 0xfbe70c357ac296cfull -> 0x794b361143554e09ull [PAR-ORDERS-2, H-THIN] Random10/1/0: boundary 0xde7dbd52ac33a7ffull -> 0x3af283057bf553afull, final_hash 0xfbe70c357ac296cfull -> 0x794b361143554e09ull [PAR-ORDERS-2, H-THIN] Random11/0/1: boundary 0x805038d1cb0a3848ull -> 0x9f99f3ad7d35ba23ull, rows 0xebc3fde6db4a37f3ull -> 0x29a35fd6705cc48aull, final_hash 0x27bc15c5c142c728ull -> 0x8235cad2a6dfb3f3ull [H-THIN] Random11/0/0: boundary 0x805038d1cb0a3848ull -> 0x9f99f3ad7d35ba23ull, final_hash 0x27bc15c5c142c728ull -> 0x8235cad2a6dfb3f3ull [H-THIN] Random11/1/1: boundary 0x4a739854a58a3db9ull -> 0xf6fb4588ed987e13ull, rows 0xc91f1e4519586a06ull -> 0x7dcd8b5314fb7562ull, final_hash 0x8af717fd3fa04097ull -> 0xb44ac10707d01de6ull [H-THIN] Random11/1/0: boundary 0x4a739854a58a3db9ull -> 0xf6fb4588ed987e13ull, final_hash 0x8af717fd3fa04097ull -> 0xb44ac10707d01de6ull [H-THIN] Random12/0/1: boundary 0xb9a9bb3fafab7349ull -> 0xc02247ff66b0dc87ull, rows 0xe93fb7b821f6a7feull -> 0xf39477bfa32bb689ull, final_hash 0x29c94b5df165789aull -> 0x5f0b7200f4ecdfe1ull [PAR-ORDERS-2, H-THIN] Random12/0/0: boundary 0xb9a9bb3fafab7349ull -> 0xc02247ff66b0dc87ull, final_hash 0x29c94b5df165789aull -> 0x5f0b7200f4ecdfe1ull [PAR-ORDERS-2, H-THIN] Random12/1/1: boundary 0x298ab491e3caa61full -> 0xfcd62d0327e0553aull, rows 0xf8e78400f1c148acull -> 0xf36949e9fad408efull, final_hash 0x927c4394206b42b2ull -> 0x84a69af72aae96d9ull [PAR-ORDERS-2, H-THIN] Random12/1/0: boundary 0x298ab491e3caa61full -> 0xfcd62d0327e0553aull, final_hash 0x927c4394206b42b2ull -> 0x84a69af72aae96d9ull [PAR-ORDERS-2, H-THIN] Random13/0/1: boundary 0x39caae56088c1db8ull -> 0x53be6a77e760fe0bull, rows 0x4028b545fb2c3ad9ull -> 0xe1b3dac90adb9f4bull, final_hash 0xc19e5f5bbefe0e7full -> 0xafd98319ef581f61ull [H-THIN] Random13/0/0: boundary 0x39caae56088c1db8ull -> 0x53be6a77e760fe0bull, final_hash 0xc19e5f5bbefe0e7full -> 0xafd98319ef581f61ull [H-THIN] Random13/1/1: boundary 0x07b714c492b5ae85ull -> 0xd238668589210027ull, rows 0xa03b3f8bed95186dull -> 0xc4b7dac2b95e01fbull, final_hash 0xa9f1d1a463925b52ull -> 0xb4b9d6f51a69342full [H-THIN] Random13/1/0: boundary 0x07b714c492b5ae85ull -> 0xd238668589210027ull, final_hash 0xa9f1d1a463925b52ull -> 0xb4b9d6f51a69342full [H-THIN] Random14/0/1: boundary 0x21085d70db4c336bull -> 0xd37abe66ab158e44ull, rows 0x394ebfbc2c15522aull -> 0x38cb950f5ceb9d9cull, final_hash 0xad1f71b1afdd37f8ull -> 0x9d488869e9756d89ull [H-THIN] Random14/0/0: boundary 0x21085d70db4c336bull -> 0xd37abe66ab158e44ull, final_hash 0xad1f71b1afdd37f8ull -> 0x9d488869e9756d89ull [H-THIN] Random14/1/1: boundary 0x5a8dbff598244e8cull -> 0xb8392469a2a0abfeull, rows 0x4c48857b5cd25816ull -> 0xfed8161637d6c938ull, final_hash 0x7f45221fb1111aa3ull -> 0x04b4096b966c585cull [H-THIN] Random14/1/0: boundary 0x5a8dbff598244e8cull -> 0xb8392469a2a0abfeull, final_hash 0x7f45221fb1111aa3ull -> 0x04b4096b966c585cull [H-THIN] Random15/0/1: boundary 0x1fe125a3fb2912dfull -> 0x1fc67d9462b82276ull, rows 0x9ef69d4200bfb809ull -> 0x167381a97aa1c870ull, final_hash 0xf8da94854e79d89eull -> 0x69f92f0d300a126eull [H-THIN] Random15/0/0: boundary 0x1fe125a3fb2912dfull -> 0x1fc67d9462b82276ull, final_hash 0xf8da94854e79d89eull -> 0x69f92f0d300a126eull [H-THIN] Random15/1/1: boundary 0x7b17bf5a43044023ull -> 0xbbbd08da5449bc19ull, rows 0xdeec1e0290d1370dull -> 0x2a9a6d807f0676baull, final_hash 0x01758143c50ac69dull -> 0xe8dd6d8df584c6d6ull [H-THIN] Random15/1/0: boundary 0x7b17bf5a43044023ull -> 0xbbbd08da5449bc19ull, final_hash 0x01758143c50ac69dull -> 0xe8dd6d8df584c6d6ull [H-THIN] Random16/0/1: boundary 0xde7f3e61caa50135ull -> 0xe8851a2e0e645003ull, rows 0x9593cb63a9a06f14ull -> 0xcfab7fb0e8c24b17ull, final_hash 0xc99a0f083d0b2507ull -> 0x5639dcc11e103f25ull [H-THIN] Random16/0/0: boundary 0xde7f3e61caa50135ull -> 0xe8851a2e0e645003ull, final_hash 0xc99a0f083d0b2507ull -> 0x5639dcc11e103f25ull [H-THIN] Random16/1/1: boundary 0xffbc34c7947c611bull -> 0x431440275998e000ull, rows 0xa2b9ee52a1338391ull -> 0x90d12ea4ed48dd05ull, final_hash 0x1f80513beefa0b1aull -> 0xf7e82ef73f56a2eeull [H-THIN] Random16/1/0: boundary 0xffbc34c7947c611bull -> 0x431440275998e000ull, final_hash 0x1f80513beefa0b1aull -> 0xf7e82ef73f56a2eeull [H-THIN] Random17/0/1: boundary 0x15bab2d913fb1d81ull -> 0xf4b303a1ef4696f7ull, rows 0x0a3d587bbe912dcfull -> 0x6417861b10d32340ull, final_hash 0x7185bfaebb406df5ull -> 0xa61332be53034674ull [PAR-ORDERS-2, H-THIN] Random17/0/0: boundary 0x15bab2d913fb1d81ull -> 0xf4b303a1ef4696f7ull, final_hash 0x7185bfaebb406df5ull -> 0xa61332be53034674ull [PAR-ORDERS-2, H-THIN] Random17/1/1: boundary 0xa978788efbcc1631ull -> 0x4fbe2d70efbe24eaull, rows 0xbcbe24594a8acfcaull -> 0xaf770da711891f5aull, final_hash 0x285eaf39ec559587ull -> 0x6665febf6fb7f354ull [PAR-ORDERS-2, H-THIN] Random17/1/0: boundary 0xa978788efbcc1631ull -> 0x4fbe2d70efbe24eaull, final_hash 0x285eaf39ec559587ull -> 0x6665febf6fb7f354ull [PAR-ORDERS-2, H-THIN] Random18/0/1: boundary 0x90ed08a2949b1fb6ull -> 0xf434360cb53f77baull, rows 0x665cda1c725bc1b2ull -> 0xff5d4060a3fafb03ull, final_hash 0xe16bbfbfa58dbeb1ull -> 0xe0f82ba48054cc9cull [PAR-ORDERS-2, H-THIN] Random18/0/0: boundary 0x90ed08a2949b1fb6ull -> 0xf434360cb53f77baull, final_hash 0xe16bbfbfa58dbeb1ull -> 0xe0f82ba48054cc9cull [PAR-ORDERS-2, H-THIN] Random18/1/1: boundary 0xbf864c04890cf915ull -> 0x7b6ad000002b1959ull, rows 0x807a82ee057fbb5dull -> 0x70472dcb4abe021full, final_hash 0x5c359872c6618dd3ull -> 0x73a5afc061a888aaull [PAR-ORDERS-2, H-THIN] Random18/1/0: boundary 0xbf864c04890cf915ull -> 0x7b6ad000002b1959ull, final_hash 0x5c359872c6618dd3ull -> 0x73a5afc061a888aaull [PAR-ORDERS-2, H-THIN] Random19/0/1: boundary 0x3c5cc975f928b775ull -> 0xa21f7b0b62d72170ull, rows 0xea691a951590ece8ull -> 0xda14d42130450ee8ull, final_hash 0x17d53da80c3a895full -> 0x0bcf8b1fde6775f8ull [H-THIN] Random19/0/0: boundary 0x3c5cc975f928b775ull -> 0xa21f7b0b62d72170ull, final_hash 0x17d53da80c3a895full -> 0x0bcf8b1fde6775f8ull [H-THIN] Random19/1/1: boundary 0x92536ad17b9fbf26ull -> 0xceae18544190aae2ull, rows 0xabc10759f5e1ffa1ull -> 0x7d8104e455ec01f8ull, final_hash 0x168791086f51c368ull -> 0x426c72d5bce82609ull [H-THIN] Random19/1/0: boundary 0x92536ad17b9fbf26ull -> 0xceae18544190aae2ull, final_hash 0x168791086f51c368ull -> 0x426c72d5bce82609ull [H-THIN] Random20/0/1: boundary 0x90b20d166e1357e0ull -> 0xad83f25bfdab1b50ull, rows 0x808745030887aa13ull -> 0x103b25042e1313deull, final_hash 0x3968c24c22199036ull -> 0xb27d00f43fad75a1ull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] Random20/0/0: boundary 0x90b20d166e1357e0ull -> 0xad83f25bfdab1b50ull, final_hash 0x3968c24c22199036ull -> 0xb27d00f43fad75a1ull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] Random20/1/1: boundary 0xa82d4e53d2192a5full -> 0x4b8bcf5ef1f015c5ull, rows 0x8e1411c055e37af3ull -> 0x6e2d790aa8a7762bull, final_hash 0x5c29f15f02417fcaull -> 0xd3e60d6fda04adbeull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] Random20/1/0: boundary 0xa82d4e53d2192a5full -> 0x4b8bcf5ef1f015c5ull, final_hash 0x5c29f15f02417fcaull -> 0xd3e60d6fda04adbeull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] Random21/0/1: boundary 0x18e96beb584671fcull -> 0xd7b52779988b5cfaull, rows 0x6c5b4e5a67209f94ull -> 0xcfddcd52fb629169ull, final_hash 0xca719be0f46f42acull -> 0xfd9b7e4dad3197b5ull [H-THIN] Random21/0/0: boundary 0x18e96beb584671fcull -> 0xd7b52779988b5cfaull, final_hash 0xca719be0f46f42acull -> 0xfd9b7e4dad3197b5ull [H-THIN] Random21/1/1: boundary 0xd32a038cc5b6ea6eull -> 0x1c515b9e6cd109a3ull, rows 0x9074957bf890f8e6ull -> 0xaa94d0f54845ee02ull, final_hash 0xb6387745e72b0075ull -> 0x29fa4e20de356d85ull [H-THIN] Random21/1/0: boundary 0xd32a038cc5b6ea6eull -> 0x1c515b9e6cd109a3ull, final_hash 0xb6387745e72b0075ull -> 0x29fa4e20de356d85ull [H-THIN] Random22/0/1: boundary 0xef05a2f01ca0c05cull -> 0x79adc16250cf8292ull, rows 0xb151ebfe9a44ebebull -> 0x2512a0afd913906aull, final_hash 0x1b1b4c42bccc2191ull -> 0x1d49ae94182c6dc3ull [PAR-ORDERS-2, H-THIN] Random22/0/0: boundary 0xef05a2f01ca0c05cull -> 0x79adc16250cf8292ull, final_hash 0x1b1b4c42bccc2191ull -> 0x1d49ae94182c6dc3ull [PAR-ORDERS-2, H-THIN] Random22/1/1: boundary 0x1624d3651edc9089ull -> 0x301434ca9762fba0ull, rows 0x2a2f5457686fdb7full -> 0x9371e473c135020full, final_hash 0xb361721c9b7f57e7ull -> 0xaadd4e3cb0a37251ull [PAR-ORDERS-2, H-THIN] Random22/1/0: boundary 0x1624d3651edc9089ull -> 0x301434ca9762fba0ull, final_hash 0xb361721c9b7f57e7ull -> 0xaadd4e3cb0a37251ull [PAR-ORDERS-2, H-THIN] Random23/0/1: boundary 0xc50cf90af093d6e9ull -> 0xbf93319ca8e8ed61ull, rows 0x629f1e11fd65d1cdull -> 0x178099cac4dd9abfull, final_hash 0x9f14134203a5c6e9ull -> 0x160121fb8a6ad628ull [PAR-ORDERS-2, H-THIN] Random23/0/0: boundary 0xc50cf90af093d6e9ull -> 0xbf93319ca8e8ed61ull, final_hash 0x9f14134203a5c6e9ull -> 0x160121fb8a6ad628ull [PAR-ORDERS-2, H-THIN] Random23/1/1: boundary 0x61be7e60a493c3efull -> 0x03336650fccfc186ull, rows 0x7182d347041d20e2ull -> 0x84fb2988132b6bdbull, final_hash 0xcac3f08a3f6d7067ull -> 0x54a3c7db6bc08316ull [PAR-ORDERS-2, H-THIN] Random23/1/0: boundary 0x61be7e60a493c3efull -> 0x03336650fccfc186ull, final_hash 0xcac3f08a3f6d7067ull -> 0x54a3c7db6bc08316ull [PAR-ORDERS-2, H-THIN] Random24/0/1: boundary 0xadbbd26ef9c19430ull -> 0x21714f0414285779ull, rows 0x7cea484a04d1bc6dull -> 0x8e07ee97e3c18592ull, final_hash 0x880c60f0ef5d5a7dull -> 0x9b4982cc0c9d5836ull [H-THIN] Random24/0/0: boundary 0xadbbd26ef9c19430ull -> 0x21714f0414285779ull, final_hash 0x880c60f0ef5d5a7dull -> 0x9b4982cc0c9d5836ull [H-THIN] Random24/1/1: boundary 0x91d34f247834f4e9ull -> 0x104927385caf1bafull, rows 0xf9fa951efcc4b1caull -> 0xb58d2d29b9934e0cull, final_hash 0xc06e2e1df8db5231ull -> 0x892eef5960b6186eull [H-THIN] Random24/1/0: boundary 0x91d34f247834f4e9ull -> 0x104927385caf1bafull, final_hash 0xc06e2e1df8db5231ull -> 0x892eef5960b6186eull [H-THIN] Random25/0/1: boundary 0x1091baf336b7e96aull -> 0x7fe75851d29b5f6aull, rows 0x5836120ce0f3d783ull -> 0xbd64625a7bcfb73cull, final_hash 0x3f0d68bfc74cf35full -> 0xdbdc12e31962344aull [H-THIN] Random25/0/0: boundary 0x1091baf336b7e96aull -> 0x7fe75851d29b5f6aull, final_hash 0x3f0d68bfc74cf35full -> 0xdbdc12e31962344aull [H-THIN] Random25/1/1: boundary 0x4c59eab8929878abull -> 0x49f7049727341b3cull, rows 0x90bf03c0ed26cd99ull -> 0x4023f4c7efa8cdacull, final_hash 0xeb9d9c36364972dfull -> 0x1e3a08e1516ae4beull [H-THIN] Random25/1/0: boundary 0x4c59eab8929878abull -> 0x49f7049727341b3cull, final_hash 0xeb9d9c36364972dfull -> 0x1e3a08e1516ae4beull [H-THIN] Random26/0/1: boundary 0x406a1ebe98ed8ee5ull -> 0xca8049a8b96e9724ull, rows 0xc4ceaa2f46b3b1c7ull -> 0xc7e39193f4b07aa7ull, final_hash 0x6d9f1512cb3c63efull -> 0xf4e31e8175400963ull [H-THIN] Random26/0/0: boundary 0x406a1ebe98ed8ee5ull -> 0xca8049a8b96e9724ull, final_hash 0x6d9f1512cb3c63efull -> 0xf4e31e8175400963ull [H-THIN] Random26/1/1: boundary 0x28ce140c5a29687dull -> 0x9c9ac48945130531ull, rows 0x383f34833e879b1bull -> 0xeb4d985eb31f93e0ull, final_hash 0x500ef2279c277ba0ull -> 0xecd913dc1c2bb6edull [H-THIN] Random26/1/0: boundary 0x28ce140c5a29687dull -> 0x9c9ac48945130531ull, final_hash 0x500ef2279c277ba0ull -> 0xecd913dc1c2bb6edull [H-THIN] Random27/0/1: boundary 0xeafcaf72c5794d88ull -> 0xa5ad84fe83acddfcull, rows 0xfbb77c6f11f0944full -> 0x4da695a0a63ae358ull, final_hash 0xf2d844781c8e9819ull -> 0x70f7b127c522e392ull [H-THIN] Random27/0/0: boundary 0xeafcaf72c5794d88ull -> 0xa5ad84fe83acddfcull, final_hash 0xf2d844781c8e9819ull -> 0x70f7b127c522e392ull [H-THIN] Random27/1/1: boundary 0x433fe5d0340138d8ull -> 0x67f5b41cf936747aull, rows 0x80e409be19d6131dull -> 0x06665f5393126836ull, final_hash 0x2a459b69d95400b1ull -> 0x94de1904cefde26dull [H-THIN] Random27/1/0: boundary 0x433fe5d0340138d8ull -> 0x67f5b41cf936747aull, final_hash 0x2a459b69d95400b1ull -> 0x94de1904cefde26dull [H-THIN] Random28/0/1: boundary 0xa0bb8c1925814fe3ull -> 0x8b9cc3d4febe4c62ull, rows 0x8e8763454e7d2dcaull -> 0x69471bc0f4097ac1ull, final_hash 0xe51c2406011cd87dull -> 0x373d1983261f87caull [H-THIN] Random28/0/0: boundary 0xa0bb8c1925814fe3ull -> 0x8b9cc3d4febe4c62ull, final_hash 0xe51c2406011cd87dull -> 0x373d1983261f87caull [H-THIN] Random28/1/1: boundary 0xdb2802eb6d8819c7ull -> 0x57f3120e39e241ddull, rows 0x910e834a102ac932ull -> 0x0a99bf96869493a4ull, final_hash 0x7406a539e21e57ceull -> 0xe2efc2ce4ee88ff9ull [H-THIN] Random28/1/0: boundary 0xdb2802eb6d8819c7ull -> 0x57f3120e39e241ddull, final_hash 0x7406a539e21e57ceull -> 0xe2efc2ce4ee88ff9ull [H-THIN] Random29/0/1: boundary 0x2bbdb428b4a78935ull -> 0x89b631d5f5f01691ull, rows 0x6c7b12c7d5aa45e4ull -> 0x167525b5e459f6a9ull, final_hash 0xfd3d2fb6be6132d7ull -> 0xb42cb126538c6f0aull [H-THIN] Random29/0/0: boundary 0x2bbdb428b4a78935ull -> 0x89b631d5f5f01691ull, final_hash 0xfd3d2fb6be6132d7ull -> 0xb42cb126538c6f0aull [H-THIN] Random29/1/1: boundary 0x2bc2551fd2128e20ull -> 0x41cca15aeaf4b30dull, rows 0xb9728dfab890bfd1ull -> 0x5c0fe05b50ac7989ull, final_hash 0x082924a3760602daull -> 0xdaa6c53d3410e42full [H-THIN] Random29/1/0: boundary 0x2bc2551fd2128e20ull -> 0x41cca15aeaf4b30dull, final_hash 0x082924a3760602daull -> 0xdaa6c53d3410e42full [H-THIN] Random30/0/1: boundary 0x585a1a9f34476a29ull -> 0xe74142e41965c7f8ull, rows 0x5d09652b7dc7603full -> 0x7d6bfad3ec602d1eull, final_hash 0x8e3b3ec00a1454a2ull -> 0x2ff813254130dde5ull [H-THIN] Random30/0/0: boundary 0x585a1a9f34476a29ull -> 0xe74142e41965c7f8ull, final_hash 0x8e3b3ec00a1454a2ull -> 0x2ff813254130dde5ull [H-THIN] Random30/1/1: boundary 0x8de0587cbe97fc0aull -> 0xffbc3c35359e842dull, rows 0x2fc1ebe82b1783eeull -> 0xa978037a0a737ed4ull, final_hash 0x7ffde30dc93fc9d7ull -> 0xb713984b2b953de4ull [H-THIN] Random30/1/0: boundary 0x8de0587cbe97fc0aull -> 0xffbc3c35359e842dull, final_hash 0x7ffde30dc93fc9d7ull -> 0xb713984b2b953de4ull [H-THIN] Random31/0/1: boundary 0x09b545a0067e8c26ull -> 0xc1d0985632aa85e5ull, rows 0x3c6ae0150bb63629ull -> 0xb1ff4f1ff8ce7ff6ull, final_hash 0xfaf6a143c8b0d038ull -> 0x0138efa3affc4452ull [H-THIN] Random31/0/0: boundary 0x09b545a0067e8c26ull -> 0xc1d0985632aa85e5ull, final_hash 0xfaf6a143c8b0d038ull -> 0x0138efa3affc4452ull [H-THIN] Random31/1/1: boundary 0xb42bd9cfbe060a73ull -> 0x29c04e4a3f62fc66ull, rows 0x928f7cfde5bdb105ull -> 0xd37bee2bcbd5b472ull, final_hash 0xf602f077ecb2b7cfull -> 0x3e283f49a532b649ull [H-THIN] Random31/1/0: boundary 0xb42bd9cfbe060a73ull -> 0x29c04e4a3f62fc66ull, final_hash 0xf602f077ecb2b7cfull -> 0x3e283f49a532b649ull [H-THIN] Random32/0/1: boundary 0x220cd02053dbc40aull -> 0x69fb37996a1a6d8aull, rows 0x9afe2ada13350695ull -> 0x4eda1ad97257444eull, final_hash 0xe577534dd3033b52ull -> 0xf8f4a46581375bc8ull [H-THIN] Random32/0/0: boundary 0x220cd02053dbc40aull -> 0x69fb37996a1a6d8aull, final_hash 0xe577534dd3033b52ull -> 0xf8f4a46581375bc8ull [H-THIN] Random32/1/1: boundary 0xc0ca43094334f008ull -> 0x6aa8d3b227f15c4cull, rows 0xe85eb4136b8c435dull -> 0xefb2e786712329f2ull, final_hash 0xba233c9fc46444a4ull -> 0xe3b7a40e1641948dull [H-THIN] Random32/1/0: boundary 0xc0ca43094334f008ull -> 0x6aa8d3b227f15c4cull, final_hash 0xba233c9fc46444a4ull -> 0xe3b7a40e1641948dull [H-THIN] Random33/0/1: boundary 0xbfcfebe96632bd39ull -> 0x9651f8f876e48213ull, rows 0x116bc2f57887ba4bull -> 0x70cd8d3b24d08640ull, final_hash 0xf8a0e855a842d39cull -> 0x495394a49a7d3245ull [H-THIN] Random33/0/0: boundary 0xbfcfebe96632bd39ull -> 0x9651f8f876e48213ull, final_hash 0xf8a0e855a842d39cull -> 0x495394a49a7d3245ull [H-THIN] Random33/1/1: boundary 0xe17f0353f37c41f1ull -> 0x23686cd9a3fe0637ull, rows 0x0ba98d74e8b769b7ull -> 0x17ecf363cd313d8aull, final_hash 0xb76d3ca699ea4ebdull -> 0xcefd187486fda5beull [H-THIN] Random33/1/0: boundary 0xe17f0353f37c41f1ull -> 0x23686cd9a3fe0637ull, final_hash 0xb76d3ca699ea4ebdull -> 0xcefd187486fda5beull [H-THIN] Random34/0/1: boundary 0xea1b68237d95805full -> 0xb0d26d6ec62ee0b2ull, rows 0xb29d9ea005d9c282ull -> 0xe24b790bed6d8209ull, final_hash 0xb03d41aea9c00c63ull -> 0xa8799fc8644a8be8ull [H-THIN] Random34/0/0: boundary 0xea1b68237d95805full -> 0xb0d26d6ec62ee0b2ull, final_hash 0xb03d41aea9c00c63ull -> 0xa8799fc8644a8be8ull [H-THIN] Random34/1/1: boundary 0x4ecacf8ab2d85c47ull -> 0x40ad548ed9e2d639ull, rows 0x294fe821fef3729dull -> 0x2d022bb216743a64ull, final_hash 0xe4e38b6ada294505ull -> 0x0a4527b569ac8a10ull [H-THIN] Random34/1/0: boundary 0x4ecacf8ab2d85c47ull -> 0x40ad548ed9e2d639ull, final_hash 0xe4e38b6ada294505ull -> 0x0a4527b569ac8a10ull [H-THIN] Random35/0/1: boundary 0xeb16fb1a6bc80c17ull -> 0xbdcab3fd94434657ull, rows 0x7265444996669f20ull -> 0x8f07aebde3f01772ull, final_hash 0xb971c6bc6d7ac182ull -> 0x4d118037ee1626adull [H-THIN, PAR-CASHFEE] Random35/0/0: boundary 0xeb16fb1a6bc80c17ull -> 0xbdcab3fd94434657ull, final_hash 0xb971c6bc6d7ac182ull -> 0x4d118037ee1626adull [H-THIN, PAR-CASHFEE] Random35/1/1: boundary 0x193ec2cb9a163900ull -> 0x9f158f1f03765f97ull, rows 0x9233c78214742aa3ull -> 0x7ea6860a76f2a755ull, final_hash 0xa9bdcf22e28567b2ull -> 0x07820fe3390f4450ull [H-THIN, PAR-CASHFEE] Random35/1/0: boundary 0x193ec2cb9a163900ull -> 0x9f158f1f03765f97ull, final_hash 0xa9bdcf22e28567b2ull -> 0x07820fe3390f4450ull [H-THIN, PAR-CASHFEE] Random36/0/1: boundary 0xd4b5588948d4b755ull -> 0xc06367cbde7ecc9aull, rows 0x6af2dc7dd208ef5dull -> 0x9c13280fd6c4a76full, final_hash 0x1980e25b047dea78ull -> 0x4a73a58e1bf4c419ull [PAR-ORDERS-2, H-THIN] Random36/0/0: boundary 0xd4b5588948d4b755ull -> 0xc06367cbde7ecc9aull, final_hash 0x1980e25b047dea78ull -> 0x4a73a58e1bf4c419ull [PAR-ORDERS-2, H-THIN] Random36/1/1: boundary 0xd1532dd9d52ae5f4ull -> 0xf78394d86184426aull, rows 0x0e1d68d8d6c4c5e2ull -> 0xd52d837b120d0443ull, final_hash 0xe93d9e6c724efef6ull -> 0x1e90f705be3da39full [PAR-ORDERS-2, H-THIN] Random36/1/0: boundary 0xd1532dd9d52ae5f4ull -> 0xf78394d86184426aull, final_hash 0xe93d9e6c724efef6ull -> 0x1e90f705be3da39full [PAR-ORDERS-2, H-THIN] Random37/0/1: boundary 0x3a89ff92c403a3e7ull -> 0x8af4f686e8afd249ull, rows 0xddda8d77310d918full -> 0xb43a05dbbc636b7aull, final_hash 0xff9c74898bf3a72aull -> 0x8692adfc1cddb1c4ull [PAR-ORDERS-2, H-THIN] Random37/0/0: boundary 0x3a89ff92c403a3e7ull -> 0x8af4f686e8afd249ull, final_hash 0xff9c74898bf3a72aull -> 0x8692adfc1cddb1c4ull [PAR-ORDERS-2, H-THIN] Random37/1/1: boundary 0x8a36a09ee42999ecull -> 0xe82a3b0bd1861474ull, rows 0xf12dd4800e96f492ull -> 0x5ea014024a78fb97ull, final_hash 0x9f14bee9808ee8baull -> 0x1ace37e580b56141ull [PAR-ORDERS-2, H-THIN] Random37/1/0: boundary 0x8a36a09ee42999ecull -> 0xe82a3b0bd1861474ull, final_hash 0x9f14bee9808ee8baull -> 0x1ace37e580b56141ull [PAR-ORDERS-2, H-THIN] Random38/0/1: boundary 0x36ed3eee0da888c2ull -> 0xeaf05ca1aedc03d5ull, rows 0x2bf58dbe1c5bd9acull -> 0x80971876a1156ee8ull, final_hash 0x5b82e26291364fc0ull -> 0x2d7719753c10f4bbull [H-THIN, PAR-CASHFEE] Random38/0/0: boundary 0x36ed3eee0da888c2ull -> 0xeaf05ca1aedc03d5ull, final_hash 0x5b82e26291364fc0ull -> 0x2d7719753c10f4bbull [H-THIN, PAR-CASHFEE] Random38/1/1: boundary 0xa3225feaf19b98feull -> 0x8285a035265d771cull, rows 0x59c34402fcf68e2aull -> 0x0d3b927724077789ull, final_hash 0x135197c1ef34cbbdull -> 0x09336a0556a2fbb7ull [H-THIN, PAR-CASHFEE] Random38/1/0: boundary 0xa3225feaf19b98feull -> 0x8285a035265d771cull, final_hash 0x135197c1ef34cbbdull -> 0x09336a0556a2fbb7ull [H-THIN, PAR-CASHFEE] Random39/0/1: boundary 0x9bcdaebb98702055ull -> 0xc989defa2d8cff00ull, rows 0xf4194fbb3f8e63f4ull -> 0x31db41d49a36aaa6ull, final_hash 0x3026f01eb73dfcdaull -> 0xb8c88da9531702d9ull [H-THIN] Random39/0/0: boundary 0x9bcdaebb98702055ull -> 0xc989defa2d8cff00ull, final_hash 0x3026f01eb73dfcdaull -> 0xb8c88da9531702d9ull [H-THIN] Random39/1/1: boundary 0x5fa98522adc27aafull -> 0x588cc94ab848f7e6ull, rows 0x50b22f98ba965993ull -> 0x3b1478373127afc0ull, final_hash 0x123f0c5b09aeae91ull -> 0xbdab759ad2c5865cull [H-THIN] Random39/1/0: boundary 0x5fa98522adc27aafull -> 0x588cc94ab848f7e6ull, final_hash 0x123f0c5b09aeae91ull -> 0xbdab759ad2c5865cull [H-THIN] Random40/0/1: boundary 0x869959795f89f7afull -> 0xc4f976129a95a285ull, rows 0xe9919376f3542ea7ull -> 0x389162a99c89a49cull, final_hash 0xd91533e6639e22f9ull -> 0xcafbb488b92f9edaull [PAR-ORDERS-2, H-THIN] Random40/0/0: boundary 0x869959795f89f7afull -> 0xc4f976129a95a285ull, final_hash 0xd91533e6639e22f9ull -> 0xcafbb488b92f9edaull [PAR-ORDERS-2, H-THIN] Random40/1/1: boundary 0xf47407130500a9bdull -> 0x1cc6041a6a6fcdcdull, rows 0xda9b4928c023ca07ull -> 0x33a043a5f0e45941ull, final_hash 0x5a8f1ec5a18f3731ull -> 0x798fe8d7596aaa9cull [PAR-ORDERS-2, H-THIN] Random40/1/0: boundary 0xf47407130500a9bdull -> 0x1cc6041a6a6fcdcdull, final_hash 0x5a8f1ec5a18f3731ull -> 0x798fe8d7596aaa9cull [PAR-ORDERS-2, H-THIN] Random41/0/1: boundary 0x6509ace183e7d2cdull -> 0xb4b89e111611f4ceull, rows 0x957c5244d07600b9ull -> 0xea3d6d0c77bc7e76ull, final_hash 0xcc1203a69591b134ull -> 0x980f8a18b3d80a23ull [PAR-ORDERS-2, H-THIN] Random41/0/0: boundary 0x6509ace183e7d2cdull -> 0xb4b89e111611f4ceull, final_hash 0xcc1203a69591b134ull -> 0x980f8a18b3d80a23ull [PAR-ORDERS-2, H-THIN] Random41/1/1: boundary 0xd2d43ab5fdd2ef0bull -> 0xf15d27dad1575f3bull, rows 0xb907bbe1bb2da083ull -> 0x9245b7b247af4cc1ull, final_hash 0xc55d2d003a5bec69ull -> 0xc730d99532f512d8ull [PAR-ORDERS-2, H-THIN] Random41/1/0: boundary 0xd2d43ab5fdd2ef0bull -> 0xf15d27dad1575f3bull, final_hash 0xc55d2d003a5bec69ull -> 0xc730d99532f512d8ull [PAR-ORDERS-2, H-THIN] Random42/0/1: boundary 0xa0ee848263988a3dull -> 0x0da9b26d3847ab1full, rows 0x1f2ad0a937e24f39ull -> 0x9c9329bd9adc3676ull, final_hash 0xb124b9f52e8cf9f2ull -> 0x49a0634035589083ull [H-THIN] Random42/0/0: boundary 0xa0ee848263988a3dull -> 0x0da9b26d3847ab1full, final_hash 0xb124b9f52e8cf9f2ull -> 0x49a0634035589083ull [H-THIN] Random42/1/1: boundary 0x7f6282e75cc72bb0ull -> 0xbd61639c42be9cb3ull, rows 0x23bb6075acaedb2eull -> 0x5d2fe8dd7d35b222ull, final_hash 0xdb2120d5edab4d9full -> 0xa80c78dbeddc4468ull [H-THIN] Random42/1/0: boundary 0x7f6282e75cc72bb0ull -> 0xbd61639c42be9cb3ull, final_hash 0xdb2120d5edab4d9full -> 0xa80c78dbeddc4468ull [H-THIN] Random43/0/1: boundary 0x7fb8967d6d6d24d2ull -> 0xc521512ba92a54c9ull, rows 0x98cae92178fe91ebull -> 0x5993bd8ceee91044ull, final_hash 0xdc29a310e9c7d06bull -> 0x5fbe995d73a8bff0ull [H-THIN] Random43/0/0: boundary 0x7fb8967d6d6d24d2ull -> 0xc521512ba92a54c9ull, final_hash 0xdc29a310e9c7d06bull -> 0x5fbe995d73a8bff0ull [H-THIN] Random43/1/1: boundary 0xffd1c55ce888ef72ull -> 0xa9607e56c7e4b193ull, rows 0x8ac3c2c1053a4819ull -> 0x941becccb8fe75ceull, final_hash 0x45393018e93b32beull -> 0xfdc00d0d1ff0cbfbull [H-THIN] Random43/1/0: boundary 0xffd1c55ce888ef72ull -> 0xa9607e56c7e4b193ull, final_hash 0x45393018e93b32beull -> 0xfdc00d0d1ff0cbfbull [H-THIN] Random44/0/1: boundary 0xdae57d80c4d2a634ull -> 0x971e7c4a38e301ddull, rows 0xf7669a1535370da9ull -> 0x55249f0ae51722feull, final_hash 0xc555f0e88b5eb378ull -> 0x5a13570337a638b5ull [PAR-ORDERS-2, H-THIN] Random44/0/0: boundary 0xdae57d80c4d2a634ull -> 0x971e7c4a38e301ddull, final_hash 0xc555f0e88b5eb378ull -> 0x5a13570337a638b5ull [PAR-ORDERS-2, H-THIN] Random44/1/1: boundary 0x360b704190f157adull -> 0x3ee064c4f943e93aull, rows 0x5b2a75cdabd7235cull -> 0xbc62822019ded427ull, final_hash 0xde36f7f290af13a5ull -> 0xc18096028ba7be64ull [PAR-ORDERS-2, H-THIN] Random44/1/0: boundary 0x360b704190f157adull -> 0x3ee064c4f943e93aull, final_hash 0xde36f7f290af13a5ull -> 0xc18096028ba7be64ull [PAR-ORDERS-2, H-THIN] Random45/0/1: boundary 0xbf351c50f4a44f8full -> 0xa155b80e37d41cf2ull, rows 0xfee900af4573bbc1ull -> 0xbacd32f5d8b94030ull, final_hash 0x7f2867c87c78723eull -> 0x598c179a2d5c735cull [PAR-ORDERS-2, H-THIN] Random45/0/0: boundary 0xbf351c50f4a44f8full -> 0xa155b80e37d41cf2ull, final_hash 0x7f2867c87c78723eull -> 0x598c179a2d5c735cull [PAR-ORDERS-2, H-THIN] Random45/1/1: boundary 0x32a1d85e85b93f0aull -> 0x9644e4dd1f925cdfull, rows 0xebcfcdf78cd848a5ull -> 0x2cc38b54d184cf0aull, final_hash 0x56b0e295081820c0ull -> 0x290739687f68f174ull [PAR-ORDERS-2, H-THIN] Random45/1/0: boundary 0x32a1d85e85b93f0aull -> 0x9644e4dd1f925cdfull, final_hash 0x56b0e295081820c0ull -> 0x290739687f68f174ull [PAR-ORDERS-2, H-THIN] Random46/0/1: boundary 0x3ad713968f2546d9ull -> 0xa74f65268d41a1bbull, rows 0x84dd0930114cd829ull -> 0xfc4e6864d98d62c0ull, final_hash 0x6bb382cfb976e148ull -> 0x7097cd184da8a4dcull [H-THIN, PAR-CASHFEE] Random46/0/0: boundary 0x3ad713968f2546d9ull -> 0xa74f65268d41a1bbull, final_hash 0x6bb382cfb976e148ull -> 0x7097cd184da8a4dcull [H-THIN, PAR-CASHFEE] Random46/1/1: boundary 0x997f2e0ee9786d91ull -> 0xb1406e1f21710782ull, rows 0x1ce86cd2c755bea3ull -> 0xe0b910f752cbc3adull, final_hash 0x556134f8585397c9ull -> 0x2539296694529005ull [H-THIN, PAR-CASHFEE] Random46/1/0: boundary 0x997f2e0ee9786d91ull -> 0xb1406e1f21710782ull, final_hash 0x556134f8585397c9ull -> 0x2539296694529005ull [H-THIN, PAR-CASHFEE] Random47/0/1: boundary 0x8ce025f147bbdb2dull -> 0x214dc1308457a01aull, rows 0x74895c0d69d6b4a8ull -> 0xe293c7196f5c9f95ull, final_hash 0x31ec37588708d5f1ull -> 0x8176d644a621a19eull [H-THIN] Random47/0/0: boundary 0x8ce025f147bbdb2dull -> 0x214dc1308457a01aull, final_hash 0x31ec37588708d5f1ull -> 0x8176d644a621a19eull [H-THIN] Random47/1/1: boundary 0xebf0544eb982483aull -> 0xfe949f5ba003397dull, rows 0xca410d7365dca790ull -> 0xe112a9940c547399ull, final_hash 0xa8aff582382caf48ull -> 0x9ee620e1bf252639ull [H-THIN] Random47/1/0: boundary 0xebf0544eb982483aull -> 0xfe949f5ba003397dull, final_hash 0xa8aff582382caf48ull -> 0x9ee620e1bf252639ull [H-THIN] Random48/0/1: boundary 0xaa6b18e4254e2f68ull -> 0x21c0bf9ae910d3acull, rows 0x2f80186c69547a10ull -> 0xe2d98f0ffb5c1b96ull, final_hash 0xaff43b38e6bc02acull -> 0x1e560afd06471537ull [H-THIN] Random48/0/0: boundary 0xaa6b18e4254e2f68ull -> 0x21c0bf9ae910d3acull, final_hash 0xaff43b38e6bc02acull -> 0x1e560afd06471537ull [H-THIN] Random48/1/1: boundary 0x655a5637ae415d60ull -> 0x4fd90c8ca1025428ull, rows 0x13866e45be14eaeeull -> 0x7706d5e9ebfd9226ull, final_hash 0xa0e205aa768a0bc9ull -> 0xee01efa6d7b40515ull [H-THIN] Random48/1/0: boundary 0x655a5637ae415d60ull -> 0x4fd90c8ca1025428ull, final_hash 0xa0e205aa768a0bc9ull -> 0xee01efa6d7b40515ull [H-THIN] test_adapter_command_allocations: 16 lines Script::ExitReissue/false/false: rows_digest 0xab588834fadd3803ull -> 0x531f1841e4e5968dull, final_hash 5699976773346796219ull -> 2515667530823692736ull [H-THIN] Script::ExitReissue/false/true: rows_digest 0x31d20a68825b4041ull -> 0x904f0e658a3f9a84ull, final_hash 5368550971089751205ull -> 10620900185585014094ull [H-THIN] Script::ExitReissue/true/false: rows_digest 0xcecfebbbe86fc767ull -> 0xb7d5805919147374ull, final_hash 6863695800300943516ull -> 15062163376854141177ull [H-THIN] Script::ExitReissue/true/true: rows_digest 0x6209f50bb7d9d6acull -> 0x5b2a95f0e24225afull, final_hash 1107562895774304952ull -> 14855689204879364685ull [H-THIN] Script::FarReissue/false/false: rows_digest 0xdf6b23adbf03e66eull -> 0x46b2ec207c22c83eull, final_hash 873772099948613669ull -> 15192390774338051974ull [H-THIN] Script::FarReissue/false/true: rows_digest 0xa3863ebe522e6dbeull -> 0x57bcf2f81127cb7cull, final_hash 4285418729649473335ull -> 2459246240855329928ull [H-THIN] Script::FarReissue/true/false: rows_digest 0x688ef29ae4ccdebeull -> 0x423a3c703f0df268ull, final_hash 1841254732555988423ull -> 15244552330600615048ull [H-THIN] Script::FarReissue/true/true: rows_digest 0xf4c8662f18c74e7bull -> 0xa76d150ee8cd57c8ull, final_hash 462243035089940333ull -> 14117483659754983584ull [H-THIN] Script::EntryReissue/false/false: rows_digest 0x80ea582d7a285f56ull -> 0xd3d6f15ce920583cull, final_hash 2970596418120183524ull -> 11220111473525669975ull [H-THIN] Script::EntryReissue/false/true: rows_digest 0xa4e881bd0166e308ull -> 0x945ec1e50439cea9ull, final_hash 10388992876317270182ull -> 12057658321036544371ull [H-THIN] Script::EntryReissue/true/false: rows_digest 0xc01d27d0c8fd1cadull -> 0xb4bf05553414c0f6ull, final_hash 8910455723513195319ull -> 10998295444980399292ull [H-THIN] Script::EntryReissue/true/true: rows_digest 0x2bf51a0396f73773ull -> 0xfafcf88fbbcb31fdull, final_hash 5882765811046345928ull -> 4518277612499075029ull [H-THIN] Script::TrailReissue/false/false: rows_digest 0x27b9526d5e960dafull -> 0xc8f7abcf99019929ull, final_hash 1551066713552723293ull -> 11128545198996405094ull [H-THIN] Script::TrailReissue/false/true: rows_digest 0x12bab345b4c21bdeull -> 0xa8b81e0ff7a1ed94ull, final_hash 5117488907256490505ull -> 16557720182371283228ull [H-THIN] Script::TrailReissue/true/false: rows_digest 0xbae98e9f6da67e3dull -> 0x98ba7b2c66e6130bull, final_hash 1320551696866588489ull -> 16341311528503403960ull [H-THIN] Script::TrailReissue/true/true: rows_digest 0x7e3565d7ef6b9c34ull -> 0xfdd8fbf5bc9ecc0aull, final_hash 12226981095802833438ull -> 15203686385562626719ull [H-THIN] test_publication_witness: 156 lines Config01: digest 0x13599d44631228e2ull -> 0xb668af594ea2f5d9ull [H-THIN] ConfigFlags101: digest 0x9d75aa3c41307c6aull -> 0xe7f27c137f45ac4bull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] Config01M: digest 0x3ebbd6d78581edeaull -> 0x1efcbeef8aa1dc6dull [H-THIN] ConfigFlags101M: digest 0x347646d359e21807ull -> 0xbee912e49e616f4eull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] Config02: digest 0x6dde6b5c5440d715ull -> 0x341cf20ba6b96fc3ull [H-THIN] ConfigFlags102: digest 0x91619a8b469ea6aeull -> 0xef9792602fc551c1ull [H-THIN] Config02M: digest 0xd04764a2d7139486ull -> 0x4983b8719bbfd192ull [H-THIN] ConfigFlags102M: digest 0x854413a4a59d0d33ull -> 0xeed018756d1228e9ull [H-THIN] Config03: digest 0x461a919ebdcd41e9ull -> 0x0fc93105580e996eull [PAR-MARGIN-2, H-THIN, PAR-CASHFEE] ConfigFlags103: digest 0xcf60de51b3acfcc3ull -> 0x9e412b4fb357ae55ull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] Config03M: digest 0x4511d5cd864e706full -> 0x2e2ff1ccdf841c8dull [H-THIN, PAR-CASHFEE] ConfigFlags103M: digest 0xdd7937eb65073a91ull -> 0xa68e0a951db111ddull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] Config04: digest 0xa0a03448564c9be0ull -> 0x63aa9da27d89b846ull [H-THIN, PAR-CASHFEE] ConfigFlags104: digest 0xaf23be135bf66e22ull -> 0xefbb1c6bfcb31f17ull [H-THIN, PAR-CASHFEE] Config04M: digest 0xe0f138c9c848b160ull -> 0xba3069b5b515a320ull [H-THIN, PAR-CASHFEE] ConfigFlags104M: digest 0xbf637ed723b215beull -> 0x0b1d2d1d8096f6b9ull [H-THIN, PAR-CASHFEE] Config05: digest 0x17fc80e7cecd9737ull -> 0x705de70e2d70af67ull [H-THIN] ConfigFlags105: digest 0x270ebbdd8cfa9d63ull -> 0x69dac2f5ec1d400cull [H-THIN] Config05M: digest 0x3e5a9fcbb1fdcd5eull -> 0x7f5b8acc384eb6daull [H-THIN] ConfigFlags105M: digest 0x680bdc701517be1aull -> 0x3e23588769a2f97bull [H-THIN] Config06: digest 0x19f57666c4c34f11ull -> 0x9f061ce6918f93c5ull [H-THIN] ConfigFlags106: digest 0x006c443d5ac275d3ull -> 0xb5e0aaa404ca93b3ull [H-THIN] Config06M: digest 0x9b6058dc837818a5ull -> 0x1ef42058337147b8ull [H-THIN] ConfigFlags106M: digest 0xdd38d808b0b2005full -> 0x3f82500e8ed30156ull [H-THIN] Config07: digest 0xd91ff183f38d87ceull -> 0x86c366bc672888faull [H-THIN, PAR-CASHFEE] ConfigFlags107: digest 0x579a2bc891f60711ull -> 0xbe9ea2687b1b417aull [H-THIN, PAR-CASHFEE] Config07M: digest 0x4225ca6a6ba7619eull -> 0x0bdf3ca651b69092ull [H-THIN, PAR-CASHFEE] ConfigFlags107M: digest 0xca1facefefeb3fceull -> 0x2a5b0d3d56b2cd03ull [H-THIN, PAR-CASHFEE] Config08: digest 0x904781b92a51efd9ull -> 0xa23953fc578a6b43ull [H-THIN, PAR-CASHFEE] ConfigFlags108: digest 0xccc9a3e471b37be3ull -> 0x10e8434816e387afull [H-THIN] Config08M: digest 0xf227bccff87ad1d6ull -> 0x1f60bb9c965465d8ull [H-THIN, PAR-CASHFEE] ConfigFlags108M: digest 0xf38c8ecf14fb7075ull -> 0x2271fc69af564e4cull [H-THIN] Config09: digest 0xc4943ac1cca48c3eull -> 0x9360d69c97c9db7bull [H-THIN, PAR-CASHFEE] ConfigFlags109: digest 0x33cf69335b29701full -> 0x30a8c80d3bd0ad61ull [H-THIN, PAR-CASHFEE] Config09M: digest 0xd60f90586ad5c873ull -> 0x9e5b912398d35af8ull [H-THIN, PAR-CASHFEE] ConfigFlags109M: digest 0xc446a8edce72ed55ull -> 0x5a3876b995a66c76ull [H-THIN, PAR-CASHFEE] Config10: digest 0xd207636458a7977dull -> 0x18f9e53074280722ull [H-THIN] ConfigFlags110: digest 0x1be5ae2e695135fdull -> 0x248ead6c9dbf66b7ull [H-THIN, PAR-CASHFEE] Config10M: digest 0x9e63b9317b0ee761ull -> 0x4be74baf8fc46644ull [H-THIN] ConfigFlags110M: digest 0x7aabfb779282bb56ull -> 0x54a9f81cdce9d069ull [H-THIN, PAR-CASHFEE] Config11: digest 0x777e1f3e0fba2d04ull -> 0xb150c52332d168ffull [H-THIN] ConfigFlags111: digest 0x4722a0ba517a8698ull -> 0x32f4fb6b779cc554ull [H-THIN] Config11M: digest 0x33ccb718a4da671eull -> 0xc8005b52c87cf782ull [H-THIN] ConfigFlags111M: digest 0xd39fafb2fda05ff0ull -> 0xdb5300166665f9c1ull [H-THIN] Config12: digest 0x4b461c21259a2e32ull -> 0xc51092de174909d7ull [H-THIN, PAR-CASHFEE] ConfigFlags112: digest 0x87321f2899dad267ull -> 0x68af9f1a740c44e9ull [H-THIN] Config12M: digest 0xd27a9a9ee92fb498ull -> 0x3edbdba7792b3233ull [H-THIN, PAR-CASHFEE] ConfigFlags112M: digest 0xa9764e2ca9500901ull -> 0xfd4565803d79a56bull [H-THIN] Config13: digest 0x5c4aa53ee5c2d4b9ull -> 0x254b2221a515f9e7ull [H-THIN, PAR-CASHFEE] ConfigFlags113: digest 0xb0d7186d612e1063ull -> 0x6c6163d6eb022517ull [H-THIN, PAR-CASHFEE] Config13M: digest 0x5ba51d9728c119b8ull -> 0x2e78fa9fbc049dedull [H-THIN, PAR-CASHFEE] ConfigFlags113M: digest 0x066c20032094d9bfull -> 0x6e9f5a9ece8a9c58ull [H-THIN, PAR-CASHFEE] Config14: digest 0x6e95423cd9f2042aull -> 0x523695406e13e23cull [H-THIN, PAR-CASHFEE] ConfigFlags114: digest 0x96a34f2bb0dc2df1ull -> 0x65aeb7fa3c3ab6bfull [H-THIN, PAR-CASHFEE] Config14M: digest 0x3c6809a3326959b5ull -> 0x9a3809499b3623c6ull [H-THIN, PAR-CASHFEE] ConfigFlags114M: digest 0xb11d614113e0ee96ull -> 0x63e140a82186a98dull [H-THIN, PAR-CASHFEE] Config15: digest 0x9072bdfaf315f3b2ull -> 0xad1c7175dc94f6bcull [H-THIN] ConfigFlags115: digest 0x9049c3bebf6874f2ull -> 0x0141076a069b89e3ull [H-THIN] Config15M: digest 0x21730b1c555a3119ull -> 0x1344d28fa92876b9ull [H-THIN] ConfigFlags115M: digest 0x2af33a20163cc455ull -> 0xd8441700cc46211dull [H-THIN] Config16: digest 0x5b7b329438d68317ull -> 0x2533d8cb0bd677c4ull [H-THIN] ConfigFlags116: digest 0x112cac461ad5e409ull -> 0xf6100c192c1b1a47ull [H-THIN, PAR-CASHFEE] Config16M: digest 0x74174f7e187f36a9ull -> 0xfff00bd45398ff09ull [H-THIN] ConfigFlags116M: digest 0xfee39f4d6acb5dd9ull -> 0x4e9b7658af4a3968ull [H-THIN, PAR-CASHFEE] CloseAllThenEntry: digest 0x64d991ad87b59143ull -> 0xc9a6feca41e82e29ull [H-THIN] CloseAllThenEntryM: digest 0x768b725ffcc7ca57ull -> 0xd0aa4f83cfd909c6ull [H-THIN] Reversals: digest 0xf5db5c66ecd1dbd2ull -> 0x23a3b43853bb2e1aull [H-THIN] ReversalsM: digest 0x36b59c9ef1737981ull -> 0x29160b951dc12d18ull [H-THIN] SameBarBatch: digest 0x13e5941fa9151818ull -> 0x67372c2ba662f6c8ull [H-THIN] SameBarBatchM: digest 0x10604224255b732aull -> 0x5517e412f630d8c8ull [H-THIN] CloseOrderAdds: digest 0xa7d126e01939e06dull -> 0xe5d547fd7ea6eb99ull [H-THIN] CloseOrderAddsM: digest 0x72762eb6d5e1230cull -> 0x2fc90068db6aa341ull [H-THIN] DelayedStops: digest 0xa655cdf979a2d473ull -> 0xf698a0012daa63b3ull [PAR-ORDERS-2, H-THIN] DelayedStopsM: digest 0x1d34635f60c4dbdbull -> 0x046314c00141b54cull [PAR-ORDERS-2, H-THIN] Storm01: digest 0x2940f65451a3189full -> 0x0d6f6de3707c0f23ull [H-THIN] Storm01M: digest 0xbfc8780fbe24d51cull -> 0x17fdf11d7effc7f0ull [H-THIN] Storm02: digest 0x951345d2c3fa83a0ull -> 0x5ab7e89bf0a32e17ull [PAR-ORDERS-2, H-THIN] Storm02M: digest 0x1521264d78122ebeull -> 0xae05144418043f11ull [PAR-ORDERS-2, H-THIN] Storm03: digest 0x25f90b9eeb5f057aull -> 0x1734fbc73a88fdafull [H-THIN] Storm03M: digest 0xe4da66913719f57aull -> 0x87784b723c37ec66ull [H-THIN] Storm04: digest 0x3157bd59f491a789ull -> 0x33ceb62ad976d38aull [H-THIN] Storm04M: digest 0xa1dd548b8f00457eull -> 0xb06e7fe199931e4aull [H-THIN] Storm05: digest 0x8eacccdcac89765cull -> 0x00106a73f4035d2bull [PAR-ORDERS-2, H-THIN] Storm05M: digest 0x67d9decf4dd40cb4ull -> 0xdef761ffd71cac33ull [PAR-ORDERS-2, H-THIN] Storm06: digest 0x4053ebf00e230b53ull -> 0x6bfcdec22c1a1c8full [H-THIN] Storm06M: digest 0x4d4c9f8c75e1901bull -> 0x44fe7480122bc682ull [H-THIN] Storm07: digest 0xf7e280a190e60133ull -> 0xacff98851a92d473ull [H-THIN] Storm07M: digest 0x3b29ea97a5b2dd0aull -> 0x196a55d2091dc429ull [H-THIN] Storm08: digest 0x9302e1c04c2dfedfull -> 0x03ba3cc50503307dull [PAR-ORDERS-2, H-THIN] Storm08M: digest 0x8944facb400c0524ull -> 0xcdb1ebd3491998e5ull [PAR-ORDERS-2, H-THIN] Storm09: digest 0x09195d4f14ec28ddull -> 0xc5746cc50cf41375ull [H-THIN] Storm09M: digest 0x5a6cd7deec2033ffull -> 0x8aa89637b4d04cf0ull [H-THIN] Storm10: digest 0x7ae3261a9cbdeff3ull -> 0x2697bb1c5389db54ull [PAR-ORDERS-2, H-THIN] Storm10M: digest 0x03595faccb3309cdull -> 0x741a981667c1a302ull [PAR-ORDERS-2, H-THIN] Storm11: digest 0xf46684d86365208aull -> 0x7cfb0ba740c2806bull [H-THIN] Storm11M: digest 0xb5205907e08e01bfull -> 0xcfc4532d945da385ull [H-THIN] Storm12: digest 0x8b151167a2310837ull -> 0x659852f60d3d358cull [PAR-ORDERS-2, H-THIN] Storm12M: digest 0xc49a83bf5709682bull -> 0xdb3db1a969f1b59aull [PAR-ORDERS-2, H-THIN] Storm13: digest 0xde3f322bf0c59a98ull -> 0xe613ff94ad1aad2aull [H-THIN] Storm13M: digest 0xf7d902eba9385f60ull -> 0xf7793304d1dbd532ull [H-THIN] Storm14: digest 0x1ac3a18ac2e4ed4bull -> 0x0b1595a2d45290f2ull [H-THIN] Storm14M: digest 0xd88e046854060dd6ull -> 0x9e9a3044d9dbcad8ull [H-THIN] Storm15: digest 0x6229ca73e971d1c1ull -> 0xc2912804f77858fbull [H-THIN] Storm15M: digest 0xa648d45a63e9a8afull -> 0xf32358c4a29e8066ull [H-THIN] Storm16: digest 0x8918c41ad3b998bdull -> 0x9f128f923d41d2e7ull [H-THIN] Storm16M: digest 0x6b216cf99a1880dfull -> 0x72b90fec2e8871d7ull [H-THIN] Storm17: digest 0x644e0ad8e5fd4833ull -> 0xd62326206a38e5f5ull [PAR-ORDERS-2, H-THIN] Storm17M: digest 0x46e2d77c04f174e3ull -> 0x9631182cfd1c6023ull [PAR-ORDERS-2, H-THIN] Storm18: digest 0xe7e874c21bf9c5fcull -> 0x86e8b0cc2cbda4aeull [PAR-ORDERS-2, H-THIN] Storm18M: digest 0x75b39ccdb9b4542dull -> 0xd9fb27e46899ccd7ull [PAR-ORDERS-2, H-THIN] Storm19: digest 0x8c49334fb012a382ull -> 0xe92413fbdc464242ull [PAR-ORDERS-2, H-THIN] Storm19M: digest 0x83ea12d967fea7e7ull -> 0xa9a584e70d470321ull [PAR-ORDERS-2, H-THIN] Storm20: digest 0xe93eebdfc01f57b4ull -> 0x93de366f51f2315dull [PAR-ORDERS-2, H-THIN] Storm20M: digest 0xfdb5a657600e2375ull -> 0x201adc8ab4ea9bdbull [PAR-ORDERS-2, H-THIN] Storm21: digest 0xc0aad15a28128003ull -> 0xd4a8eda3db34c6eeull [H-THIN] Storm21M: digest 0xbc3fbaacf5272ad1ull -> 0xefaa36484eb9c7a1ull [H-THIN] Storm22: digest 0xda5de455f464504dull -> 0x686821a1adb13279ull [PAR-ORDERS-2, H-THIN] Storm22M: digest 0xfdaa191a97b50614ull -> 0x28a2c0fefdac67afull [PAR-ORDERS-2, H-THIN] Storm23: digest 0xb91e295d6da6778cull -> 0xfac7dcb54dc93113ull [PAR-ORDERS-2, H-THIN] Storm23M: digest 0x78f7d0134456384full -> 0xe66e15ba3a360e59ull [PAR-ORDERS-2, H-THIN] Storm24: digest 0x6f40ccb0266ca278ull -> 0x9a05f4cc2a37c8b8ull [H-THIN] Storm24M: digest 0xa2eeb9f1d0e10619ull -> 0xa1f1acfcca37ef51ull [H-THIN] Legs01: digest 0xb34653b540b649dfull -> 0x2257c1f1b017aff6ull [H-THIN] Legs01M: digest 0xafbbc030265c824full -> 0xdeeb42cf30042b94ull [H-THIN] LegsStops01: digest 0x622c768899d0febaull -> 0xe82893532dd3da24ull [H-THIN] LegsStops01M: digest 0xd71b800ba07f1670ull -> 0x40cde458e42af83dull [H-THIN] Legs02: digest 0xe6cecd70ee4e816cull -> 0x290854a540c0effdull [H-THIN] Legs02M: digest 0x9932159d4daf8f18ull -> 0xa31bffc389143eaaull [H-THIN] LegsStops02: digest 0x032c556130d84ee8ull -> 0x7aa8ab78f1f5ada3ull [H-THIN] LegsStops02M: digest 0xb2d41090d3afed86ull -> 0xa1d4657aea1b015dull [H-THIN] Legs03: digest 0xe484f43d3f03e46aull -> 0x4a813820bb3a558dull [H-THIN] Legs03M: digest 0x5a7256077ac9caf7ull -> 0xc10d45c7cb3cbee5ull [H-THIN] LegsStops03: digest 0xd1cb8cd8c6459831ull -> 0xc02aaa3f7c355e7aull [H-THIN] LegsStops03M: digest 0x6f87644ed30f1f84ull -> 0x55a7fe1ea0fc100aull [H-THIN] Legs04: digest 0x9405751b42b03526ull -> 0x254b1a89ea0388ddull [H-THIN] Legs04M: digest 0xf80fabe52a3a310bull -> 0x079706336deb5323ull [H-THIN] LegsStops04: digest 0x3b9823513ae000abull -> 0xc6a5620646c92b3cull [H-THIN] LegsStops04M: digest 0x939d11e8a52ef4a6ull -> 0xbcad7c107485fbe8ull [H-THIN] Legs05: digest 0xbe440458b5a03c6bull -> 0x2b3d7db72415c3ecull [H-THIN] Legs05M: digest 0xab877ee06eeef9deull -> 0x88fd0678c5020748ull [H-THIN] LegsStops05: digest 0x9dd37a12391bd5f1ull -> 0x94435fe6bea6a39dull [H-THIN] LegsStops05M: digest 0x5895c3b5e20f05d2ull -> 0x2722fdb103c71c42ull [H-THIN] Legs06: digest 0x8273cddc66300d47ull -> 0x6e7d43e693bec002ull [H-THIN] Legs06M: digest 0x38b98f46b0793f2cull -> 0x2fc1828cff3085f2ull [H-THIN] LegsStops06: digest 0x5eafd9de638208b3ull -> 0x198f7d7e224884c0ull [H-THIN] LegsStops06M: digest 0x06d7e046fb01bdf4ull -> 0x507c89b50b4d4b2bull [H-THIN] TwoEntries: digest 0x8c5be2d95d0be7e5ull -> 0x06f2ecb7c1a3a480ull [H-THIN] TwoEntriesM: digest 0xfbbd9f232e1ccfbcull -> 0x7c956e01ced10ccbull [H-THIN] PyramidLots: digest 0x196e1c6f6d80b3feull -> 0x243689d4eeaf8d3bull [H-THIN] PyramidLotsM: digest 0xc771a9a6a70374c6ull -> 0x7c425d1716c9a5a2ull [H-THIN] LegBesideClose: digest 0x805b56fa470a4dbeull -> 0xfd494f8d3e8228d4ull [H-THIN] LegBesideCloseM: digest 0x8e0bb72d36ce8063ull -> 0x4a9e3df3ac353419ull [H-THIN] TwoLegsOnly: digest 0x0ed47ce9f98c045aull -> 0x1e97c755ea7f6e5full [H-THIN] TwoLegsOnlyM: digest 0xa713e15d8063372full -> 0x8b4283e07b576d5full [H-THIN] LegsApart: digest 0xcfcd1f1d87803a44ull -> 0x64ff417b6bcf212full [H-THIN] LegsApartM: digest 0x701aca7df9071fb5ull -> 0xaa0c742ddeacc4d5ull [H-THIN] test_adapter_host_view_memo: 13 lines batch/A: hash=496593797863776142 -> hash=14113207875665531755 [H-THIN] batch/B: hash=5316201723780989919 -> hash=3001060687180726304 [H-THIN] stream/A: hash=4351362905685696693 -> hash=1927432040325474646 [H-THIN] stream/B: hash=1033336853289890500 -> hash=13106388348010577033 [H-THIN] interleaved/A: hash=4351362905685696693 -> hash=1927432040325474646 [H-THIN] interleaved/B: hash=1033336853289890500 -> hash=13106388348010577033 [H-THIN] round_robin/A1: hash=4351362905685696693 -> hash=1927432040325474646 [H-THIN] round_robin/B: hash=1033336853289890500 -> hash=13106388348010577033 [H-THIN] round_robin/A2: hash=4351362905685696693 -> hash=1927432040325474646 [H-THIN] rebuilt/0: hash=496593797863776142 -> hash=14113207875665531755 [H-THIN] rebuilt/1: hash=5316201723780989919 -> hash=3001060687180726304 [H-THIN] rebuilt/2: hash=496593797863776142 -> hash=14113207875665531755 [H-THIN] rebuilt/3: hash=5316201723780989919 -> hash=3001060687180726304 [H-THIN] Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_adapter_command_allocations.cpp | 49 +- tests/test_adapter_continuation_view.cpp | 518 ++++-- tests/test_adapter_host_view_memo_data.hpp | 41 +- ...test_adapter_lookup_index_witness_data.hpp | 1596 +++++++++++++---- tests/test_adapter_quiet_bar_pinned.inc | 817 ++++++--- tests/test_adapter_receipts_in_place.cpp | 540 +++++- tests/test_adapter_recording_hash_witness.cpp | 813 ++++++--- tests/test_adapter_report_relower.cpp | 219 ++- tests/test_publication_witness_pinned.inc | 469 +++-- 9 files changed, 3681 insertions(+), 1381 deletions(-) diff --git a/tests/test_adapter_command_allocations.cpp b/tests/test_adapter_command_allocations.cpp index 80d0c401..c9442708 100644 --- a/tests/test_adapter_command_allocations.cpp +++ b/tests/test_adapter_command_allocations.cpp @@ -308,23 +308,40 @@ constexpr Script kScripts[] = {Script::ExitReissue, Script::FarReissue, Script:: // Script::TrailReissue/false/true: rows_digest 0x56954a0db7786b71ull -> 0x12bab345b4c21bdeull, final_hash 5779925080749936105ull -> 5117488907256490505ull // Script::TrailReissue/true/false: rows_digest 0x09315718e551137full -> 0xbae98e9f6da67e3dull, final_hash 269689715775399305ull -> 1320551696866588489ull // Script::TrailReissue/true/true: rows_digest 0x5ef417ff409d6fadull -> 0x7e3565d7ef6b9c34ull, final_hash 3401687048604417982ull -> 12226981095802833438ull +// expectation corrected (INT26 v19 hash re-pin, 32 values over 16 rows), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; this host projects one fixed execution hash, so the continuation does not enter; the recorded row count, the trades and the trade digest did not move: +// Script::ExitReissue/false/false: rows_digest 0xab588834fadd3803ull -> 0x531f1841e4e5968dull, final_hash 5699976773346796219ull -> 2515667530823692736ull [H-THIN] +// Script::ExitReissue/false/true: rows_digest 0x31d20a68825b4041ull -> 0x904f0e658a3f9a84ull, final_hash 5368550971089751205ull -> 10620900185585014094ull [H-THIN] +// Script::ExitReissue/true/false: rows_digest 0xcecfebbbe86fc767ull -> 0xb7d5805919147374ull, final_hash 6863695800300943516ull -> 15062163376854141177ull [H-THIN] +// Script::ExitReissue/true/true: rows_digest 0x6209f50bb7d9d6acull -> 0x5b2a95f0e24225afull, final_hash 1107562895774304952ull -> 14855689204879364685ull [H-THIN] +// Script::FarReissue/false/false: rows_digest 0xdf6b23adbf03e66eull -> 0x46b2ec207c22c83eull, final_hash 873772099948613669ull -> 15192390774338051974ull [H-THIN] +// Script::FarReissue/false/true: rows_digest 0xa3863ebe522e6dbeull -> 0x57bcf2f81127cb7cull, final_hash 4285418729649473335ull -> 2459246240855329928ull [H-THIN] +// Script::FarReissue/true/false: rows_digest 0x688ef29ae4ccdebeull -> 0x423a3c703f0df268ull, final_hash 1841254732555988423ull -> 15244552330600615048ull [H-THIN] +// Script::FarReissue/true/true: rows_digest 0xf4c8662f18c74e7bull -> 0xa76d150ee8cd57c8ull, final_hash 462243035089940333ull -> 14117483659754983584ull [H-THIN] +// Script::EntryReissue/false/false: rows_digest 0x80ea582d7a285f56ull -> 0xd3d6f15ce920583cull, final_hash 2970596418120183524ull -> 11220111473525669975ull [H-THIN] +// Script::EntryReissue/false/true: rows_digest 0xa4e881bd0166e308ull -> 0x945ec1e50439cea9ull, final_hash 10388992876317270182ull -> 12057658321036544371ull [H-THIN] +// Script::EntryReissue/true/false: rows_digest 0xc01d27d0c8fd1cadull -> 0xb4bf05553414c0f6ull, final_hash 8910455723513195319ull -> 10998295444980399292ull [H-THIN] +// Script::EntryReissue/true/true: rows_digest 0x2bf51a0396f73773ull -> 0xfafcf88fbbcb31fdull, final_hash 5882765811046345928ull -> 4518277612499075029ull [H-THIN] +// Script::TrailReissue/false/false: rows_digest 0x27b9526d5e960dafull -> 0xc8f7abcf99019929ull, final_hash 1551066713552723293ull -> 11128545198996405094ull [H-THIN] +// Script::TrailReissue/false/true: rows_digest 0x12bab345b4c21bdeull -> 0xa8b81e0ff7a1ed94ull, final_hash 5117488907256490505ull -> 16557720182371283228ull [H-THIN] +// Script::TrailReissue/true/false: rows_digest 0xbae98e9f6da67e3dull -> 0x98ba7b2c66e6130bull, final_hash 1320551696866588489ull -> 16341311528503403960ull [H-THIN] +// Script::TrailReissue/true/true: rows_digest 0x7e3565d7ef6b9c34ull -> 0xfdd8fbf5bc9ecc0aull, final_hash 12226981095802833438ull -> 15203686385562626719ull [H-THIN] const Pin kPins[] = { - {Script::ExitReissue, false, false, 0xab588834fadd3803ull, 60ull, 5699976773346796219ull, 0x584fc4b9ebbba609ull, 4}, - {Script::ExitReissue, false, true, 0x31d20a68825b4041ull, 60ull, 5368550971089751205ull, 0x584fc4b9ebbba609ull, 4}, - {Script::ExitReissue, true, false, 0xcecfebbbe86fc767ull, 60ull, 6863695800300943516ull, 0x41e6c3b1e0481c20ull, 4}, - {Script::ExitReissue, true, true, 0x6209f50bb7d9d6acull, 60ull, 1107562895774304952ull, 0x41e6c3b1e0481c20ull, 4}, - {Script::FarReissue, false, false, 0xdf6b23adbf03e66eull, 60ull, 873772099948613669ull, 0x7aac154ea6a8629aull, 4}, - {Script::FarReissue, false, true, 0xa3863ebe522e6dbeull, 60ull, 4285418729649473335ull, 0x7aac154ea6a8629aull, 4}, - {Script::FarReissue, true, false, 0x688ef29ae4ccdebeull, 60ull, 1841254732555988423ull, 0xfdb435b76f2571b6ull, 4}, - {Script::FarReissue, true, true, 0xf4c8662f18c74e7bull, 60ull, 462243035089940333ull, 0xfdb435b76f2571b6ull, 4}, - {Script::EntryReissue, false, false, 0x80ea582d7a285f56ull, 60ull, 2970596418120183524ull, 0xfa79a63a9a8de06aull, 29}, - {Script::EntryReissue, false, true, 0xa4e881bd0166e308ull, 60ull, 10388992876317270182ull, 0xdc986aa5f5cc536aull, 29}, - {Script::EntryReissue, true, false, 0xc01d27d0c8fd1cadull, 60ull, 8910455723513195319ull, 0xfa79a63a9a8de06aull, 29}, - {Script::EntryReissue, true, true, 0x2bf51a0396f73773ull, 60ull, 5882765811046345928ull, 0xdc986aa5f5cc536aull, 29}, - {Script::TrailReissue, false, false, 0x27b9526d5e960dafull, 60ull, 1551066713552723293ull, 0xd64b560956c46b33ull, 3}, - {Script::TrailReissue, false, true, 0x12bab345b4c21bdeull, 60ull, 5117488907256490505ull, 0x5241bc8c55a4a4a3ull, 3}, - {Script::TrailReissue, true, false, 0xbae98e9f6da67e3dull, 60ull, 1320551696866588489ull, 0xcdcafa54f49f508aull, 3}, - {Script::TrailReissue, true, true, 0x7e3565d7ef6b9c34ull, 60ull, 12226981095802833438ull, 0x45d4c2aceeb466faull, 3}, + {Script::ExitReissue, false, false, 0x531f1841e4e5968dull, 60ull, 2515667530823692736ull, 0x584fc4b9ebbba609ull, 4}, + {Script::ExitReissue, false, true, 0x904f0e658a3f9a84ull, 60ull, 10620900185585014094ull, 0x584fc4b9ebbba609ull, 4}, + {Script::ExitReissue, true, false, 0xb7d5805919147374ull, 60ull, 15062163376854141177ull, 0x41e6c3b1e0481c20ull, 4}, + {Script::ExitReissue, true, true, 0x5b2a95f0e24225afull, 60ull, 14855689204879364685ull, 0x41e6c3b1e0481c20ull, 4}, + {Script::FarReissue, false, false, 0x46b2ec207c22c83eull, 60ull, 15192390774338051974ull, 0x7aac154ea6a8629aull, 4}, + {Script::FarReissue, false, true, 0x57bcf2f81127cb7cull, 60ull, 2459246240855329928ull, 0x7aac154ea6a8629aull, 4}, + {Script::FarReissue, true, false, 0x423a3c703f0df268ull, 60ull, 15244552330600615048ull, 0xfdb435b76f2571b6ull, 4}, + {Script::FarReissue, true, true, 0xa76d150ee8cd57c8ull, 60ull, 14117483659754983584ull, 0xfdb435b76f2571b6ull, 4}, + {Script::EntryReissue, false, false, 0xd3d6f15ce920583cull, 60ull, 11220111473525669975ull, 0xfa79a63a9a8de06aull, 29}, + {Script::EntryReissue, false, true, 0x945ec1e50439cea9ull, 60ull, 12057658321036544371ull, 0xdc986aa5f5cc536aull, 29}, + {Script::EntryReissue, true, false, 0xb4bf05553414c0f6ull, 60ull, 10998295444980399292ull, 0xfa79a63a9a8de06aull, 29}, + {Script::EntryReissue, true, true, 0xfafcf88fbbcb31fdull, 60ull, 4518277612499075029ull, 0xdc986aa5f5cc536aull, 29}, + {Script::TrailReissue, false, false, 0xc8f7abcf99019929ull, 60ull, 11128545198996405094ull, 0xd64b560956c46b33ull, 3}, + {Script::TrailReissue, false, true, 0xa8b81e0ff7a1ed94ull, 60ull, 16557720182371283228ull, 0x5241bc8c55a4a4a3ull, 3}, + {Script::TrailReissue, true, false, 0x98ba7b2c66e6130bull, 60ull, 16341311528503403960ull, 0xcdcafa54f49f508aull, 3}, + {Script::TrailReissue, true, true, 0xfdd8fbf5bc9ecc0aull, 60ull, 15203686385562626719ull, 0x45d4c2aceeb466faull, 3}, }; void the_scripts_produce_the_values_pinned_before_the_lane() { diff --git a/tests/test_adapter_continuation_view.cpp b/tests/test_adapter_continuation_view.cpp index 7f1ce403..6429cac8 100644 --- a/tests/test_adapter_continuation_view.cpp +++ b/tests/test_adapter_continuation_view.cpp @@ -346,14 +346,19 @@ void emit(const char* name, const std::vector& reads) { // 4370795498617211977ull -> 2898399952537902865ull // 10361542891827113526ull -> 8964175343669296125ull // 4370795498617211977ull -> 2898399952537902865ull +// expectation corrected (INT26 v19 hash re-pin, k_chart, 4 of 7 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary: +// 17778912298194596574ull -> 2540222023230692795ull [H-THIN] +// 8941018855024971093ull -> 7277266162097189602ull [H-THIN] +// 2898399952537902865ull -> 13929697350787785222ull [H-THIN] +// 2898399952537902865ull -> 13929697350787785222ull [H-THIN] constexpr std::uint64_t k_chart[] = { 5575980828577033360ull, - 17778912298194596574ull, + 2540222023230692795ull, 16463834114134690168ull, - 8941018855024971093ull, - 2898399952537902865ull, + 7277266162097189602ull, + 13929697350787785222ull, 8964175343669296125ull, - 2898399952537902865ull, + 13929697350787785222ull, }; // expectation corrected (k_magnifier, 7 of 7 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19): // 10072028075855707823ull -> 2055499121853794215ull @@ -391,14 +396,19 @@ constexpr std::uint64_t k_chart[] = { // 15984354419168920510ull -> 13581436790443683042ull // 16607038826027722339ull -> 14737345821341801219ull // 15984354419168920510ull -> 13581436790443683042ull +// expectation corrected (INT26 v19 hash re-pin, k_magnifier, 4 of 7 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary: +// 14738067743722080804ull -> 10187142414804555211ull [H-THIN] +// 2917741806179085516ull -> 13316113226488762715ull [H-THIN] +// 13581436790443683042ull -> 489358674406674971ull [H-THIN] +// 13581436790443683042ull -> 489358674406674971ull [H-THIN] constexpr std::uint64_t k_magnifier[] = { 4430826578646987126ull, - 14738067743722080804ull, + 10187142414804555211ull, 2077753189921542836ull, - 2917741806179085516ull, - 13581436790443683042ull, + 13316113226488762715ull, + 489358674406674971ull, 14737345821341801219ull, - 13581436790443683042ull, + 489358674406674971ull, }; // expectation corrected (k_aggregated, 5 of 5 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19): // 15579341700060222430ull -> 7408940017323371281ull @@ -421,12 +431,16 @@ constexpr std::uint64_t k_magnifier[] = { // 4995545673543620333ull -> 6906214955679447270ull // 9893483468656556008ull -> 10543459672655655434ull // 4995545673543620333ull -> 6906214955679447270ull +// expectation corrected (INT26 v19 hash re-pin, k_aggregated, 3 of 5 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary: +// 13031512803108236715ull -> 12703961494282243636ull [H-THIN] +// 6906214955679447270ull -> 4636245846172056503ull [H-THIN] +// 6906214955679447270ull -> 4636245846172056503ull [H-THIN] constexpr std::uint64_t k_aggregated[] = { 16542564830293500828ull, - 13031512803108236715ull, - 6906214955679447270ull, + 12703961494282243636ull, + 4636245846172056503ull, 10543459672655655434ull, - 6906214955679447270ull, + 4636245846172056503ull, }; // expectation corrected (k_aggregated_magnifier, 5 of 5 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19): // 12594241837591534357ull -> 2376229218543853263ull @@ -453,12 +467,16 @@ constexpr std::uint64_t k_aggregated[] = { // 10607093485200722581ull -> 7363580706773703120ull // 22607172627576334ull -> 5380311451522306220ull // 22607172627576334ull -> 5380311451522306220ull +// expectation corrected (INT26 v19 hash re-pin, k_aggregated_magnifier, 3 of 5 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary: +// 7363580706773703120ull -> 6822321611223800121ull [H-THIN] +// 5380311451522306220ull -> 1968062868302397607ull [H-THIN] +// 5380311451522306220ull -> 1968062868302397607ull [H-THIN] constexpr std::uint64_t k_aggregated_magnifier[] = { 14031106247497767919ull, - 7363580706773703120ull, - 5380311451522306220ull, + 6822321611223800121ull, + 1968062868302397607ull, 12265832737220737843ull, - 5380311451522306220ull, + 1968062868302397607ull, }; // expectation corrected (k_coof, 10 of 10 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19): // 10454171698854540065ull -> 8160477046829279565ull @@ -507,17 +525,23 @@ constexpr std::uint64_t k_aggregated_magnifier[] = { // 10916717879615654635ull -> 784137025905909089ull // 17916071608255404005ull -> 11981249830754474578ull // 10916717879615654635ull -> 784137025905909089ull +// expectation corrected (INT26 v19 hash re-pin, k_coof, 5 of 10 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary: +// 1856715264914641232ull -> 17725898018428294595ull [H-THIN] +// 9115044940243703863ull -> 8234685298969568092ull [PAR-ORDERS-2, H-THIN] +// 9198850018143016781ull -> 13907186164657073030ull [H-THIN] +// 784137025905909089ull -> 5915100548425272856ull [H-THIN] +// 784137025905909089ull -> 5915100548425272856ull [H-THIN] constexpr std::uint64_t k_coof[] = { 16820608513549156800ull, 9462991605602344175ull, 12220673217690837314ull, - 1856715264914641232ull, + 17725898018428294595ull, 12811765853164186626ull, - 9115044940243703863ull, - 9198850018143016781ull, - 784137025905909089ull, + 8234685298969568092ull, + 13907186164657073030ull, + 5915100548425272856ull, 11981249830754474578ull, - 784137025905909089ull, + 5915100548425272856ull, }; // expectation corrected (k_recording, 71 of 72 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19): // 2196150024938657215ull -> 2543779130970217232ull @@ -857,78 +881,147 @@ constexpr std::uint64_t k_coof[] = { // 5548300568428407565ull -> 4010960734010157804ull // 4191735526950747220ull -> 5172512134487820157ull // 4370795498617211977ull -> 2898399952537902865ull +// expectation corrected (INT26 v19 hash re-pin, k_recording, 68 of 72 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary: +// 16309388099732889217ull -> 13909724611150684752ull [H-THIN] +// 14697449118452563264ull -> 6058006111773907271ull [H-THIN] +// 2898399952537902865ull -> 13929697350787785222ull [H-THIN] +// 2898399952537902865ull -> 13929697350787785222ull [H-THIN] +// 11912929120179152844ull -> 14971695095207148331ull [H-THIN] +// 7111499071038382551ull -> 10535748930949530126ull [H-THIN] +// 14395892391241930212ull -> 12991008539092747157ull [H-THIN] +// 9034635987304089269ull -> 15956762843095239782ull [H-THIN] +// 7745035282290493167ull -> 2607245150229644476ull [H-THIN] +// 9653978480792136139ull -> 2184445209065093070ull [H-THIN] +// 6575784925310671623ull -> 2897664079743275822ull [H-THIN] +// 16756306850270906652ull -> 4157996150621490761ull [H-THIN] +// 6791844355462743637ull -> 10537503143714109070ull [H-THIN] +// 9752175874850418240ull -> 13754546407945448507ull [H-THIN] +// 5581794927458177019ull -> 16440431375654315850ull [H-THIN] +// 16367531041072987671ull -> 7003661191077414576ull [H-THIN] +// 5499206076623092635ull -> 11353048097065184288ull [H-THIN] +// 16891823664600062531ull -> 12848736740973846400ull [H-THIN] +// 15306266440153708710ull -> 3161842879106046797ull [H-THIN] +// 8382791724770590634ull -> 10847056918133263985ull [H-THIN] +// 4926336483410460532ull -> 8350647617751486789ull [H-THIN] +// 12823090459845782834ull -> 1856794670313281419ull [H-THIN] +// 10217177223946990874ull -> 3189033237966511301ull [H-THIN] +// 12680312482263936655ull -> 9556919139953515754ull [H-THIN] +// 15032085204869760060ull -> 5681429441061062241ull [H-THIN] +// 7696336452231268012ull -> 10425853480784439841ull [H-THIN] +// 6819477884530924764ull -> 12097190747565164239ull [H-THIN] +// 13932522791731114426ull -> 12637836836690018377ull [H-THIN] +// 15842817164955438950ull -> 14424918618138801803ull [H-THIN] +// 4250764768037495411ull -> 17903030095845904958ull [H-THIN] +// 1760469320874157114ull -> 15510962773836543503ull [H-THIN] +// 7565330933885892856ull -> 16031650111081263649ull [H-THIN] +// 3348650657119335616ull -> 15453192344610479715ull [H-THIN] +// 16026642042438818681ull -> 12604736624722151438ull [H-THIN] +// 15336121108426360137ull -> 17308957480242505390ull [H-THIN] +// 12610087835245307199ull -> 452142121530738962ull [H-THIN] +// 3169655936610324835ull -> 6150193546982553982ull [H-THIN] +// 18005345939863936504ull -> 12917335185226580263ull [H-THIN] +// 10374887129263366256ull -> 3249697982624166735ull [H-THIN] +// 3316605285128650350ull -> 3343978410969230181ull [H-THIN] +// 10411230090089985874ull -> 16247831777829745607ull [H-THIN] +// 2321647345879300563ull -> 7514289197254818298ull [H-THIN] +// 9282855342318040804ull -> 17972948283988009341ull [H-THIN] +// 8782939554936041465ull -> 4872902272073809254ull [H-THIN] +// 9270809468617755726ull -> 16118270799549857939ull [H-THIN] +// 9705564861311910669ull -> 14672005653726242528ull [H-THIN] +// 1984027035687621850ull -> 2133921838620278979ull [H-THIN] +// 5309816640440296119ull -> 13677532362142245502ull [H-THIN] +// 61906512444861614ull -> 7883065542401673469ull [H-THIN] +// 18408253739502061131ull -> 7126839422434123000ull [H-THIN] +// 2900636347809437670ull -> 3907128035298066349ull [H-THIN] +// 8590510826456084479ull -> 4948527793100784708ull [H-THIN] +// 16739036358771975930ull -> 142644456983149613ull [H-THIN] +// 3846505090147632664ull -> 14555917326272639085ull [H-THIN] +// 13346518438036570112ull -> 5212018862612370299ull [H-THIN] +// 3939181335635768762ull -> 865072719653853777ull [H-THIN] +// 1996675722330704212ull -> 14621783374817857573ull [H-THIN] +// 271113896427528122ull -> 12663926541947072239ull [H-THIN] +// 1511893874670543988ull -> 7631475035369278017ull [H-THIN] +// 15973548105797383183ull -> 951171557373679674ull [H-THIN] +// 4392899708297163730ull -> 9891512222295021637ull [H-THIN] +// 11467883812317724901ull -> 14067535954397498792ull [H-THIN] +// 11051413718335140252ull -> 14311549026741467081ull [H-THIN] +// 903533060614176790ull -> 775415503723595045ull [H-THIN] +// 14466209299323094098ull -> 2025287021890762573ull [H-THIN] +// 4010960734010157804ull -> 5576052922734185379ull [H-THIN] +// 5172512134487820157ull -> 1575805364117614574ull [H-THIN] +// 2898399952537902865ull -> 13929697350787785222ull [H-THIN] constexpr std::uint64_t k_recording[] = { 5575980828577033360ull, - 16309388099732889217ull, + 13909724611150684752ull, 16463834114134690168ull, - 14697449118452563264ull, - 2898399952537902865ull, + 6058006111773907271ull, + 13929697350787785222ull, 8964175343669296125ull, - 2898399952537902865ull, - 11912929120179152844ull, - 7111499071038382551ull, - 14395892391241930212ull, - 9034635987304089269ull, - 7745035282290493167ull, - 9653978480792136139ull, - 6575784925310671623ull, - 16756306850270906652ull, - 6791844355462743637ull, - 9752175874850418240ull, - 5581794927458177019ull, - 16367531041072987671ull, - 5499206076623092635ull, - 16891823664600062531ull, - 15306266440153708710ull, - 8382791724770590634ull, - 4926336483410460532ull, - 12823090459845782834ull, - 10217177223946990874ull, - 12680312482263936655ull, - 15032085204869760060ull, - 7696336452231268012ull, - 6819477884530924764ull, - 13932522791731114426ull, - 15842817164955438950ull, - 4250764768037495411ull, - 1760469320874157114ull, - 7565330933885892856ull, - 3348650657119335616ull, - 16026642042438818681ull, - 15336121108426360137ull, - 12610087835245307199ull, - 3169655936610324835ull, - 18005345939863936504ull, - 10374887129263366256ull, - 3316605285128650350ull, - 10411230090089985874ull, - 2321647345879300563ull, - 9282855342318040804ull, - 8782939554936041465ull, - 9270809468617755726ull, - 9705564861311910669ull, - 1984027035687621850ull, - 5309816640440296119ull, - 61906512444861614ull, - 18408253739502061131ull, - 2900636347809437670ull, - 8590510826456084479ull, - 16739036358771975930ull, - 3846505090147632664ull, - 13346518438036570112ull, - 3939181335635768762ull, - 1996675722330704212ull, - 271113896427528122ull, - 1511893874670543988ull, - 15973548105797383183ull, - 4392899708297163730ull, - 11467883812317724901ull, - 11051413718335140252ull, - 903533060614176790ull, - 14466209299323094098ull, - 4010960734010157804ull, - 5172512134487820157ull, - 2898399952537902865ull, + 13929697350787785222ull, + 14971695095207148331ull, + 10535748930949530126ull, + 12991008539092747157ull, + 15956762843095239782ull, + 2607245150229644476ull, + 2184445209065093070ull, + 2897664079743275822ull, + 4157996150621490761ull, + 10537503143714109070ull, + 13754546407945448507ull, + 16440431375654315850ull, + 7003661191077414576ull, + 11353048097065184288ull, + 12848736740973846400ull, + 3161842879106046797ull, + 10847056918133263985ull, + 8350647617751486789ull, + 1856794670313281419ull, + 3189033237966511301ull, + 9556919139953515754ull, + 5681429441061062241ull, + 10425853480784439841ull, + 12097190747565164239ull, + 12637836836690018377ull, + 14424918618138801803ull, + 17903030095845904958ull, + 15510962773836543503ull, + 16031650111081263649ull, + 15453192344610479715ull, + 12604736624722151438ull, + 17308957480242505390ull, + 452142121530738962ull, + 6150193546982553982ull, + 12917335185226580263ull, + 3249697982624166735ull, + 3343978410969230181ull, + 16247831777829745607ull, + 7514289197254818298ull, + 17972948283988009341ull, + 4872902272073809254ull, + 16118270799549857939ull, + 14672005653726242528ull, + 2133921838620278979ull, + 13677532362142245502ull, + 7883065542401673469ull, + 7126839422434123000ull, + 3907128035298066349ull, + 4948527793100784708ull, + 142644456983149613ull, + 14555917326272639085ull, + 5212018862612370299ull, + 865072719653853777ull, + 14621783374817857573ull, + 12663926541947072239ull, + 7631475035369278017ull, + 951171557373679674ull, + 9891512222295021637ull, + 14067535954397498792ull, + 14311549026741467081ull, + 775415503723595045ull, + 2025287021890762573ull, + 5576052922734185379ull, + 1575805364117614574ull, + 13929697350787785222ull, 8ull, }; // expectation corrected (k_stream, 54 of 54 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19): @@ -1191,61 +1284,101 @@ constexpr std::uint64_t k_recording[] = { // 8444087394523764362ull -> 971242319250035563ull // 14079671624423886806ull -> 6931684686417541656ull // 8444087394523764362ull -> 971242319250035563ull +// expectation corrected (INT26 v19 hash re-pin, k_stream, 39 of 54 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary: +// 16458853630937199693ull -> 6675987432147888000ull [H-THIN] +// 15852567477617018112ull -> 15316836834528354089ull [H-THIN] +// 8395254103899656747ull -> 122347678355491576ull [H-THIN] +// 9137153746257222586ull -> 17060189170921966279ull [H-THIN] +// 4321243615010347791ull -> 12998233506120003610ull [H-THIN] +// 4335032133758631355ull -> 5790160451626852554ull [H-THIN] +// 3418642895666580588ull -> 8747488118396803171ull [H-THIN] +// 15778603109418817912ull -> 11301555486053984887ull [H-THIN] +// 8082887755714141501ull -> 16019190197976852534ull [H-THIN] +// 18272796975887582060ull -> 14554266247005678391ull [H-THIN] +// 4620349392656757831ull -> 9863308761378043570ull [H-THIN] +// 11681022048666484124ull -> 3981212442403576087ull [H-THIN] +// 1110410394930231379ull -> 16893712934576968012ull [H-THIN] +// 8353289200898942510ull -> 17103134812120181635ull [H-THIN] +// 5464661787014092283ull -> 2716679541586828074ull [H-THIN] +// 17913569146247237990ull -> 2142815117417671925ull [H-THIN] +// 14252084119842247317ull -> 1297101144475427900ull [H-THIN] +// 14984524306684102418ull -> 17860190683404402179ull [H-THIN] +// 8427843384471180011ull -> 12743930332679229528ull [H-THIN] +// 12312569890916686665ull -> 13672064593010625780ull [H-THIN] +// 7806471840435792838ull -> 15322331001617686957ull [H-THIN] +// 10841176947711978698ull -> 2540452936526860029ull [H-THIN] +// 15796138674937755121ull -> 5707160760001918266ull [H-THIN] +// 4930499034900633496ull -> 17256519628932910437ull [H-THIN] +// 11894749826123257324ull -> 1265742856577179629ull [H-THIN] +// 6209022505833017115ull -> 8935675646451046114ull [H-THIN] +// 13004202639896233962ull -> 1995113508368758049ull [H-THIN] +// 1029059189446661931ull -> 12126669220334860080ull [H-THIN] +// 9236479431486641300ull -> 13100650686759242355ull [H-THIN] +// 5209128789544115679ull -> 1320523439935521014ull [H-THIN] +// 5582126543454279956ull -> 10835572324283436971ull [H-THIN] +// 16435999169585034112ull -> 411402308123821837ull [H-THIN] +// 2858404675926398753ull -> 18325644631502656152ull [H-THIN] +// 2863207613356019619ull -> 6533533400826708814ull [H-THIN] +// 1651019462700250297ull -> 11035145778810736216ull [H-THIN] +// 9342015881796079139ull -> 4664106270365911266ull [H-THIN] +// 971242319250035563ull -> 6512545530849110454ull [H-THIN] +// 971242319250035563ull -> 6512545530849110454ull [H-THIN] +// 971242319250035563ull -> 6512545530849110454ull [H-THIN] constexpr std::uint64_t k_stream[] = { - 16458853630937199693ull, + 6675987432147888000ull, 10534014544629119321ull, - 15852567477617018112ull, - 8395254103899656747ull, - 9137153746257222586ull, - 4321243615010347791ull, + 15316836834528354089ull, + 122347678355491576ull, + 17060189170921966279ull, + 12998233506120003610ull, 2464381025353933752ull, - 4335032133758631355ull, - 3418642895666580588ull, + 5790160451626852554ull, + 8747488118396803171ull, 12043850726481513634ull, - 15778603109418817912ull, + 11301555486053984887ull, 7422979898823356625ull, - 8082887755714141501ull, - 18272796975887582060ull, - 4620349392656757831ull, + 16019190197976852534ull, + 14554266247005678391ull, + 9863308761378043570ull, 11062470475981718812ull, - 11681022048666484124ull, - 1110410394930231379ull, - 8353289200898942510ull, - 5464661787014092283ull, + 3981212442403576087ull, + 16893712934576968012ull, + 17103134812120181635ull, + 2716679541586828074ull, 18042314908697358845ull, - 17913569146247237990ull, - 14252084119842247317ull, + 2142815117417671925ull, + 1297101144475427900ull, 13951402111806527659ull, - 14984524306684102418ull, + 17860190683404402179ull, 4929388494852177725ull, - 8427843384471180011ull, - 12312569890916686665ull, - 7806471840435792838ull, - 10841176947711978698ull, + 12743930332679229528ull, + 13672064593010625780ull, + 15322331001617686957ull, + 2540452936526860029ull, 14141658027994245481ull, - 15796138674937755121ull, - 4930499034900633496ull, - 11894749826123257324ull, + 5707160760001918266ull, + 17256519628932910437ull, + 1265742856577179629ull, 331885767850924737ull, - 6209022505833017115ull, - 13004202639896233962ull, - 1029059189446661931ull, + 8935675646451046114ull, + 1995113508368758049ull, + 12126669220334860080ull, 9179914352160302837ull, - 9236479431486641300ull, - 5209128789544115679ull, - 5582126543454279956ull, + 13100650686759242355ull, + 1320523439935521014ull, + 10835572324283436971ull, 14370442711223214011ull, - 16435999169585034112ull, - 2858404675926398753ull, - 2863207613356019619ull, + 411402308123821837ull, + 18325644631502656152ull, + 6533533400826708814ull, 4197841241754323017ull, - 1651019462700250297ull, - 9342015881796079139ull, - 971242319250035563ull, + 11035145778810736216ull, + 4664106270365911266ull, + 6512545530849110454ull, 6367780246848246992ull, - 971242319250035563ull, + 6512545530849110454ull, 6931684686417541656ull, - 971242319250035563ull, + 6512545530849110454ull, }; // expectation corrected (k_stream_aborted, 35 of 35 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19): // 1675429297538291515ull -> 9926360377267477400ull @@ -1417,42 +1550,68 @@ constexpr std::uint64_t k_stream[] = { // 12788838416146221578ull -> 4930499034900633496ull // 6600350751158217016ull -> 6596242839819495123ull // 12788838416146221578ull -> 4930499034900633496ull +// expectation corrected (INT26 v19 hash re-pin, k_stream_aborted, 25 of 35 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary: +// 16458853630937199693ull -> 6675987432147888000ull [H-THIN] +// 15852567477617018112ull -> 15316836834528354089ull [H-THIN] +// 8395254103899656747ull -> 122347678355491576ull [H-THIN] +// 9137153746257222586ull -> 17060189170921966279ull [H-THIN] +// 4321243615010347791ull -> 12998233506120003610ull [H-THIN] +// 4335032133758631355ull -> 5790160451626852554ull [H-THIN] +// 3418642895666580588ull -> 8747488118396803171ull [H-THIN] +// 15778603109418817912ull -> 11301555486053984887ull [H-THIN] +// 8082887755714141501ull -> 16019190197976852534ull [H-THIN] +// 18272796975887582060ull -> 14554266247005678391ull [H-THIN] +// 4620349392656757831ull -> 9863308761378043570ull [H-THIN] +// 11681022048666484124ull -> 3981212442403576087ull [H-THIN] +// 1110410394930231379ull -> 16893712934576968012ull [H-THIN] +// 8353289200898942510ull -> 17103134812120181635ull [H-THIN] +// 5464661787014092283ull -> 2716679541586828074ull [H-THIN] +// 17913569146247237990ull -> 2142815117417671925ull [H-THIN] +// 14252084119842247317ull -> 1297101144475427900ull [H-THIN] +// 14984524306684102418ull -> 17860190683404402179ull [H-THIN] +// 8427843384471180011ull -> 12743930332679229528ull [H-THIN] +// 12312569890916686665ull -> 13672064593010625780ull [H-THIN] +// 7806471840435792838ull -> 15322331001617686957ull [H-THIN] +// 10841176947711978698ull -> 2540452936526860029ull [H-THIN] +// 15796138674937755121ull -> 5707160760001918266ull [H-THIN] +// 4930499034900633496ull -> 17256519628932910437ull [H-THIN] +// 4930499034900633496ull -> 17256519628932910437ull [H-THIN] constexpr std::uint64_t k_stream_aborted[] = { - 16458853630937199693ull, + 6675987432147888000ull, 10534014544629119321ull, - 15852567477617018112ull, - 8395254103899656747ull, - 9137153746257222586ull, - 4321243615010347791ull, + 15316836834528354089ull, + 122347678355491576ull, + 17060189170921966279ull, + 12998233506120003610ull, 2464381025353933752ull, - 4335032133758631355ull, - 3418642895666580588ull, + 5790160451626852554ull, + 8747488118396803171ull, 12043850726481513634ull, - 15778603109418817912ull, + 11301555486053984887ull, 7422979898823356625ull, - 8082887755714141501ull, - 18272796975887582060ull, - 4620349392656757831ull, + 16019190197976852534ull, + 14554266247005678391ull, + 9863308761378043570ull, 11062470475981718812ull, - 11681022048666484124ull, - 1110410394930231379ull, - 8353289200898942510ull, - 5464661787014092283ull, + 3981212442403576087ull, + 16893712934576968012ull, + 17103134812120181635ull, + 2716679541586828074ull, 18042314908697358845ull, - 17913569146247237990ull, - 14252084119842247317ull, + 2142815117417671925ull, + 1297101144475427900ull, 13951402111806527659ull, - 14984524306684102418ull, + 17860190683404402179ull, 4929388494852177725ull, - 8427843384471180011ull, - 12312569890916686665ull, - 7806471840435792838ull, - 10841176947711978698ull, + 12743930332679229528ull, + 13672064593010625780ull, + 15322331001617686957ull, + 2540452936526860029ull, 14141658027994245481ull, - 15796138674937755121ull, - 4930499034900633496ull, + 5707160760001918266ull, + 17256519628932910437ull, 6596242839819495123ull, - 4930499034900633496ull, + 17256519628932910437ull, }; // expectation corrected (k_aborted, 6 of 6 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19): // 2196150024938657215ull -> 2543779130970217232ull @@ -1485,13 +1644,17 @@ constexpr std::uint64_t k_stream_aborted[] = { // 12415576349368044635ull -> 12754404695042992156ull // 14564863596030526264ull -> 8825354032181240290ull // 12415576349368044635ull -> 12754404695042992156ull +// expectation corrected (INT26 v19 hash re-pin, k_aborted, 3 of 6 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary: +// 17778912298194596574ull -> 2540222023230692795ull [H-THIN] +// 12754404695042992156ull -> 8965275888317139301ull [H-THIN] +// 12754404695042992156ull -> 8965275888317139301ull [H-THIN] constexpr std::uint64_t k_aborted[] = { 5575980828577033360ull, - 17778912298194596574ull, + 2540222023230692795ull, 16463834114134690168ull, - 12754404695042992156ull, + 8965275888317139301ull, 8825354032181240290ull, - 12754404695042992156ull, + 8965275888317139301ull, }; // expectation corrected (k_reused, 15 of 15 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19): // 2196150024938657215ull -> 2543779130970217232ull @@ -1565,22 +1728,32 @@ constexpr std::uint64_t k_aborted[] = { // 3498362567022607364ull -> 4620349392656757831ull // 9810780513184341561ull -> 7040750059417794475ull // 3498362567022607364ull -> 4620349392656757831ull +// expectation corrected (INT26 v19 hash re-pin, k_reused, 9 of 15 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary: +// 17778912298194596574ull -> 2540222023230692795ull [H-THIN] +// 8941018855024971093ull -> 7277266162097189602ull [H-THIN] +// 17778912298194596574ull -> 2540222023230692795ull [H-THIN] +// 8941018855024971093ull -> 7277266162097189602ull [H-THIN] +// 2898399952537902865ull -> 13929697350787785222ull [H-THIN] +// 2898399952537902865ull -> 13929697350787785222ull [H-THIN] +// 4063572268643314481ull -> 3045160088509885646ull [H-THIN] +// 4620349392656757831ull -> 9863308761378043570ull [H-THIN] +// 4620349392656757831ull -> 9863308761378043570ull [H-THIN] constexpr std::uint64_t k_reused[] = { 5575980828577033360ull, - 17778912298194596574ull, + 2540222023230692795ull, 16463834114134690168ull, - 8941018855024971093ull, + 7277266162097189602ull, 5575980828577033360ull, - 17778912298194596574ull, + 2540222023230692795ull, 16463834114134690168ull, - 8941018855024971093ull, - 2898399952537902865ull, + 7277266162097189602ull, + 13929697350787785222ull, 8964175343669296125ull, - 2898399952537902865ull, - 4063572268643314481ull, - 4620349392656757831ull, + 13929697350787785222ull, + 3045160088509885646ull, + 9863308761378043570ull, 7040750059417794475ull, - 4620349392656757831ull, + 9863308761378043570ull, }; // expectation corrected (k_refused, 11 of 11 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19): // 9578444851499107596ull -> 5623648009772522091ull @@ -1636,18 +1809,25 @@ constexpr std::uint64_t k_reused[] = { // 4370795498617211977ull -> 2898399952537902865ull // 10361542891827113526ull -> 8964175343669296125ull // 4370795498617211977ull -> 2898399952537902865ull +// expectation corrected (INT26 v19 hash re-pin, k_refused, 6 of 11 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary: +// 2898399952537902865ull -> 13929697350787785222ull [H-THIN] +// 2898399952537902865ull -> 13929697350787785222ull [H-THIN] +// 17778912298194596574ull -> 2540222023230692795ull [H-THIN] +// 8941018855024971093ull -> 7277266162097189602ull [H-THIN] +// 2898399952537902865ull -> 13929697350787785222ull [H-THIN] +// 2898399952537902865ull -> 13929697350787785222ull [H-THIN] constexpr std::uint64_t k_refused[] = { 8964175343669296125ull, - 2898399952537902865ull, + 13929697350787785222ull, 8964175343669296125ull, - 2898399952537902865ull, + 13929697350787785222ull, 5575980828577033360ull, - 17778912298194596574ull, + 2540222023230692795ull, 16463834114134690168ull, - 8941018855024971093ull, - 2898399952537902865ull, + 7277266162097189602ull, + 13929697350787785222ull, 8964175343669296125ull, - 2898399952537902865ull, + 13929697350787785222ull, }; // P1_PINNED_DATA_END diff --git a/tests/test_adapter_host_view_memo_data.hpp b/tests/test_adapter_host_view_memo_data.hpp index b7db1bf8..446b87ec 100644 --- a/tests/test_adapter_host_view_memo_data.hpp +++ b/tests/test_adapter_host_view_memo_data.hpp @@ -105,31 +105,46 @@ // rebuilt/1: hash=16942810041893704776 -> hash=5316201723780989919 // rebuilt/2: hash=13831511007419341774 -> hash=496593797863776142 // rebuilt/3: hash=16942810041893704776 -> hash=5316201723780989919 +// expectation corrected (INT26 v19 hash re-pin): every run's broker-state hash moved once more, because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; +// trades, trade digests, net profits and errors did not move: +// batch/A: hash=496593797863776142 -> hash=14113207875665531755 [H-THIN] +// batch/B: hash=5316201723780989919 -> hash=3001060687180726304 [H-THIN] +// stream/A: hash=4351362905685696693 -> hash=1927432040325474646 [H-THIN] +// stream/B: hash=1033336853289890500 -> hash=13106388348010577033 [H-THIN] +// interleaved/A: hash=4351362905685696693 -> hash=1927432040325474646 [H-THIN] +// interleaved/B: hash=1033336853289890500 -> hash=13106388348010577033 [H-THIN] +// round_robin/A1: hash=4351362905685696693 -> hash=1927432040325474646 [H-THIN] +// round_robin/B: hash=1033336853289890500 -> hash=13106388348010577033 [H-THIN] +// round_robin/A2: hash=4351362905685696693 -> hash=1927432040325474646 [H-THIN] +// rebuilt/0: hash=496593797863776142 -> hash=14113207875665531755 [H-THIN] +// rebuilt/1: hash=5316201723780989919 -> hash=3001060687180726304 [H-THIN] +// rebuilt/2: hash=496593797863776142 -> hash=14113207875665531755 [H-THIN] +// rebuilt/3: hash=5316201723780989919 -> hash=3001060687180726304 [H-THIN] constexpr Pinned kPinned[] = { {"batch/A", - "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=496593797863776142 error=''"}, + "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=14113207875665531755 error=''"}, {"batch/B", - "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=5316201723780989919 error=''"}, + "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=3001060687180726304 error=''"}, {"stream/A", - "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=4351362905685696693 error=''"}, + "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=1927432040325474646 error=''"}, {"stream/B", - "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=1033336853289890500 error=''"}, + "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=13106388348010577033 error=''"}, {"interleaved/A", - "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=4351362905685696693 error=''"}, + "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=1927432040325474646 error=''"}, {"interleaved/B", - "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=1033336853289890500 error=''"}, + "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=13106388348010577033 error=''"}, {"round_robin/A1", - "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=4351362905685696693 error=''"}, + "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=1927432040325474646 error=''"}, {"round_robin/B", - "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=1033336853289890500 error=''"}, + "trades=35 fnv=3eaaaacd90998d64 net=-306.89379320283734 hash=13106388348010577033 error=''"}, {"round_robin/A2", - "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=4351362905685696693 error=''"}, + "trades=23 fnv=a0e7b82126f9e8de net=17.321792890020646 hash=1927432040325474646 error=''"}, {"rebuilt/0", - "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=496593797863776142 error=''"}, + "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=14113207875665531755 error=''"}, {"rebuilt/1", - "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=5316201723780989919 error=''"}, + "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=3001060687180726304 error=''"}, {"rebuilt/2", - "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=496593797863776142 error=''"}, + "trades=23 fnv=e337022ac3d8b681 net=71.786509342348978 hash=14113207875665531755 error=''"}, {"rebuilt/3", - "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=5316201723780989919 error=''"}, + "trades=35 fnv=9fbd9e232cb9843a net=-268.32270548365625 hash=3001060687180726304 error=''"}, }; diff --git a/tests/test_adapter_lookup_index_witness_data.hpp b/tests/test_adapter_lookup_index_witness_data.hpp index 2941a67a..59056503 100644 --- a/tests/test_adapter_lookup_index_witness_data.hpp +++ b/tests/test_adapter_lookup_index_witness_data.hpp @@ -364,39 +364,136 @@ // 12276848500238226962ull -> 17288887437003769330ull // 1287632709215711658ull -> 3196584109952517226ull // 8142912162810668916ull -> 9440871734801450516ull +// expectation corrected (INT26 v19 hash re-pin, kFlip_rows, 96 of 96 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 15092016227729062170ull -> 2713710203384152283ull [H-THIN] +// 4449511944144175244ull -> 6223096899160842027ull [H-THIN] +// 14173574221892226517ull -> 8359657276913190150ull [H-THIN] +// 11141868197266883358ull -> 13333415662077313847ull [H-THIN] +// 5770620033420772121ull -> 2339692489009935305ull [H-THIN] +// 3157789451207414333ull -> 13881977959297861087ull [H-THIN] +// 15624036521156382865ull -> 6037739369311942191ull [H-THIN] +// 17987828433123328363ull -> 13360499968018975827ull [H-THIN] +// 3782132892539745074ull -> 3716214627590530178ull [H-THIN] +// 14406538727996760883ull -> 10595879371001202161ull [H-THIN] +// 14845895464439448971ull -> 17310026037100513916ull [H-THIN] +// 14487807311329646997ull -> 12667453818951594687ull [H-THIN] +// 14748183486665371140ull -> 2079604483202772421ull [H-THIN] +// 12768807557967508254ull -> 3113949815737359904ull [H-THIN] +// 9713166925369100573ull -> 3920341595463994402ull [H-THIN] +// 16480235611441211183ull -> 9084177151567397255ull [H-THIN] +// 6732375251738161555ull -> 18359863285932392610ull [H-THIN] +// 9492220356384667994ull -> 17884599320337535118ull [H-THIN] +// 7836886659987736698ull -> 17357906624164980943ull [H-THIN] +// 11041759722035017537ull -> 7307330041717258533ull [H-THIN] +// 1923076241747348592ull -> 17225382223780507170ull [H-THIN] +// 16491371029791645211ull -> 10189175013051411098ull [H-THIN] +// 1800320518007831877ull -> 12861484300776397436ull [H-THIN] +// 571647502003500868ull -> 18158906195875357228ull [H-THIN] +// 2739724503975357253ull -> 668263264598955855ull [H-THIN] +// 9819507431665488276ull -> 12373521030877882901ull [H-THIN] +// 9945110324091627954ull -> 13472266762540203511ull [H-THIN] +// 11949472718108945819ull -> 1520792362654463139ull [H-THIN] +// 9762923116876284513ull -> 8057949129317313833ull [H-THIN] +// 18027851627748731010ull -> 7504246145768874050ull [H-THIN] +// 1366892124494590874ull -> 14499427008266620636ull [H-THIN] +// 14363491720205864892ull -> 12041531785093130716ull [H-THIN] +// 5814793330491118429ull -> 1801834117749396818ull [H-THIN] +// 14054560180567661527ull -> 17865123298655850846ull [H-THIN] +// 7046697962876325595ull -> 1083628892442129306ull [H-THIN] +// 13964478754584930616ull -> 8913360708937979364ull [H-THIN] +// 10828030611661121392ull -> 3270649769713143173ull [H-THIN] +// 16763687418918419771ull -> 2058499959797060144ull [H-THIN] +// 14089257237702634390ull -> 3618662235985025981ull [H-THIN] +// 4126250144527807077ull -> 10418131399291610085ull [H-THIN] +// 18195401520799288599ull -> 5976888785228459955ull [H-THIN] +// 1257461765259766246ull -> 5642573737457759491ull [H-THIN] +// 859001078874563294ull -> 8755896700597066515ull [H-THIN] +// 4425771211202385044ull -> 9228163713756312671ull [H-THIN] +// 11354019857073859374ull -> 16057503475246941040ull [H-THIN] +// 14877394547945152326ull -> 9166486181684479621ull [H-THIN] +// 6393077053177388522ull -> 12196317256432235408ull [H-THIN] +// 7888781154589438239ull -> 8248464056444717792ull [H-THIN] +// 6757741011218066608ull -> 15765659994617767915ull [H-THIN] +// 6658586324753656657ull -> 6741853983312669853ull [H-THIN] +// 4423113805183200923ull -> 10896023542341293900ull [H-THIN] +// 16281985077084422619ull -> 13744950457412991552ull [H-THIN] +// 1045183813508792423ull -> 2675255887306542834ull [H-THIN] +// 9313942597962496343ull -> 5645825323621631733ull [H-THIN] +// 12979192255068283250ull -> 3920811839771533221ull [H-THIN] +// 4141974475510294052ull -> 7755538476974379703ull [H-THIN] +// 7255579447356474699ull -> 13132499480713676622ull [H-THIN] +// 11500737887308332433ull -> 5764644161685668523ull [H-THIN] +// 11396364648984166081ull -> 15355218105878313483ull [H-THIN] +// 17999943307750433945ull -> 5253835154794121854ull [H-THIN] +// 1207486754606428343ull -> 16356433441734928501ull [H-THIN] +// 13698332948978760389ull -> 9284859115909895525ull [H-THIN] +// 11589549934838942681ull -> 10869385438384466871ull [H-THIN] +// 9964006416615122225ull -> 2981556363023708914ull [H-THIN] +// 16845154769434317988ull -> 18170305916189617187ull [H-THIN] +// 7334258627544164572ull -> 4703464358355960681ull [H-THIN] +// 4908786545179256930ull -> 580923567071968203ull [H-THIN] +// 3299406129086178611ull -> 13542563271263616250ull [H-THIN] +// 5312427811860831678ull -> 1241688105587480446ull [H-THIN] +// 11839292439534512009ull -> 18195118200181923889ull [H-THIN] +// 10875737639895929077ull -> 18177172509460860789ull [H-THIN] +// 11779092014895227520ull -> 15332939137911226852ull [H-THIN] +// 4793765047901110384ull -> 12867779665010719103ull [H-THIN] +// 1012289705290011692ull -> 6631355797698381202ull [H-THIN] +// 1667267280248270342ull -> 6751318601181665851ull [H-THIN] +// 8455992061434657085ull -> 15964807147277222246ull [H-THIN] +// 15957521919212433920ull -> 410299832240558536ull [H-THIN] +// 10969413931083416374ull -> 4534261760619620647ull [H-THIN] +// 7170250270476017946ull -> 12405100549638781593ull [H-THIN] +// 15006273935975173912ull -> 1594024600982885699ull [H-THIN] +// 11184968259335349501ull -> 5379847191493293482ull [H-THIN] +// 6276009473983766703ull -> 16387988109738828311ull [H-THIN] +// 1757274001997493069ull -> 8185426235741655660ull [H-THIN] +// 7772193038207188421ull -> 3218474949250087435ull [H-THIN] +// 15686643752958926138ull -> 15335138187047848164ull [H-THIN] +// 17800105557473441299ull -> 15994853274085433009ull [H-THIN] +// 5863137483546792400ull -> 15487434336049414427ull [H-THIN] +// 7816936454583767258ull -> 1769663901820159206ull [H-THIN] +// 6161705640611834793ull -> 15148139558826312238ull [H-THIN] +// 7780124240324826458ull -> 8665254425768362077ull [H-THIN] +// 13353384012423321986ull -> 10696940446584417573ull [H-THIN] +// 9198310857862468197ull -> 6408081173200491198ull [H-THIN] +// 388071097687117133ull -> 325415082935288696ull [H-THIN] +// 17288887437003769330ull -> 16992578023938135234ull [H-THIN] +// 3196584109952517226ull -> 1363812141911607255ull [H-THIN] +// 9440871734801450516ull -> 7381600230201732590ull [H-THIN] constexpr std::uint64_t kFlip_rows[] = { - 15092016227729062170ull, 4449511944144175244ull, 14173574221892226517ull, - 11141868197266883358ull, 5770620033420772121ull, 3157789451207414333ull, - 15624036521156382865ull, 17987828433123328363ull, 3782132892539745074ull, - 14406538727996760883ull, 14845895464439448971ull, 14487807311329646997ull, - 14748183486665371140ull, 12768807557967508254ull, 9713166925369100573ull, - 16480235611441211183ull, 6732375251738161555ull, 9492220356384667994ull, - 7836886659987736698ull, 11041759722035017537ull, 1923076241747348592ull, - 16491371029791645211ull, 1800320518007831877ull, 571647502003500868ull, - 2739724503975357253ull, 9819507431665488276ull, 9945110324091627954ull, - 11949472718108945819ull, 9762923116876284513ull, 18027851627748731010ull, - 1366892124494590874ull, 14363491720205864892ull, 5814793330491118429ull, - 14054560180567661527ull, 7046697962876325595ull, 13964478754584930616ull, - 10828030611661121392ull, 16763687418918419771ull, 14089257237702634390ull, - 4126250144527807077ull, 18195401520799288599ull, 1257461765259766246ull, - 859001078874563294ull, 4425771211202385044ull, 11354019857073859374ull, - 14877394547945152326ull, 6393077053177388522ull, 7888781154589438239ull, - 6757741011218066608ull, 6658586324753656657ull, 4423113805183200923ull, - 16281985077084422619ull, 1045183813508792423ull, 9313942597962496343ull, - 12979192255068283250ull, 4141974475510294052ull, 7255579447356474699ull, - 11500737887308332433ull, 11396364648984166081ull, 17999943307750433945ull, - 1207486754606428343ull, 13698332948978760389ull, 11589549934838942681ull, - 9964006416615122225ull, 16845154769434317988ull, 7334258627544164572ull, - 4908786545179256930ull, 3299406129086178611ull, 5312427811860831678ull, - 11839292439534512009ull, 10875737639895929077ull, 11779092014895227520ull, - 4793765047901110384ull, 1012289705290011692ull, 1667267280248270342ull, - 8455992061434657085ull, 15957521919212433920ull, 10969413931083416374ull, - 7170250270476017946ull, 15006273935975173912ull, 11184968259335349501ull, - 6276009473983766703ull, 1757274001997493069ull, 7772193038207188421ull, - 15686643752958926138ull, 17800105557473441299ull, 5863137483546792400ull, - 7816936454583767258ull, 6161705640611834793ull, 7780124240324826458ull, - 13353384012423321986ull, 9198310857862468197ull, 388071097687117133ull, - 17288887437003769330ull, 3196584109952517226ull, 9440871734801450516ull, + 2713710203384152283ull, 6223096899160842027ull, 8359657276913190150ull, + 13333415662077313847ull, 2339692489009935305ull, 13881977959297861087ull, + 6037739369311942191ull, 13360499968018975827ull, 3716214627590530178ull, + 10595879371001202161ull, 17310026037100513916ull, 12667453818951594687ull, + 2079604483202772421ull, 3113949815737359904ull, 3920341595463994402ull, + 9084177151567397255ull, 18359863285932392610ull, 17884599320337535118ull, + 17357906624164980943ull, 7307330041717258533ull, 17225382223780507170ull, + 10189175013051411098ull, 12861484300776397436ull, 18158906195875357228ull, + 668263264598955855ull, 12373521030877882901ull, 13472266762540203511ull, + 1520792362654463139ull, 8057949129317313833ull, 7504246145768874050ull, + 14499427008266620636ull, 12041531785093130716ull, 1801834117749396818ull, + 17865123298655850846ull, 1083628892442129306ull, 8913360708937979364ull, + 3270649769713143173ull, 2058499959797060144ull, 3618662235985025981ull, + 10418131399291610085ull, 5976888785228459955ull, 5642573737457759491ull, + 8755896700597066515ull, 9228163713756312671ull, 16057503475246941040ull, + 9166486181684479621ull, 12196317256432235408ull, 8248464056444717792ull, + 15765659994617767915ull, 6741853983312669853ull, 10896023542341293900ull, + 13744950457412991552ull, 2675255887306542834ull, 5645825323621631733ull, + 3920811839771533221ull, 7755538476974379703ull, 13132499480713676622ull, + 5764644161685668523ull, 15355218105878313483ull, 5253835154794121854ull, + 16356433441734928501ull, 9284859115909895525ull, 10869385438384466871ull, + 2981556363023708914ull, 18170305916189617187ull, 4703464358355960681ull, + 580923567071968203ull, 13542563271263616250ull, 1241688105587480446ull, + 18195118200181923889ull, 18177172509460860789ull, 15332939137911226852ull, + 12867779665010719103ull, 6631355797698381202ull, 6751318601181665851ull, + 15964807147277222246ull, 410299832240558536ull, 4534261760619620647ull, + 12405100549638781593ull, 1594024600982885699ull, 5379847191493293482ull, + 16387988109738828311ull, 8185426235741655660ull, 3218474949250087435ull, + 15335138187047848164ull, 15994853274085433009ull, 15487434336049414427ull, + 1769663901820159206ull, 15148139558826312238ull, 8665254425768362077ull, + 10696940446584417573ull, 6408081173200491198ull, 325415082935288696ull, + 16992578023938135234ull, 1363812141911607255ull, 7381600230201732590ull, }; // expectation corrected (kFlip_reads, 96 of 96 values), because v19's broker-state hash folds the closed rows as their count and a running digest, each row once when it is final (pineforge-broker-state/v19); this host projects one fixed execution hash, so the continuation does not enter; the trades did not move; re-harvested the same way on INT19's tree; V19-A's tip (6211dc94) gives the same rows: // 8609918845194998862ull -> 10296358092730895491ull @@ -758,45 +855,143 @@ constexpr std::uint64_t kFlip_rows[] = { // 9845813721495779641ull -> 7513550480889044889ull // 10443580480075697786ull -> 14297335183847666490ull // 5230704585495255452ull -> 3496786386085197052ull +// expectation corrected (INT26 v19 hash re-pin, kFlip_reads, 96 of 96 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 10304825573213448968ull -> 3114684496819061489ull [H-THIN] +// 14774003860189224946ull -> 6956326645770273433ull [H-THIN] +// 11801395281262233716ull -> 6687820258270235301ull [H-THIN] +// 8061934143473682151ull -> 12696019809107805010ull [H-THIN] +// 15983140295339383862ull -> 6271839642900128638ull [H-THIN] +// 5166653331022736661ull -> 17758335370804468308ull [H-THIN] +// 5250563911700661104ull -> 6246269702984932325ull [H-THIN] +// 7793104124436293557ull -> 756407504012262357ull [H-THIN] +// 1336784375798636377ull -> 10752289111609390933ull [H-THIN] +// 6339830337021579012ull -> 10702669286541029164ull [H-THIN] +// 16252798169114118083ull -> 3961413467939854492ull [H-THIN] +// 10937184901662388644ull -> 13925604324515645251ull [H-THIN] +// 18064999594731702480ull -> 11175435615622050442ull [H-THIN] +// 3546952347196282172ull -> 6776561790919030493ull [H-THIN] +// 13395340388338915323ull -> 577659037159878982ull [H-THIN] +// 8229077577636098658ull -> 10163490242105190374ull [H-THIN] +// 6541813106524409698ull -> 11276159262602451941ull [H-THIN] +// 25242829173903041ull -> 3717944350664878703ull [H-THIN] +// 8874377228534856759ull -> 17786578140867347109ull [H-THIN] +// 13648938798136741164ull -> 253867406630618996ull [H-THIN] +// 11200265295650397519ull -> 3554686122410655613ull [H-THIN] +// 11420272068366920678ull -> 17187792775761839863ull [H-THIN] +// 3753005678972800967ull -> 15223748191501073730ull [H-THIN] +// 4896870620618706776ull -> 4622823325308959265ull [H-THIN] +// 6895518920767105217ull -> 3947596021646594531ull [H-THIN] +// 7636324161499144341ull -> 18103100887606856772ull [H-THIN] +// 8785945007507579830ull -> 3330742851831023775ull [H-THIN] +// 5297575674933281534ull -> 12522884863273911920ull [H-THIN] +// 13537048838690550187ull -> 11078204221518180355ull [H-THIN] +// 16051941383930194236ull -> 7792565789695194874ull [H-THIN] +// 10552551845124524652ull -> 5837249087395420066ull [H-THIN] +// 15672638477538649241ull -> 15061096079220184405ull [H-THIN] +// 14086945637463962641ull -> 10949413619028940858ull [H-THIN] +// 10134206932178793443ull -> 3927414729140235852ull [H-THIN] +// 11512602377523606119ull -> 10786374148287235402ull [H-THIN] +// 2958147687598254910ull -> 13739564127065017562ull [H-THIN] +// 6458026266382303356ull -> 6792673025550256385ull [H-THIN] +// 1094964303920914155ull -> 9120117456385770000ull [H-THIN] +// 6484787112426264738ull -> 2702365990173828030ull [H-THIN] +// 12388725274527404683ull -> 14552218038038255731ull [H-THIN] +// 17043242768143307178ull -> 15974235985971979302ull [H-THIN] +// 5476835457134592009ull -> 5859075624711984363ull [H-THIN] +// 2025850526353442102ull -> 6107800350680289807ull [H-THIN] +// 1451154197906290714ull -> 6809837422484044317ull [H-THIN] +// 6635868355983485389ull -> 4235871646442648099ull [H-THIN] +// 7018381114706383243ull -> 2112314171124100852ull [H-THIN] +// 13248420308395735018ull -> 16908957968796171152ull [H-THIN] +// 774767130641043275ull -> 16595556766713987481ull [H-THIN] +// 13252007359081704531ull -> 17393639219555656528ull [H-THIN] +// 6750492291307654391ull -> 258880825276626691ull [H-THIN] +// 2031163009273496765ull -> 6502647070460437768ull [H-THIN] +// 9769809183116681670ull -> 9521652940428922473ull [H-THIN] +// 14493685479901302050ull -> 12532170261876847654ull [H-THIN] +// 4876420665985364975ull -> 7965261589232878523ull [H-THIN] +// 12501338337948389822ull -> 14891396965396161229ull [H-THIN] +// 14207736975056792147ull -> 8954363812786394023ull [H-THIN] +// 10604319388773724637ull -> 16427886737643839568ull [H-THIN] +// 1181294512016461575ull -> 14570810525240019901ull [H-THIN] +// 5062201717018553710ull -> 18385114195862288592ull [H-THIN] +// 5819082412660383228ull -> 356400710711660674ull [H-THIN] +// 4925793200130354182ull -> 6131866860079521894ull [H-THIN] +// 14181407321277195773ull -> 5473687205525959309ull [H-THIN] +// 18275617508396411315ull -> 1368982430503028604ull [H-THIN] +// 4666214838307866879ull -> 12966841479814647796ull [H-THIN] +// 13997906523391047231ull -> 11709399142050353312ull [H-THIN] +// 11459677085401802248ull -> 5078259748833979006ull [H-THIN] +// 5810149534385663204ull -> 12253705469518588621ull [H-THIN] +// 8487172727633256810ull -> 14449800309393374267ull [H-THIN] +// 7417382166705753312ull -> 11263108227153639840ull [H-THIN] +// 9462378120791609816ull -> 4291291545838288200ull [H-THIN] +// 12680325011617368327ull -> 10391124708572243222ull [H-THIN] +// 7441435328919647574ull -> 11045512779463427838ull [H-THIN] +// 13407951422463106989ull -> 14252271658091688198ull [H-THIN] +// 9986964831043767237ull -> 5864385547990959743ull [H-THIN] +// 14464180164790259585ull -> 10060761729093637760ull [H-THIN] +// 5220370176240914239ull -> 6012435413813366588ull [H-THIN] +// 3518323198598619449ull -> 4863606450928885329ull [H-THIN] +// 2133255899590215934ull -> 13441348122273594418ull [H-THIN] +// 3011009182271535838ull -> 2704170472265346213ull [H-THIN] +// 14836384915404578807ull -> 12941384362597894872ull [H-THIN] +// 9555956714003340276ull -> 10030988591835912271ull [H-THIN] +// 11964636267214528571ull -> 7895088774892271145ull [H-THIN] +// 16921397335982440924ull -> 8891008075855122089ull [H-THIN] +// 10586397832022517650ull -> 9575328812735763041ull [H-THIN] +// 18096821421422867234ull -> 15722680013241790924ull [H-THIN] +// 14108486707933080521ull -> 14330185982019726263ull [H-THIN] +// 2822797701958308088ull -> 3361192221763941904ull [H-THIN] +// 8446901108100253910ull -> 12098417980386469814ull [H-THIN] +// 9675426367378755701ull -> 5014147383869998166ull [H-THIN] +// 4615585398233598887ull -> 4177883635838955668ull [H-THIN] +// 4951618999197916175ull -> 7150992680430954116ull [H-THIN] +// 13485186398181682580ull -> 18395049852860007119ull [H-THIN] +// 2264266327693067886ull -> 17907122275511118531ull [H-THIN] +// 7513550480889044889ull -> 10212301139308990009ull [H-THIN] +// 14297335183847666490ull -> 17920870463788384967ull [H-THIN] +// 3496786386085197052ull -> 2787743584436740230ull [H-THIN] constexpr std::uint64_t kFlip_reads[] = { - 10304825573213448968ull, 14774003860189224946ull, 11801395281262233716ull, - 8061934143473682151ull, 15983140295339383862ull, 5166653331022736661ull, - 5250563911700661104ull, 7793104124436293557ull, 1336784375798636377ull, - 6339830337021579012ull, 16252798169114118083ull, 10937184901662388644ull, - 18064999594731702480ull, 3546952347196282172ull, 13395340388338915323ull, - 8229077577636098658ull, 6541813106524409698ull, 25242829173903041ull, - 8874377228534856759ull, 13648938798136741164ull, 11200265295650397519ull, - 11420272068366920678ull, 3753005678972800967ull, 4896870620618706776ull, - 6895518920767105217ull, 7636324161499144341ull, 8785945007507579830ull, - 5297575674933281534ull, 13537048838690550187ull, 16051941383930194236ull, - 10552551845124524652ull, 15672638477538649241ull, 14086945637463962641ull, - 10134206932178793443ull, 11512602377523606119ull, 2958147687598254910ull, - 6458026266382303356ull, 1094964303920914155ull, 6484787112426264738ull, - 12388725274527404683ull, 17043242768143307178ull, 5476835457134592009ull, - 2025850526353442102ull, 1451154197906290714ull, 6635868355983485389ull, - 7018381114706383243ull, 13248420308395735018ull, 774767130641043275ull, - 13252007359081704531ull, 6750492291307654391ull, 2031163009273496765ull, - 9769809183116681670ull, 14493685479901302050ull, 4876420665985364975ull, - 12501338337948389822ull, 14207736975056792147ull, 10604319388773724637ull, - 1181294512016461575ull, 5062201717018553710ull, 5819082412660383228ull, - 4925793200130354182ull, 14181407321277195773ull, 18275617508396411315ull, - 4666214838307866879ull, 13997906523391047231ull, 11459677085401802248ull, - 5810149534385663204ull, 8487172727633256810ull, 7417382166705753312ull, - 9462378120791609816ull, 12680325011617368327ull, 7441435328919647574ull, - 13407951422463106989ull, 9986964831043767237ull, 14464180164790259585ull, - 5220370176240914239ull, 3518323198598619449ull, 2133255899590215934ull, - 3011009182271535838ull, 14836384915404578807ull, 9555956714003340276ull, - 11964636267214528571ull, 16921397335982440924ull, 10586397832022517650ull, - 18096821421422867234ull, 14108486707933080521ull, 2822797701958308088ull, - 8446901108100253910ull, 9675426367378755701ull, 4615585398233598887ull, - 4951618999197916175ull, 13485186398181682580ull, 2264266327693067886ull, - 7513550480889044889ull, 14297335183847666490ull, 3496786386085197052ull, + 3114684496819061489ull, 6956326645770273433ull, 6687820258270235301ull, + 12696019809107805010ull, 6271839642900128638ull, 17758335370804468308ull, + 6246269702984932325ull, 756407504012262357ull, 10752289111609390933ull, + 10702669286541029164ull, 3961413467939854492ull, 13925604324515645251ull, + 11175435615622050442ull, 6776561790919030493ull, 577659037159878982ull, + 10163490242105190374ull, 11276159262602451941ull, 3717944350664878703ull, + 17786578140867347109ull, 253867406630618996ull, 3554686122410655613ull, + 17187792775761839863ull, 15223748191501073730ull, 4622823325308959265ull, + 3947596021646594531ull, 18103100887606856772ull, 3330742851831023775ull, + 12522884863273911920ull, 11078204221518180355ull, 7792565789695194874ull, + 5837249087395420066ull, 15061096079220184405ull, 10949413619028940858ull, + 3927414729140235852ull, 10786374148287235402ull, 13739564127065017562ull, + 6792673025550256385ull, 9120117456385770000ull, 2702365990173828030ull, + 14552218038038255731ull, 15974235985971979302ull, 5859075624711984363ull, + 6107800350680289807ull, 6809837422484044317ull, 4235871646442648099ull, + 2112314171124100852ull, 16908957968796171152ull, 16595556766713987481ull, + 17393639219555656528ull, 258880825276626691ull, 6502647070460437768ull, + 9521652940428922473ull, 12532170261876847654ull, 7965261589232878523ull, + 14891396965396161229ull, 8954363812786394023ull, 16427886737643839568ull, + 14570810525240019901ull, 18385114195862288592ull, 356400710711660674ull, + 6131866860079521894ull, 5473687205525959309ull, 1368982430503028604ull, + 12966841479814647796ull, 11709399142050353312ull, 5078259748833979006ull, + 12253705469518588621ull, 14449800309393374267ull, 11263108227153639840ull, + 4291291545838288200ull, 10391124708572243222ull, 11045512779463427838ull, + 14252271658091688198ull, 5864385547990959743ull, 10060761729093637760ull, + 6012435413813366588ull, 4863606450928885329ull, 13441348122273594418ull, + 2704170472265346213ull, 12941384362597894872ull, 10030988591835912271ull, + 7895088774892271145ull, 8891008075855122089ull, 9575328812735763041ull, + 15722680013241790924ull, 14330185982019726263ull, 3361192221763941904ull, + 12098417980386469814ull, 5014147383869998166ull, 4177883635838955668ull, + 7150992680430954116ull, 18395049852860007119ull, 17907122275511118531ull, + 10212301139308990009ull, 17920870463788384967ull, 2787743584436740230ull, }; // expectation corrected: kFlip_final 10293382543261562607ull -> 7432577004220403082ull, because v19's broker-state hash folds the closed rows as their count and a running digest, each row once when it is final (pineforge-broker-state/v19); this host projects one fixed execution hash, so the continuation does not enter; the trades did not move; re-harvested the same way on INT19's tree; V19-A's tip (6211dc94) gives the same rows. // expectation corrected (v19-E): kFlip_final 7432577004220403082ull -> 3653829772705383872ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (v19-D): kFlip_final 3653829772705383872ull -> 8142912162810668916ull, because v19-D keeps fewer retired rows and folds a bracket family's erased members behind a retained one as runs (pineforge-source-adapter/v4: K1 releases a current-cycle leg the revival's superseded test answers for; BracketRoster parks); the trades did not move; harvested with the TU's own switch on this tree (r5/v19-d). // expectation corrected (V19-FIX): kFlip_final 8142912162810668916ull -> 9440871734801450516ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kFlip_final = 9440871734801450516ull; +// expectation corrected (INT26 v19 hash re-pin): kFlip_final 9440871734801450516ull -> 7381600230201732590ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move. +constexpr std::uint64_t kFlip_final = 7381600230201732590ull; // R5 lane H-THIN's P10 moves one kFlip trade (INT26: the behaviour half of its // hash step ab297364, harvested on the integrated tree, equal to the lane's): // {1736121780000, 1736121840000, 103, 106} -> {1736121780000, 1736121900000, @@ -1322,47 +1517,168 @@ constexpr Trade kFlip_trades[] = { // 16808495481113052294ull -> 18129147092791966950ull // 14610337086459375067ull -> 12096596197056575067ull // 3288451699475749751ull -> 12419458164604031287ull +// expectation corrected (INT26 v19 hash re-pin, kBrackets_rows, 120 of 120 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 910134550716034036ull -> 7661078636126692219ull [H-THIN] +// 10119054608448654679ull -> 9096500876121352440ull [H-THIN] +// 5666477157831623024ull -> 14112738771422015627ull [H-THIN] +// 202804637143337815ull -> 3424342483834498990ull [H-THIN] +// 4829290339771875162ull -> 4773449085765542223ull [H-THIN] +// 10813129365163527157ull -> 11446110045063788208ull [H-THIN] +// 2873082008237019980ull -> 10433831526790963875ull [H-THIN] +// 17093419766211306788ull -> 13032183593567110055ull [H-THIN] +// 17114718258109977185ull -> 6742781461778894496ull [H-THIN] +// 13157751279945694777ull -> 15715452229856127120ull [H-THIN] +// 218728992015652860ull -> 8358119382546858485ull [H-THIN] +// 14366067783818258415ull -> 2290319528819275924ull [H-THIN] +// 2053665803239548226ull -> 6782857020890029813ull [H-THIN] +// 16605355454280111952ull -> 17953535504978118895ull [H-THIN] +// 12012249101044242847ull -> 4063231953652586830ull [H-THIN] +// 6571732001567235539ull -> 1777830241952955486ull [H-THIN] +// 1030372738842403124ull -> 9409772117090094177ull [H-THIN] +// 5009615712759518098ull -> 18086159773284292787ull [H-THIN] +// 11188453092947462355ull -> 8322892009219716946ull [H-THIN] +// 7315481592762531637ull -> 5534653117672349686ull [H-THIN] +// 15172698683941778544ull -> 15974950853372725031ull [H-THIN] +// 13076695872824005863ull -> 17858881224485069236ull [H-THIN] +// 8630952403774920278ull -> 14177855542759430887ull [H-THIN] +// 5446218045192577731ull -> 11337212344183599690ull [H-THIN] +// 8868444455935332378ull -> 2175468754819186737ull [H-THIN] +// 11935211056196573296ull -> 4612364077073358813ull [H-THIN] +// 10851985789766247663ull -> 12868865454125389666ull [H-THIN] +// 13162717003573213214ull -> 13883346286574059481ull [H-THIN] +// 3878781687326319893ull -> 10942687616239908444ull [H-THIN] +// 17299684057647085908ull -> 16439500824616424713ull [H-THIN] +// 3804735542114435005ull -> 13534132873070141390ull [H-THIN] +// 10757881489481288273ull -> 1657001366552425314ull [H-THIN] +// 7304910952741449329ull -> 2790121310768896680ull [H-THIN] +// 6446837817330552219ull -> 493357323053936518ull [H-THIN] +// 517463869567802231ull -> 1703129217351950834ull [H-THIN] +// 16486081082077568694ull -> 820240013594959273ull [H-THIN] +// 8342654310771400857ull -> 12701410792546982706ull [H-THIN] +// 18315162927804124892ull -> 12841347796780112451ull [H-THIN] +// 11131921289741383191ull -> 6762571077584465114ull [H-THIN] +// 4272804311264028152ull -> 9839274063428681485ull [H-THIN] +// 17329153790653310123ull -> 1142671265814790566ull [H-THIN] +// 2805380709705245281ull -> 7977023869714740224ull [H-THIN] +// 11779111360073316823ull -> 12196334454371478488ull [H-THIN] +// 169194399111162280ull -> 10719326225194309969ull [H-THIN] +// 11895438524630435024ull -> 2933196093229072601ull [H-THIN] +// 11609073112680438102ull -> 11873952510683230649ull [H-THIN] +// 13966776254915757287ull -> 11807266724158176848ull [H-THIN] +// 429534206234234885ull -> 4367492907737103676ull [H-THIN] +// 15196669054502239260ull -> 8270195260561269801ull [H-THIN] +// 11931512644904705052ull -> 8207582755124985801ull [H-THIN] +// 11456573762602716761ull -> 11179776345437501518ull [H-THIN] +// 15718773719268670624ull -> 15415488505702541339ull [H-THIN] +// 10989825173285828350ull -> 11371155962071199053ull [H-THIN] +// 11346633789448934891ull -> 514527900501657286ull [H-THIN] +// 1699367467603584576ull -> 16506041037325274955ull [H-THIN] +// 9509380442542362055ull -> 13369657622015869344ull [H-THIN] +// 5849765746586438263ull -> 9255829141470398292ull [H-THIN] +// 15258883296401804974ull -> 15142394148477901141ull [H-THIN] +// 12668399090838414326ull -> 5111689152684702997ull [H-THIN] +// 17428702340345096816ull -> 11236186855000699047ull [H-THIN] +// 16044980621491544275ull -> 3566968125769434750ull [H-THIN] +// 7858613838309440451ull -> 10566985719991230638ull [H-THIN] +// 4973377506472252675ull -> 8566448383289752548ull [H-THIN] +// 15666991206354451962ull -> 8352146082968021413ull [H-THIN] +// 427471937805578063ull -> 8514065980832786824ull [H-THIN] +// 13475650653372852055ull -> 4245601606500227800ull [H-THIN] +// 8639057947851950159ull -> 8540602924026434500ull [H-THIN] +// 5938463183864513700ull -> 11642724078551580301ull [H-THIN] +// 5648745424618098437ull -> 4082406208929632142ull [H-THIN] +// 447259644375350934ull -> 16999316433304684901ull [H-THIN] +// 16648371541827537609ull -> 12630189843512310176ull [H-THIN] +// 16429366851237249424ull -> 13269378032529487365ull [H-THIN] +// 279187655991346264ull -> 4089528504669692435ull [H-THIN] +// 15953137644069010440ull -> 13468147224454604453ull [H-THIN] +// 7431687286361549413ull -> 2050641591772190580ull [H-THIN] +// 5023789027380833817ull -> 13884969392006403536ull [H-THIN] +// 10248295502726720638ull -> 18342784319159857231ull [H-THIN] +// 640594980859648944ull -> 6373895992970415839ull [H-THIN] +// 448592334131119277ull -> 6052197144968057496ull [H-THIN] +// 16276383010288949834ull -> 4516024341018020553ull [H-THIN] +// 442889274262864391ull -> 16151109736050180518ull [H-THIN] +// 4333712493194412434ull -> 3962683434800121797ull [H-THIN] +// 14568797647822043048ull -> 10101286877014092167ull [H-THIN] +// 2692075964144184930ull -> 3322622454096003149ull [H-THIN] +// 13961322833977115428ull -> 12861272284809283343ull [H-THIN] +// 15127421684109569508ull -> 4487614500502847957ull [H-THIN] +// 14509829626384044961ull -> 11763183347924504138ull [H-THIN] +// 5027884074442838157ull -> 4022970112297852778ull [H-THIN] +// 9918179728360057649ull -> 4113353824470960398ull [H-THIN] +// 3559274869490871147ull -> 4712660953944041544ull [H-THIN] +// 2619065441802592089ull -> 620536408398626286ull [H-THIN] +// 9017534287281633674ull -> 13279868472225083865ull [H-THIN] +// 10142753397763051047ull -> 15776436722626106676ull [H-THIN] +// 1825167456880839822ull -> 2688432314125484999ull [H-THIN] +// 12808870162637299498ull -> 5425066327213429727ull [H-THIN] +// 16833369335393909673ull -> 17655979472884703908ull [H-THIN] +// 18259761263316881488ull -> 7599015585915916887ull [H-THIN] +// 2751903624558806694ull -> 14084462871382252093ull [H-THIN] +// 12840688134865061533ull -> 5327406007590004902ull [H-THIN] +// 17213452481114196577ull -> 1327260407587226834ull [H-THIN] +// 110464992498916575ull -> 9419238100865204704ull [H-THIN] +// 2694244057601629716ull -> 7851171160816794259ull [H-THIN] +// 4590220084927802863ull -> 14694071302479188878ull [H-THIN] +// 2437199406152424049ull -> 9265024402745593748ull [H-THIN] +// 8159801410563329851ull -> 16604561282375327072ull [H-THIN] +// 14735699852171413911ull -> 14329385415067150240ull [H-THIN] +// 9157222081581125695ull -> 6772020752524046436ull [H-THIN] +// 1085291491204167422ull -> 4903877568298110599ull [H-THIN] +// 6346057658078798747ull -> 125984323879217730ull [H-THIN] +// 9775349377282567210ull -> 13515966623569247839ull [H-THIN] +// 18290376404771660045ull -> 14623667090670085180ull [H-THIN] +// 14109635784428120043ull -> 8396494711544938138ull [H-THIN] +// 6793532772339349305ull -> 14666598984172760326ull [H-THIN] +// 12315454366145997871ull -> 13546459695976942876ull [H-THIN] +// 3340912588114346141ull -> 17394619144074825006ull [H-THIN] +// 10626957428750553111ull -> 4698093564093878598ull [H-THIN] +// 16133891667902416811ull -> 18182954524309548546ull [H-THIN] +// 18129147092791966950ull -> 2493468370943321295ull [H-THIN] +// 12096596197056575067ull -> 746152301302218096ull [H-THIN] +// 12419458164604031287ull -> 6645823410963443848ull [H-THIN] constexpr std::uint64_t kBrackets_rows[] = { - 910134550716034036ull, 10119054608448654679ull, 5666477157831623024ull, - 202804637143337815ull, 4829290339771875162ull, 10813129365163527157ull, - 2873082008237019980ull, 17093419766211306788ull, 17114718258109977185ull, - 13157751279945694777ull, 218728992015652860ull, 14366067783818258415ull, - 2053665803239548226ull, 16605355454280111952ull, 12012249101044242847ull, - 6571732001567235539ull, 1030372738842403124ull, 5009615712759518098ull, - 11188453092947462355ull, 7315481592762531637ull, 15172698683941778544ull, - 13076695872824005863ull, 8630952403774920278ull, 5446218045192577731ull, - 8868444455935332378ull, 11935211056196573296ull, 10851985789766247663ull, - 13162717003573213214ull, 3878781687326319893ull, 17299684057647085908ull, - 3804735542114435005ull, 10757881489481288273ull, 7304910952741449329ull, - 6446837817330552219ull, 517463869567802231ull, 16486081082077568694ull, - 8342654310771400857ull, 18315162927804124892ull, 11131921289741383191ull, - 4272804311264028152ull, 17329153790653310123ull, 2805380709705245281ull, - 11779111360073316823ull, 169194399111162280ull, 11895438524630435024ull, - 11609073112680438102ull, 13966776254915757287ull, 429534206234234885ull, - 15196669054502239260ull, 11931512644904705052ull, 11456573762602716761ull, - 15718773719268670624ull, 10989825173285828350ull, 11346633789448934891ull, - 1699367467603584576ull, 9509380442542362055ull, 5849765746586438263ull, - 15258883296401804974ull, 12668399090838414326ull, 17428702340345096816ull, - 16044980621491544275ull, 7858613838309440451ull, 4973377506472252675ull, - 15666991206354451962ull, 427471937805578063ull, 13475650653372852055ull, - 8639057947851950159ull, 5938463183864513700ull, 5648745424618098437ull, - 447259644375350934ull, 16648371541827537609ull, 16429366851237249424ull, - 279187655991346264ull, 15953137644069010440ull, 7431687286361549413ull, - 5023789027380833817ull, 10248295502726720638ull, 640594980859648944ull, - 448592334131119277ull, 16276383010288949834ull, 442889274262864391ull, - 4333712493194412434ull, 14568797647822043048ull, 2692075964144184930ull, - 13961322833977115428ull, 15127421684109569508ull, 14509829626384044961ull, - 5027884074442838157ull, 9918179728360057649ull, 3559274869490871147ull, - 2619065441802592089ull, 9017534287281633674ull, 10142753397763051047ull, - 1825167456880839822ull, 12808870162637299498ull, 16833369335393909673ull, - 18259761263316881488ull, 2751903624558806694ull, 12840688134865061533ull, - 17213452481114196577ull, 110464992498916575ull, 2694244057601629716ull, - 4590220084927802863ull, 2437199406152424049ull, 8159801410563329851ull, - 14735699852171413911ull, 9157222081581125695ull, 1085291491204167422ull, - 6346057658078798747ull, 9775349377282567210ull, 18290376404771660045ull, - 14109635784428120043ull, 6793532772339349305ull, 12315454366145997871ull, - 3340912588114346141ull, 10626957428750553111ull, 16133891667902416811ull, - 18129147092791966950ull, 12096596197056575067ull, 12419458164604031287ull, + 7661078636126692219ull, 9096500876121352440ull, 14112738771422015627ull, + 3424342483834498990ull, 4773449085765542223ull, 11446110045063788208ull, + 10433831526790963875ull, 13032183593567110055ull, 6742781461778894496ull, + 15715452229856127120ull, 8358119382546858485ull, 2290319528819275924ull, + 6782857020890029813ull, 17953535504978118895ull, 4063231953652586830ull, + 1777830241952955486ull, 9409772117090094177ull, 18086159773284292787ull, + 8322892009219716946ull, 5534653117672349686ull, 15974950853372725031ull, + 17858881224485069236ull, 14177855542759430887ull, 11337212344183599690ull, + 2175468754819186737ull, 4612364077073358813ull, 12868865454125389666ull, + 13883346286574059481ull, 10942687616239908444ull, 16439500824616424713ull, + 13534132873070141390ull, 1657001366552425314ull, 2790121310768896680ull, + 493357323053936518ull, 1703129217351950834ull, 820240013594959273ull, + 12701410792546982706ull, 12841347796780112451ull, 6762571077584465114ull, + 9839274063428681485ull, 1142671265814790566ull, 7977023869714740224ull, + 12196334454371478488ull, 10719326225194309969ull, 2933196093229072601ull, + 11873952510683230649ull, 11807266724158176848ull, 4367492907737103676ull, + 8270195260561269801ull, 8207582755124985801ull, 11179776345437501518ull, + 15415488505702541339ull, 11371155962071199053ull, 514527900501657286ull, + 16506041037325274955ull, 13369657622015869344ull, 9255829141470398292ull, + 15142394148477901141ull, 5111689152684702997ull, 11236186855000699047ull, + 3566968125769434750ull, 10566985719991230638ull, 8566448383289752548ull, + 8352146082968021413ull, 8514065980832786824ull, 4245601606500227800ull, + 8540602924026434500ull, 11642724078551580301ull, 4082406208929632142ull, + 16999316433304684901ull, 12630189843512310176ull, 13269378032529487365ull, + 4089528504669692435ull, 13468147224454604453ull, 2050641591772190580ull, + 13884969392006403536ull, 18342784319159857231ull, 6373895992970415839ull, + 6052197144968057496ull, 4516024341018020553ull, 16151109736050180518ull, + 3962683434800121797ull, 10101286877014092167ull, 3322622454096003149ull, + 12861272284809283343ull, 4487614500502847957ull, 11763183347924504138ull, + 4022970112297852778ull, 4113353824470960398ull, 4712660953944041544ull, + 620536408398626286ull, 13279868472225083865ull, 15776436722626106676ull, + 2688432314125484999ull, 5425066327213429727ull, 17655979472884703908ull, + 7599015585915916887ull, 14084462871382252093ull, 5327406007590004902ull, + 1327260407587226834ull, 9419238100865204704ull, 7851171160816794259ull, + 14694071302479188878ull, 9265024402745593748ull, 16604561282375327072ull, + 14329385415067150240ull, 6772020752524046436ull, 4903877568298110599ull, + 125984323879217730ull, 13515966623569247839ull, 14623667090670085180ull, + 8396494711544938138ull, 14666598984172760326ull, 13546459695976942876ull, + 17394619144074825006ull, 4698093564093878598ull, 18182954524309548546ull, + 2493468370943321295ull, 746152301302218096ull, 6645823410963443848ull, }; // expectation corrected (kBrackets_reads, 120 of 120 values), because v19's broker-state hash folds the closed rows as their count and a running digest, each row once when it is final (pineforge-broker-state/v19); this host projects one fixed execution hash, so the continuation does not enter; the trades did not move; re-harvested the same way on INT19's tree; V19-A's tip (6211dc94) gives the same rows: // 15220523432559179916ull -> 1676626113542829169ull @@ -1826,53 +2142,175 @@ constexpr std::uint64_t kBrackets_rows[] = { // 8670934686211779845ull -> 8075508688952706341ull // 12646157929630605187ull -> 3795209660336936451ull // 5822263896316998189ull -> 10247852932638933229ull +// expectation corrected (INT26 v19 hash re-pin, kBrackets_reads, 120 of 120 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 174544879155911710ull -> 6447178462726161489ull [H-THIN] +// 6847890701157152189ull -> 7282051271575243766ull [H-THIN] +// 14236720572403425120ull -> 3204765037450491575ull [H-THIN] +// 9323947307018159523ull -> 18267580849453675956ull [H-THIN] +// 6706848899178691109ull -> 12480191273474156080ull [H-THIN] +// 6612524546470024288ull -> 13176315927737097143ull [H-THIN] +// 10668883385252112854ull -> 2613978327321436125ull [H-THIN] +// 231581670271637154ull -> 6672818892360288129ull [H-THIN] +// 13242222575440997023ull -> 11742746030453123914ull [H-THIN] +// 17290172295419987875ull -> 10755408511157905806ull [H-THIN] +// 3265116288063283204ull -> 5043690746894668881ull [H-THIN] +// 8205670759236763993ull -> 9940769973078973490ull [H-THIN] +// 112561924048020734ull -> 13766311310658315637ull [H-THIN] +// 15428098460808809263ull -> 17445549063740998476ull [H-THIN] +// 17567572508977123509ull -> 11888716919086895768ull [H-THIN] +// 11229049955366294166ull -> 2021557805667300987ull [H-THIN] +// 18321533072536116520ull -> 17925926338552567217ull [H-THIN] +// 16105561550759864220ull -> 477557840133476497ull [H-THIN] +// 483522463541730617ull -> 5775694257083602140ull [H-THIN] +// 9570911977664884728ull -> 16386436657763025955ull [H-THIN] +// 7580047313596786995ull -> 3123738760236770072ull [H-THIN] +// 4665256402668508715ull -> 14223494751084252108ull [H-THIN] +// 421497521705913896ull -> 18354245747609319561ull [H-THIN] +// 17087607459663445442ull -> 8349411476214893155ull [H-THIN] +// 12527499930243019525ull -> 3923626943346498186ull [H-THIN] +// 2971437309355039777ull -> 6383384587798256840ull [H-THIN] +// 9863935661287706359ull -> 12975754508323072986ull [H-THIN] +// 6851589387112729438ull -> 1647783811313703689ull [H-THIN] +// 10761885404547615008ull -> 4455718202356565173ull [H-THIN] +// 5179210173701881257ull -> 8665836061694240420ull [H-THIN] +// 12700378635574144447ull -> 11408952070111923920ull [H-THIN] +// 12732163138534186016ull -> 7614293002298548875ull [H-THIN] +// 14016674439498449031ull -> 5629517015204608062ull [H-THIN] +// 11327403139305040686ull -> 4158951807679315351ull [H-THIN] +// 5747816375553482093ull -> 11024401079205504452ull [H-THIN] +// 12386497247962690388ull -> 2438635214836639347ull [H-THIN] +// 16040095552360636577ull -> 3497662992807996570ull [H-THIN] +// 17340479382499593300ull -> 16474642763659850795ull [H-THIN] +// 12046954179175030003ull -> 16735708978364955206ull [H-THIN] +// 3330771664215515798ull -> 3252272045281708895ull [H-THIN] +// 2985817778317811366ull -> 15474076080136309379ull [H-THIN] +// 3949828394086364361ull -> 13848019505242045158ull [H-THIN] +// 16198687903297035507ull -> 3476711314567542992ull [H-THIN] +// 11881556093292430173ull -> 4854017566382016708ull [H-THIN] +// 10043888482375429571ull -> 14076054934784788294ull [H-THIN] +// 7435285615030433029ull -> 17570234807141684014ull [H-THIN] +// 9041137245577600492ull -> 12112735759254556435ull [H-THIN] +// 993655137088931422ull -> 14613241064154119463ull [H-THIN] +// 16182190595469348839ull -> 18349864350883260054ull [H-THIN] +// 5762190984609537746ull -> 15417110870088006819ull [H-THIN] +// 87663964338170542ull -> 17128247651204467173ull [H-THIN] +// 15322366287499145950ull -> 10807796678683691685ull [H-THIN] +// 13403031039768262916ull -> 6278785944056007979ull [H-THIN] +// 13795061532521471462ull -> 8049557354080931463ull [H-THIN] +// 6407369734919200583ull -> 17791516491718511800ull [H-THIN] +// 8575404273882652022ull -> 11305578665809417121ull [H-THIN] +// 962815066506496169ull -> 13230018917786094638ull [H-THIN] +// 13267308122545089892ull -> 1502537382570686975ull [H-THIN] +// 11888142924425674761ull -> 14318816668076449218ull [H-THIN] +// 12543244761273968195ull -> 15331500228353786720ull [H-THIN] +// 743116436140857884ull -> 17346729228089869405ull [H-THIN] +// 13428981689322201921ull -> 1561708263858075840ull [H-THIN] +// 3888528505659186179ull -> 229263709144259356ull [H-THIN] +// 16920970929338735704ull -> 17327472028118720615ull [H-THIN] +// 11486836585396753956ull -> 12182983198162567319ull [H-THIN] +// 3912352931341674340ull -> 8439356152210307823ull [H-THIN] +// 488391584166124984ull -> 11858668009824522697ull [H-THIN] +// 594818243880175417ull -> 10098763073045529668ull [H-THIN] +// 5897410099435466572ull -> 178845652746320211ull [H-THIN] +// 6292341051554398594ull -> 2011898511618741601ull [H-THIN] +// 5652341650549197297ull -> 10212557729617550900ull [H-THIN] +// 15500575164592052866ull -> 17051694765221730851ull [H-THIN] +// 6616463239552538973ull -> 9868130284962937766ull [H-THIN] +// 6924796937138718592ull -> 18032334360967082277ull [H-THIN] +// 12199564954470416935ull -> 14727145473477122778ull [H-THIN] +// 1262923056330410530ull -> 16259529515940238053ull [H-THIN] +// 14580867601298688079ull -> 16828614615166433170ull [H-THIN] +// 695300185954652101ull -> 8468897962405976228ull [H-THIN] +// 4911858693999487313ull -> 5207571868172488684ull [H-THIN] +// 4052266239546809097ull -> 7657491728824699978ull [H-THIN] +// 16261647227390403486ull -> 7536338060220416859ull [H-THIN] +// 9269996099404471907ull -> 16443584424917468468ull [H-THIN] +// 17364765293714899346ull -> 5422665264074241505ull [H-THIN] +// 9253740477123675482ull -> 8096893673298686321ull [H-THIN] +// 3598536848022417347ull -> 10330127297609212492ull [H-THIN] +// 15772584097012446157ull -> 3143371143401203384ull [H-THIN] +// 13220139554987575832ull -> 17744960029344161943ull [H-THIN] +// 5696897036054960153ull -> 743800041325432758ull [H-THIN] +// 16985841889497997093ull -> 576351043472799110ull [H-THIN] +// 10415231377040584138ull -> 15626777699481064933ull [H-THIN] +// 12984613912057588504ull -> 25613991977989083ull [H-THIN] +// 598945118998222943ull -> 423738522170636072ull [H-THIN] +// 6912671141973928854ull -> 2370489562900647761ull [H-THIN] +// 18429543982731833536ull -> 5138368878345319471ull [H-THIN] +// 5917122860046364322ull -> 4753877877775442691ull [H-THIN] +// 14977518267164775357ull -> 17962273710729275004ull [H-THIN] +// 15294004932482913795ull -> 17588425223806176188ull [H-THIN] +// 14861726070560837386ull -> 17355922851713147397ull [H-THIN] +// 14467806706499675692ull -> 5559047461962127399ull [H-THIN] +// 6260501953792293914ull -> 15923451304054496993ull [H-THIN] +// 8227642000683598159ull -> 1395104389648954276ull [H-THIN] +// 16328367771257271624ull -> 12594813164804610403ull [H-THIN] +// 2080811162938952899ull -> 8499202279082002358ull [H-THIN] +// 4230200989955191115ull -> 3976288064231381934ull [H-THIN] +// 2243443394831501937ull -> 6053679002470499114ull [H-THIN] +// 14575945386085730904ull -> 16698836563482206307ull [H-THIN] +// 7236032703621894143ull -> 9330221417640539684ull [H-THIN] +// 11053348144569293517ull -> 11686916916826934224ull [H-THIN] +// 12741152113770246445ull -> 1635100190140340380ull [H-THIN] +// 6345919259715423413ull -> 14429355558491266072ull [H-THIN] +// 4116643839905048431ull -> 2638545206399705470ull [H-THIN] +// 13283796175120077278ull -> 9334315816579956891ull [H-THIN] +// 14167185253504234566ull -> 4285818535841220481ull [H-THIN] +// 11430800879610209506ull -> 6571870152547998309ull [H-THIN] +// 14033714145752220328ull -> 5503577911173824511ull [H-THIN] +// 739675710812434647ull -> 8243465699060733690ull [H-THIN] +// 11662106361049752258ull -> 418090560305378255ull [H-THIN] +// 8075508688952706341ull -> 17066859890889434988ull [H-THIN] +// 3795209660336936451ull -> 888971320708135628ull [H-THIN] +// 10247852932638933229ull -> 5198182708464165974ull [H-THIN] constexpr std::uint64_t kBrackets_reads[] = { - 174544879155911710ull, 6847890701157152189ull, 14236720572403425120ull, - 9323947307018159523ull, 6706848899178691109ull, 6612524546470024288ull, - 10668883385252112854ull, 231581670271637154ull, 13242222575440997023ull, - 17290172295419987875ull, 3265116288063283204ull, 8205670759236763993ull, - 112561924048020734ull, 15428098460808809263ull, 17567572508977123509ull, - 11229049955366294166ull, 18321533072536116520ull, 16105561550759864220ull, - 483522463541730617ull, 9570911977664884728ull, 7580047313596786995ull, - 4665256402668508715ull, 421497521705913896ull, 17087607459663445442ull, - 12527499930243019525ull, 2971437309355039777ull, 9863935661287706359ull, - 6851589387112729438ull, 10761885404547615008ull, 5179210173701881257ull, - 12700378635574144447ull, 12732163138534186016ull, 14016674439498449031ull, - 11327403139305040686ull, 5747816375553482093ull, 12386497247962690388ull, - 16040095552360636577ull, 17340479382499593300ull, 12046954179175030003ull, - 3330771664215515798ull, 2985817778317811366ull, 3949828394086364361ull, - 16198687903297035507ull, 11881556093292430173ull, 10043888482375429571ull, - 7435285615030433029ull, 9041137245577600492ull, 993655137088931422ull, - 16182190595469348839ull, 5762190984609537746ull, 87663964338170542ull, - 15322366287499145950ull, 13403031039768262916ull, 13795061532521471462ull, - 6407369734919200583ull, 8575404273882652022ull, 962815066506496169ull, - 13267308122545089892ull, 11888142924425674761ull, 12543244761273968195ull, - 743116436140857884ull, 13428981689322201921ull, 3888528505659186179ull, - 16920970929338735704ull, 11486836585396753956ull, 3912352931341674340ull, - 488391584166124984ull, 594818243880175417ull, 5897410099435466572ull, - 6292341051554398594ull, 5652341650549197297ull, 15500575164592052866ull, - 6616463239552538973ull, 6924796937138718592ull, 12199564954470416935ull, - 1262923056330410530ull, 14580867601298688079ull, 695300185954652101ull, - 4911858693999487313ull, 4052266239546809097ull, 16261647227390403486ull, - 9269996099404471907ull, 17364765293714899346ull, 9253740477123675482ull, - 3598536848022417347ull, 15772584097012446157ull, 13220139554987575832ull, - 5696897036054960153ull, 16985841889497997093ull, 10415231377040584138ull, - 12984613912057588504ull, 598945118998222943ull, 6912671141973928854ull, - 18429543982731833536ull, 5917122860046364322ull, 14977518267164775357ull, - 15294004932482913795ull, 14861726070560837386ull, 14467806706499675692ull, - 6260501953792293914ull, 8227642000683598159ull, 16328367771257271624ull, - 2080811162938952899ull, 4230200989955191115ull, 2243443394831501937ull, - 14575945386085730904ull, 7236032703621894143ull, 11053348144569293517ull, - 12741152113770246445ull, 6345919259715423413ull, 4116643839905048431ull, - 13283796175120077278ull, 14167185253504234566ull, 11430800879610209506ull, - 14033714145752220328ull, 739675710812434647ull, 11662106361049752258ull, - 8075508688952706341ull, 3795209660336936451ull, 10247852932638933229ull, + 6447178462726161489ull, 7282051271575243766ull, 3204765037450491575ull, + 18267580849453675956ull, 12480191273474156080ull, 13176315927737097143ull, + 2613978327321436125ull, 6672818892360288129ull, 11742746030453123914ull, + 10755408511157905806ull, 5043690746894668881ull, 9940769973078973490ull, + 13766311310658315637ull, 17445549063740998476ull, 11888716919086895768ull, + 2021557805667300987ull, 17925926338552567217ull, 477557840133476497ull, + 5775694257083602140ull, 16386436657763025955ull, 3123738760236770072ull, + 14223494751084252108ull, 18354245747609319561ull, 8349411476214893155ull, + 3923626943346498186ull, 6383384587798256840ull, 12975754508323072986ull, + 1647783811313703689ull, 4455718202356565173ull, 8665836061694240420ull, + 11408952070111923920ull, 7614293002298548875ull, 5629517015204608062ull, + 4158951807679315351ull, 11024401079205504452ull, 2438635214836639347ull, + 3497662992807996570ull, 16474642763659850795ull, 16735708978364955206ull, + 3252272045281708895ull, 15474076080136309379ull, 13848019505242045158ull, + 3476711314567542992ull, 4854017566382016708ull, 14076054934784788294ull, + 17570234807141684014ull, 12112735759254556435ull, 14613241064154119463ull, + 18349864350883260054ull, 15417110870088006819ull, 17128247651204467173ull, + 10807796678683691685ull, 6278785944056007979ull, 8049557354080931463ull, + 17791516491718511800ull, 11305578665809417121ull, 13230018917786094638ull, + 1502537382570686975ull, 14318816668076449218ull, 15331500228353786720ull, + 17346729228089869405ull, 1561708263858075840ull, 229263709144259356ull, + 17327472028118720615ull, 12182983198162567319ull, 8439356152210307823ull, + 11858668009824522697ull, 10098763073045529668ull, 178845652746320211ull, + 2011898511618741601ull, 10212557729617550900ull, 17051694765221730851ull, + 9868130284962937766ull, 18032334360967082277ull, 14727145473477122778ull, + 16259529515940238053ull, 16828614615166433170ull, 8468897962405976228ull, + 5207571868172488684ull, 7657491728824699978ull, 7536338060220416859ull, + 16443584424917468468ull, 5422665264074241505ull, 8096893673298686321ull, + 10330127297609212492ull, 3143371143401203384ull, 17744960029344161943ull, + 743800041325432758ull, 576351043472799110ull, 15626777699481064933ull, + 25613991977989083ull, 423738522170636072ull, 2370489562900647761ull, + 5138368878345319471ull, 4753877877775442691ull, 17962273710729275004ull, + 17588425223806176188ull, 17355922851713147397ull, 5559047461962127399ull, + 15923451304054496993ull, 1395104389648954276ull, 12594813164804610403ull, + 8499202279082002358ull, 3976288064231381934ull, 6053679002470499114ull, + 16698836563482206307ull, 9330221417640539684ull, 11686916916826934224ull, + 1635100190140340380ull, 14429355558491266072ull, 2638545206399705470ull, + 9334315816579956891ull, 4285818535841220481ull, 6571870152547998309ull, + 5503577911173824511ull, 8243465699060733690ull, 418090560305378255ull, + 17066859890889434988ull, 888971320708135628ull, 5198182708464165974ull, }; // expectation corrected: kBrackets_final 9769255577571703860ull -> 10619878969101331239ull, because v19's broker-state hash folds the closed rows as their count and a running digest, each row once when it is final (pineforge-broker-state/v19); this host projects one fixed execution hash, so the continuation does not enter; the trades did not move; re-harvested the same way on INT19's tree; V19-A's tip (6211dc94) gives the same rows. // expectation corrected (v19-E): kBrackets_final 10619878969101331239ull -> 14337296651809392799ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (INT21 K3): kBrackets_final 14337296651809392799ull -> 3288451699475749751ull, because K3 now keeps the first re-issue of each revival candidate instead of every one (INT21): more retired exit legs are erased, and the v4 fold reads the retained rows beside the erased-row count and digest; the trades did not move; harvested with its switch on this tree; the tree before the correction reproduces every old pin. // expectation corrected (V19-FIX): kBrackets_final 3288451699475749751ull -> 12419458164604031287ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kBrackets_final = 12419458164604031287ull; +// expectation corrected (INT26 v19 hash re-pin): kBrackets_final 12419458164604031287ull -> 6645823410963443848ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move. +constexpr std::uint64_t kBrackets_final = 6645823410963443848ull; constexpr Trade kBrackets_trades[] = { {1736121900000LL, 1736121960000LL, 105.5, 108.25, 1, 0}, {1736121900000LL, 1736124000000LL, 105.5, 106.5, 1, 0}, @@ -2369,61 +2807,222 @@ constexpr Trade kBrackets_trades[] = { // 5880827786329181412ull -> 7792848154392720004ull // 309376478011650575ull -> 65916750986234223ull // 7955431514292085542ull -> 13869421282421271494ull +// expectation corrected (INT26 v19 hash re-pin, kChains_rows, 160 of 160 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 18068305618654701499ull -> 17216550202785438742ull [H-THIN] +// 7631310091482109571ull -> 888925525783100684ull [H-THIN] +// 3548794957611195047ull -> 14971086878838481907ull [H-THIN] +// 9238067768069886424ull -> 1294272531176600360ull [H-THIN] +// 15801514563772350680ull -> 1886455716578836766ull [H-THIN] +// 17906063143479564342ull -> 11637635059042252090ull [H-THIN] +// 14314488034311577333ull -> 17515667699609373101ull [H-THIN] +// 10547448244184431820ull -> 7000083051162580056ull [H-THIN] +// 7123489397519609406ull -> 9300983128218736014ull [H-THIN] +// 1602213910524840811ull -> 9958894908037006139ull [H-THIN] +// 9787597445052736151ull -> 1635532543853096379ull [H-THIN] +// 17601291968173311470ull -> 16017861459144616409ull [H-THIN] +// 5776057093989319106ull -> 17338287424821881179ull [H-THIN] +// 5050622455696579782ull -> 1983186311692138031ull [H-THIN] +// 14615448368462347221ull -> 9090376350238380314ull [H-THIN] +// 17881255140791172527ull -> 12881684737752619620ull [H-THIN] +// 12765402170990768512ull -> 15135573007448004119ull [H-THIN] +// 17731227218325476252ull -> 9716971973878048845ull [H-THIN] +// 1751899773874851993ull -> 1434832193067712479ull [H-THIN] +// 17508832565167325456ull -> 13391945418166012328ull [H-THIN] +// 9700834346346522071ull -> 4222942739049503617ull [H-THIN] +// 1282406435677068885ull -> 14263738008833632447ull [H-THIN] +// 3633697705136355869ull -> 13268772213335834263ull [H-THIN] +// 7615819126236245163ull -> 5997455233863395761ull [H-THIN] +// 483990815715194534ull -> 1068503963899776944ull [H-THIN] +// 17161117397723763954ull -> 17141612644617594564ull [H-THIN] +// 13706192373597017281ull -> 9871144343592514033ull [H-THIN] +// 8349535569627227390ull -> 5276840031980785269ull [H-THIN] +// 1043431932614584276ull -> 15355133066636605819ull [H-THIN] +// 14601237308263653315ull -> 2728946953552743916ull [H-THIN] +// 5546360134897835890ull -> 15136838873153049319ull [H-THIN] +// 6847183254477845044ull -> 5645466354956180049ull [H-THIN] +// 780618388197566036ull -> 18409791973237644079ull [H-THIN] +// 1869891317481091735ull -> 14278855388951708356ull [H-THIN] +// 14666026974162495080ull -> 17266360427613746801ull [H-THIN] +// 2462504664095454997ull -> 18261188449347737966ull [H-THIN] +// 9050713444551814707ull -> 10896461346494157758ull [H-THIN] +// 18273004200638640909ull -> 16041849805505875804ull [H-THIN] +// 2005853044969644190ull -> 10185251185371103195ull [H-THIN] +// 3149925508439969851ull -> 1646822496082916590ull [H-THIN] +// 5854844698006220443ull -> 7947291941730160766ull [H-THIN] +// 9381219014372881502ull -> 8578518959639923799ull [H-THIN] +// 18324992465300580137ull -> 18335246465501349816ull [H-THIN] +// 1461894063981629002ull -> 9718231080356630143ull [H-THIN] +// 17437165102333019839ull -> 16219609048603415226ull [H-THIN] +// 8044544612955041611ull -> 10959371403202421450ull [H-THIN] +// 16351394341610307986ull -> 17602634145882978471ull [H-THIN] +// 14412883952315230739ull -> 8425686225706397430ull [H-THIN] +// 10721912884113213397ull -> 12708049332763053506ull [H-THIN] +// 12992048657322263867ull -> 7627006390785584896ull [H-THIN] +// 3339483349076882774ull -> 8859726068956568565ull [H-THIN] +// 12589462032708666350ull -> 3362393005477244925ull [H-THIN] +// 4157120851447613547ull -> 5181063662459076181ull [H-THIN] +// 14533664878044930220ull -> 11384647369363252140ull [H-THIN] +// 10713585920480808169ull -> 14541944896990249375ull [H-THIN] +// 2002537927364435313ull -> 2314324140859013727ull [H-THIN] +// 10170703978995689194ull -> 13411671664513209746ull [H-THIN] +// 3860537231187349238ull -> 5868706585110359110ull [H-THIN] +// 13713491075625798656ull -> 10711939723010575336ull [H-THIN] +// 608981548879404887ull -> 3729675833838621372ull [H-THIN] +// 13504708270136522593ull -> 9243393460869700874ull [H-THIN] +// 6427407450358014981ull -> 17371699057649433318ull [H-THIN] +// 4929862301177442641ull -> 3997723996730063192ull [H-THIN] +// 2268853989619820532ull -> 11433050125327522239ull [H-THIN] +// 6103887638466893787ull -> 12541125813245406968ull [H-THIN] +// 11509749389073642609ull -> 9039672968915681536ull [H-THIN] +// 18133726165290830677ull -> 15608011746644681292ull [H-THIN] +// 15915101668393006094ull -> 5054687594698147473ull [H-THIN] +// 5109293539399095562ull -> 3477709473812172570ull [H-THIN] +// 4762639120474267940ull -> 10152311901395946216ull [H-THIN] +// 16578121073498307040ull -> 8891349751852823074ull [H-THIN] +// 12298653987130833045ull -> 16144546084129249441ull [H-THIN] +// 14615479905553429955ull -> 13685016483773539513ull [H-THIN] +// 8498021699752483664ull -> 13522218457313117986ull [H-THIN] +// 2237995378251169439ull -> 4995503363249481535ull [H-THIN] +// 3400521619940694194ull -> 16748306406958785033ull [H-THIN] +// 13553756526528636558ull -> 9311515972501057953ull [H-THIN] +// 2109463370083716881ull -> 7365920725454078940ull [H-THIN] +// 14168593821002454122ull -> 8173149860273300269ull [H-THIN] +// 16341261596541620307ull -> 3129205185712362172ull [H-THIN] +// 10041235417076058114ull -> 10311992612364215267ull [H-THIN] +// 7909486330303743747ull -> 6430081021816923700ull [H-THIN] +// 11640249140605321865ull -> 3812940485274326802ull [H-THIN] +// 1871933448816404727ull -> 12258588714698561684ull [H-THIN] +// 10857133212749050668ull -> 16977065476345962723ull [H-THIN] +// 11914116902843745018ull -> 16840034821380621151ull [H-THIN] +// 8338386234307664155ull -> 196921697771132750ull [H-THIN] +// 7693563294105136383ull -> 1532115488081461896ull [H-THIN] +// 4393637264257635043ull -> 11299803424734145910ull [H-THIN] +// 10817114663804369156ull -> 843050419706294665ull [H-THIN] +// 777552857201851974ull -> 3060973841661433401ull [H-THIN] +// 4318860778166919945ull -> 1061493805797114660ull [H-THIN] +// 3440765181079071375ull -> 11945392687849806220ull [H-THIN] +// 18150517180267406702ull -> 2644275880755387635ull [H-THIN] +// 3822860841058540379ull -> 18268997097508906988ull [H-THIN] +// 18408233930596919548ull -> 10440378040220682715ull [H-THIN] +// 15483250115370548957ull -> 5460631613847058284ull [H-THIN] +// 12187081253452044158ull -> 90541039206026679ull [H-THIN] +// 15096704468692058143ull -> 1525179726704156361ull [H-THIN] +// 351395931644162269ull -> 4568196733230345761ull [H-THIN] +// 7681099562665395463ull -> 4900564848107415469ull [H-THIN] +// 621673586916640985ull -> 905839454594031045ull [H-THIN] +// 1548670563434988819ull -> 5600806767076858089ull [H-THIN] +// 15018421197641490508ull -> 15853491948794316602ull [H-THIN] +// 14843102830690661585ull -> 4436926389284220729ull [H-THIN] +// 18078373819427574057ull -> 12484426071009033621ull [H-THIN] +// 2156517456339179906ull -> 14565732008483823358ull [H-THIN] +// 1550063316561336971ull -> 5421750991390559718ull [H-THIN] +// 682120972313074028ull -> 16654253476033026771ull [H-THIN] +// 13222505441996051255ull -> 4914934475284686956ull [H-THIN] +// 894762928574935768ull -> 11353492294670403361ull [H-THIN] +// 6921956189824964594ull -> 2952007191267046267ull [H-THIN] +// 1470489317032329184ull -> 4137400735535674785ull [H-THIN] +// 3757845431780800368ull -> 13313570887318196803ull [H-THIN] +// 4291423602824827155ull -> 16333832917038083335ull [H-THIN] +// 11888096106209469513ull -> 15983016049430097831ull [H-THIN] +// 3227573574798329590ull -> 16516441775145748186ull [H-THIN] +// 13089038313828989382ull -> 9980463146813617446ull [H-THIN] +// 7508021719959037954ull -> 14892416906114576210ull [H-THIN] +// 2308535366701241976ull -> 6560664027287701548ull [H-THIN] +// 1246288425489315813ull -> 11333039054045879633ull [H-THIN] +// 11433845872149110256ull -> 10624127326473164730ull [H-THIN] +// 10295145752395079770ull -> 10585896247857052348ull [H-THIN] +// 2890366903333661801ull -> 9715815904094644978ull [H-THIN] +// 9960659940131424438ull -> 12043177962550059801ull [H-THIN] +// 14272369748982538569ull -> 6319376608257543486ull [H-THIN] +// 17076452457032941051ull -> 16904749238070947842ull [H-THIN] +// 3424791957006073618ull -> 6095240370596913147ull [H-THIN] +// 1811248193508923525ull -> 4788859803642606588ull [H-THIN] +// 14588001261256884000ull -> 6996529386998857627ull [H-THIN] +// 13923059957978311005ull -> 4078288008260849702ull [H-THIN] +// 16983695715066873498ull -> 4804551383567784305ull [H-THIN] +// 608375160627328848ull -> 9950943025990090361ull [H-THIN] +// 8684084180111108347ull -> 884570081640732608ull [H-THIN] +// 15679623628995063704ull -> 626856908236235819ull [H-THIN] +// 8464528085679884071ull -> 6383254879440463492ull [H-THIN] +// 14932854744042668023ull -> 17222449793445921158ull [H-THIN] +// 569982362832841031ull -> 12876151944089409278ull [H-THIN] +// 7496415600731368183ull -> 664057026045247402ull [H-THIN] +// 6021406431901867511ull -> 14473992423923157020ull [H-THIN] +// 14375074717677642639ull -> 16134349783577291380ull [H-THIN] +// 15805523719787038613ull -> 17824754404789806578ull [H-THIN] +// 12126947588968916209ull -> 11922334275858293940ull [H-THIN] +// 11019915590207223465ull -> 12324529413121065212ull [H-THIN] +// 12152802468277950710ull -> 12586161209074725511ull [H-THIN] +// 9974141365018685887ull -> 17841333238070299986ull [H-THIN] +// 7966238851102234546ull -> 4939054549376478538ull [H-THIN] +// 9747060662483912123ull -> 12653974792798479453ull [H-THIN] +// 1717605098271755502ull -> 168169012948749036ull [H-THIN] +// 9342227818610348684ull -> 1600379719717813316ull [H-THIN] +// 2114030550145565950ull -> 9597243467462794310ull [H-THIN] +// 418023209795270565ull -> 746366577121939789ull [H-THIN] +// 15770927766697271034ull -> 4939870284827214452ull [H-THIN] +// 12606063420003019757ull -> 14763601424926594359ull [H-THIN] +// 1303915722987961013ull -> 14442078822824496949ull [H-THIN] +// 11281002575808296045ull -> 10722539175905485350ull [H-THIN] +// 5386973648178682980ull -> 1668204868435100495ull [H-THIN] +// 7792848154392720004ull -> 3125339228212761319ull [H-THIN] +// 65916750986234223ull -> 12970829842683432868ull [H-THIN] +// 13869421282421271494ull -> 14563213551732184257ull [H-THIN] constexpr std::uint64_t kChains_rows[] = { - 18068305618654701499ull, 7631310091482109571ull, 3548794957611195047ull, - 9238067768069886424ull, 15801514563772350680ull, 17906063143479564342ull, - 14314488034311577333ull, 10547448244184431820ull, 7123489397519609406ull, - 1602213910524840811ull, 9787597445052736151ull, 17601291968173311470ull, - 5776057093989319106ull, 5050622455696579782ull, 14615448368462347221ull, - 17881255140791172527ull, 12765402170990768512ull, 17731227218325476252ull, - 1751899773874851993ull, 17508832565167325456ull, 9700834346346522071ull, - 1282406435677068885ull, 3633697705136355869ull, 7615819126236245163ull, - 483990815715194534ull, 17161117397723763954ull, 13706192373597017281ull, - 8349535569627227390ull, 1043431932614584276ull, 14601237308263653315ull, - 5546360134897835890ull, 6847183254477845044ull, 780618388197566036ull, - 1869891317481091735ull, 14666026974162495080ull, 2462504664095454997ull, - 9050713444551814707ull, 18273004200638640909ull, 2005853044969644190ull, - 3149925508439969851ull, 5854844698006220443ull, 9381219014372881502ull, - 18324992465300580137ull, 1461894063981629002ull, 17437165102333019839ull, - 8044544612955041611ull, 16351394341610307986ull, 14412883952315230739ull, - 10721912884113213397ull, 12992048657322263867ull, 3339483349076882774ull, - 12589462032708666350ull, 4157120851447613547ull, 14533664878044930220ull, - 10713585920480808169ull, 2002537927364435313ull, 10170703978995689194ull, - 3860537231187349238ull, 13713491075625798656ull, 608981548879404887ull, - 13504708270136522593ull, 6427407450358014981ull, 4929862301177442641ull, - 2268853989619820532ull, 6103887638466893787ull, 11509749389073642609ull, - 18133726165290830677ull, 15915101668393006094ull, 5109293539399095562ull, - 4762639120474267940ull, 16578121073498307040ull, 12298653987130833045ull, - 14615479905553429955ull, 8498021699752483664ull, 2237995378251169439ull, - 3400521619940694194ull, 13553756526528636558ull, 2109463370083716881ull, - 14168593821002454122ull, 16341261596541620307ull, 10041235417076058114ull, - 7909486330303743747ull, 11640249140605321865ull, 1871933448816404727ull, - 10857133212749050668ull, 11914116902843745018ull, 8338386234307664155ull, - 7693563294105136383ull, 4393637264257635043ull, 10817114663804369156ull, - 777552857201851974ull, 4318860778166919945ull, 3440765181079071375ull, - 18150517180267406702ull, 3822860841058540379ull, 18408233930596919548ull, - 15483250115370548957ull, 12187081253452044158ull, 15096704468692058143ull, - 351395931644162269ull, 7681099562665395463ull, 621673586916640985ull, - 1548670563434988819ull, 15018421197641490508ull, 14843102830690661585ull, - 18078373819427574057ull, 2156517456339179906ull, 1550063316561336971ull, - 682120972313074028ull, 13222505441996051255ull, 894762928574935768ull, - 6921956189824964594ull, 1470489317032329184ull, 3757845431780800368ull, - 4291423602824827155ull, 11888096106209469513ull, 3227573574798329590ull, - 13089038313828989382ull, 7508021719959037954ull, 2308535366701241976ull, - 1246288425489315813ull, 11433845872149110256ull, 10295145752395079770ull, - 2890366903333661801ull, 9960659940131424438ull, 14272369748982538569ull, - 17076452457032941051ull, 3424791957006073618ull, 1811248193508923525ull, - 14588001261256884000ull, 13923059957978311005ull, 16983695715066873498ull, - 608375160627328848ull, 8684084180111108347ull, 15679623628995063704ull, - 8464528085679884071ull, 14932854744042668023ull, 569982362832841031ull, - 7496415600731368183ull, 6021406431901867511ull, 14375074717677642639ull, - 15805523719787038613ull, 12126947588968916209ull, 11019915590207223465ull, - 12152802468277950710ull, 9974141365018685887ull, 7966238851102234546ull, - 9747060662483912123ull, 1717605098271755502ull, 9342227818610348684ull, - 2114030550145565950ull, 418023209795270565ull, 15770927766697271034ull, - 12606063420003019757ull, 1303915722987961013ull, 11281002575808296045ull, - 5386973648178682980ull, 7792848154392720004ull, 65916750986234223ull, - 13869421282421271494ull, + 17216550202785438742ull, 888925525783100684ull, 14971086878838481907ull, + 1294272531176600360ull, 1886455716578836766ull, 11637635059042252090ull, + 17515667699609373101ull, 7000083051162580056ull, 9300983128218736014ull, + 9958894908037006139ull, 1635532543853096379ull, 16017861459144616409ull, + 17338287424821881179ull, 1983186311692138031ull, 9090376350238380314ull, + 12881684737752619620ull, 15135573007448004119ull, 9716971973878048845ull, + 1434832193067712479ull, 13391945418166012328ull, 4222942739049503617ull, + 14263738008833632447ull, 13268772213335834263ull, 5997455233863395761ull, + 1068503963899776944ull, 17141612644617594564ull, 9871144343592514033ull, + 5276840031980785269ull, 15355133066636605819ull, 2728946953552743916ull, + 15136838873153049319ull, 5645466354956180049ull, 18409791973237644079ull, + 14278855388951708356ull, 17266360427613746801ull, 18261188449347737966ull, + 10896461346494157758ull, 16041849805505875804ull, 10185251185371103195ull, + 1646822496082916590ull, 7947291941730160766ull, 8578518959639923799ull, + 18335246465501349816ull, 9718231080356630143ull, 16219609048603415226ull, + 10959371403202421450ull, 17602634145882978471ull, 8425686225706397430ull, + 12708049332763053506ull, 7627006390785584896ull, 8859726068956568565ull, + 3362393005477244925ull, 5181063662459076181ull, 11384647369363252140ull, + 14541944896990249375ull, 2314324140859013727ull, 13411671664513209746ull, + 5868706585110359110ull, 10711939723010575336ull, 3729675833838621372ull, + 9243393460869700874ull, 17371699057649433318ull, 3997723996730063192ull, + 11433050125327522239ull, 12541125813245406968ull, 9039672968915681536ull, + 15608011746644681292ull, 5054687594698147473ull, 3477709473812172570ull, + 10152311901395946216ull, 8891349751852823074ull, 16144546084129249441ull, + 13685016483773539513ull, 13522218457313117986ull, 4995503363249481535ull, + 16748306406958785033ull, 9311515972501057953ull, 7365920725454078940ull, + 8173149860273300269ull, 3129205185712362172ull, 10311992612364215267ull, + 6430081021816923700ull, 3812940485274326802ull, 12258588714698561684ull, + 16977065476345962723ull, 16840034821380621151ull, 196921697771132750ull, + 1532115488081461896ull, 11299803424734145910ull, 843050419706294665ull, + 3060973841661433401ull, 1061493805797114660ull, 11945392687849806220ull, + 2644275880755387635ull, 18268997097508906988ull, 10440378040220682715ull, + 5460631613847058284ull, 90541039206026679ull, 1525179726704156361ull, + 4568196733230345761ull, 4900564848107415469ull, 905839454594031045ull, + 5600806767076858089ull, 15853491948794316602ull, 4436926389284220729ull, + 12484426071009033621ull, 14565732008483823358ull, 5421750991390559718ull, + 16654253476033026771ull, 4914934475284686956ull, 11353492294670403361ull, + 2952007191267046267ull, 4137400735535674785ull, 13313570887318196803ull, + 16333832917038083335ull, 15983016049430097831ull, 16516441775145748186ull, + 9980463146813617446ull, 14892416906114576210ull, 6560664027287701548ull, + 11333039054045879633ull, 10624127326473164730ull, 10585896247857052348ull, + 9715815904094644978ull, 12043177962550059801ull, 6319376608257543486ull, + 16904749238070947842ull, 6095240370596913147ull, 4788859803642606588ull, + 6996529386998857627ull, 4078288008260849702ull, 4804551383567784305ull, + 9950943025990090361ull, 884570081640732608ull, 626856908236235819ull, + 6383254879440463492ull, 17222449793445921158ull, 12876151944089409278ull, + 664057026045247402ull, 14473992423923157020ull, 16134349783577291380ull, + 17824754404789806578ull, 11922334275858293940ull, 12324529413121065212ull, + 12586161209074725511ull, 17841333238070299986ull, 4939054549376478538ull, + 12653974792798479453ull, 168169012948749036ull, 1600379719717813316ull, + 9597243467462794310ull, 746366577121939789ull, 4939870284827214452ull, + 14763601424926594359ull, 14442078822824496949ull, 10722539175905485350ull, + 1668204868435100495ull, 3125339228212761319ull, 12970829842683432868ull, + 14563213551732184257ull, }; // expectation corrected (kChains_reads, 160 of 160 values), because v19's broker-state hash folds the closed rows as their count and a running digest, each row once when it is final (pineforge-broker-state/v19); this host projects one fixed execution hash, so the continuation does not enter; the trades did not move; re-harvested the same way on INT19's tree; V19-A's tip (6211dc94) gives the same rows: // 5257511505227357930ull -> 16535074925318602333ull @@ -2908,66 +3507,228 @@ constexpr std::uint64_t kChains_rows[] = { // 17398681471387928472ull -> 7546140302872708152ull // 4358702080325541764ull -> 11057158687385367268ull // 7087356504073994303ull -> 930681743930080351ull +// expectation corrected (INT26 v19 hash re-pin, kChains_reads, 160 of 160 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 9570007664485953953ull -> 10110664357585261028ull [H-THIN] +// 1562834205308840657ull -> 5809989452861246834ull [H-THIN] +// 9925417414736888460ull -> 8219150533614246984ull [H-THIN] +// 16797489091140150745ull -> 9160394247003716585ull [H-THIN] +// 8864771617498668987ull -> 8016378449870904253ull [H-THIN] +// 10813620960703012830ull -> 9413734555269737202ull [H-THIN] +// 9917769484038902883ull -> 10408793874733580811ull [H-THIN] +// 11399566133788975554ull -> 2285942971403387014ull [H-THIN] +// 13065799061037800748ull -> 7421889705292008284ull [H-THIN] +// 10473842100614025729ull -> 17682777079617910737ull [H-THIN] +// 10356315431649319903ull -> 4481707948696649371ull [H-THIN] +// 6718697686702398031ull -> 7827336907453999164ull [H-THIN] +// 3085131610026641094ull -> 14974364552734208895ull [H-THIN] +// 7920913286497161545ull -> 13128401401658011940ull [H-THIN] +// 6965112488735434783ull -> 17827869807111927604ull [H-THIN] +// 17824918572352876790ull -> 9719840719700136041ull [H-THIN] +// 431137757590538768ull -> 2665859956187061075ull [H-THIN] +// 5096278457151630514ull -> 9254561921243745847ull [H-THIN] +// 16629172470762388288ull -> 2360259598999100592ull [H-THIN] +// 11652117991390508417ull -> 12205711579178689313ull [H-THIN] +// 2618729299425190784ull -> 14232976407316743690ull [H-THIN] +// 212332156515024145ull -> 5965032367641075215ull [H-THIN] +// 12246593551859292419ull -> 14573502756353459737ull [H-THIN] +// 12545038087661601474ull -> 15400971490512734092ull [H-THIN] +// 1460766259544383584ull -> 4098087993772423194ull [H-THIN] +// 10861778317578249995ull -> 6112934256622287033ull [H-THIN] +// 20932238954851345ull -> 15862596503276202833ull [H-THIN] +// 18286529377979065696ull -> 13670568930995334427ull [H-THIN] +// 5666326454301174821ull -> 9788130664428183490ull [H-THIN] +// 6228635006760178710ull -> 16335392583796627193ull [H-THIN] +// 9906791842642905532ull -> 4410190916080756281ull [H-THIN] +// 2721687798792728445ull -> 12834531680111998344ull [H-THIN] +// 16653381899846261400ull -> 14109653350817882099ull [H-THIN] +// 16503363491014194836ull -> 7442946405223661239ull [H-THIN] +// 5987209830733009584ull -> 4479757411535168501ull [H-THIN] +// 11879508822085901633ull -> 1056910844796346930ull [H-THIN] +// 4428380683177019699ull -> 10866647835240895022ull [H-THIN] +// 6702580677339138469ull -> 8626263364100578116ull [H-THIN] +// 12470873918402201930ull -> 5095382084298433023ull [H-THIN] +// 7755601604835325521ull -> 2483955861156609744ull [H-THIN] +// 17100550686359839522ull -> 10313301897794116551ull [H-THIN] +// 9982417146672614117ull -> 8562911020376654396ull [H-THIN] +// 16219053929893216833ull -> 8063731069482460736ull [H-THIN] +// 8335500495109512692ull -> 11518609544766773477ull [H-THIN] +// 17499407126158105512ull -> 5810028710378801057ull [H-THIN] +// 8520742716124176357ull -> 61058730138114860ull [H-THIN] +// 6969237881725548098ull -> 2100804384988582495ull [H-THIN] +// 6167898056600340972ull -> 7209163061824649505ull [H-THIN] +// 10796080984723706778ull -> 4878378036811209549ull [H-THIN] +// 2659129401831175873ull -> 4404719757894447566ull [H-THIN] +// 4113136533365713821ull -> 4720502478547523234ull [H-THIN] +// 16959859587150714548ull -> 1753398606656246895ull [H-THIN] +// 11977596023171988133ull -> 16246112928648551635ull [H-THIN] +// 3997121899712306414ull -> 10657174667652890190ull [H-THIN] +// 302851387997930277ull -> 14743154045868530737ull [H-THIN] +// 7004784366449869436ull -> 6268694762545713086ull [H-THIN] +// 17731084291599622060ull -> 16976140361266671940ull [H-THIN] +// 2201184572610804096ull -> 10119949631905980432ull [H-THIN] +// 15700916709431740066ull -> 11203266787989620834ull [H-THIN] +// 4158286781895941687ull -> 17512206047802952924ull [H-THIN] +// 5992524740679929691ull -> 13293769686508049256ull [H-THIN] +// 1449426275268650261ull -> 8545075081067204330ull [H-THIN] +// 18162824504997853425ull -> 12091509383956641936ull [H-THIN] +// 14899520499454266758ull -> 9675593530383755541ull [H-THIN] +// 13393817499128108248ull -> 8212271365170382599ull [H-THIN] +// 10711743217016099970ull -> 16962447560998690087ull [H-THIN] +// 2691444242389935931ull -> 1232604283450205942ull [H-THIN] +// 8294060884410393919ull -> 2707154508237646712ull [H-THIN] +// 12352805812450672776ull -> 11313850582244987544ull [H-THIN] +// 696110624400704576ull -> 14369487380029217132ull [H-THIN] +// 10843628086540515552ull -> 11921066809935672874ull [H-THIN] +// 8971856522667982633ull -> 14007992411558869707ull [H-THIN] +// 11210433033614009510ull -> 6598319271298697372ull [H-THIN] +// 8384649526502906040ull -> 5417331013900840530ull [H-THIN] +// 2393883974530973309ull -> 13345232768434677145ull [H-THIN] +// 4665626305905607638ull -> 1935249115057575465ull [H-THIN] +// 8867654732330596907ull -> 13220457444093362632ull [H-THIN] +// 4416041359039881293ull -> 13358973226079502740ull [H-THIN] +// 13719154741412155838ull -> 2889830699990711661ull [H-THIN] +// 7098510039070530028ull -> 1211171047890347763ull [H-THIN] +// 10831403092179419039ull -> 18014776936753683306ull [H-THIN] +// 8578164481500217838ull -> 14390427333071081721ull [H-THIN] +// 14156042948797799583ull -> 7011925151867020864ull [H-THIN] +// 18079113731505369907ull -> 3169302683092777636ull [H-THIN] +// 11217579006112077760ull -> 8884683607532542707ull [H-THIN] +// 578274504487938291ull -> 1220390021601311746ull [H-THIN] +// 12858268482564452648ull -> 4563751557195908857ull [H-THIN] +// 13457093904989238219ull -> 9371430556201687572ull [H-THIN] +// 7535108913864740263ull -> 8755857123799478610ull [H-THIN] +// 4941944547036948489ull -> 11347889350981525264ull [H-THIN] +// 15143761260194159051ull -> 2948337259249410352ull [H-THIN] +// 3169992748722096748ull -> 16392540589482694957ull [H-THIN] +// 2935645212104261694ull -> 13084006063425074665ull [H-THIN] +// 1503156579032907045ull -> 13166369752734976728ull [H-THIN] +// 12825108615254555255ull -> 751990060505666340ull [H-THIN] +// 11152144432285654023ull -> 5970943418956605684ull [H-THIN] +// 14374425704408319362ull -> 10521883552184644999ull [H-THIN] +// 13181125330469706258ull -> 16052318444984852327ull [H-THIN] +// 8173826051536434519ull -> 11187570176199389997ull [H-THIN] +// 3445997924383545490ull -> 15460763283392291862ull [H-THIN] +// 6587016227990886659ull -> 10338663625416646013ull [H-THIN] +// 4779228536467194685ull -> 4881396789477267769ull [H-THIN] +// 13151647749864074127ull -> 4212938202001384025ull [H-THIN] +// 9173722582032837076ull -> 14988355991036891650ull [H-THIN] +// 14434113364655604703ull -> 17837779332496626471ull [H-THIN] +// 749323963540978962ull -> 11682505807010385846ull [H-THIN] +// 9886586197280394206ull -> 17870499886893847586ull [H-THIN] +// 17593161773407266268ull -> 679199023029947373ull [H-THIN] +// 16651394056260196842ull -> 3970199140902816509ull [H-THIN] +// 5523306718191251504ull -> 17401534532171766295ull [H-THIN] +// 12137483147531194660ull -> 775181258658200153ull [H-THIN] +// 1385603333851191898ull -> 16283811115647142047ull [H-THIN] +// 4496586139598675537ull -> 1589955952552870168ull [H-THIN] +// 8942420997722419200ull -> 1739626929334374295ull [H-THIN] +// 16186100995544900609ull -> 14787527140441919317ull [H-THIN] +// 10571664598865577953ull -> 9655802592323111739ull [H-THIN] +// 9783208691725612411ull -> 5654513164848938839ull [H-THIN] +// 17727619977969867138ull -> 10659820542718417314ull [H-THIN] +// 12308607250191287290ull -> 7500865454428219602ull [H-THIN] +// 13136936254003785978ull -> 14944030626459508318ull [H-THIN] +// 4799706175857105538ull -> 8358528417073095116ull [H-THIN] +// 15342824296389601806ull -> 12655664015256093170ull [H-THIN] +// 10278727004657844890ull -> 13209887606846213484ull [H-THIN] +// 40192321974480982ull -> 6069560199676759525ull [H-THIN] +// 9783890578221359931ull -> 6696082621071745728ull [H-THIN] +// 11215532213811366070ull -> 757716102432190821ull [H-THIN] +// 14480169342549825019ull -> 15487397720156294866ull [H-THIN] +// 15414792187977659182ull -> 884148487135854651ull [H-THIN] +// 8493082597081956684ull -> 3880366816499564477ull [H-THIN] +// 8564012831232873370ull -> 15047621949732128129ull [H-THIN] +// 5491576330870414255ull -> 9322903483901568796ull [H-THIN] +// 14022487705176382100ull -> 687002260497979135ull [H-THIN] +// 4124805857279107720ull -> 9491707471648859049ull [H-THIN] +// 15467889468829316615ull -> 8185964782213665656ull [H-THIN] +// 7295497316328640107ull -> 1858930161053607088ull [H-THIN] +// 15082002607429366135ull -> 1233846040989093404ull [H-THIN] +// 12499158975010305465ull -> 6120927370816337024ull [H-THIN] +// 3208794190730501637ull -> 4886803687938088384ull [H-THIN] +// 15064216799612457293ull -> 16477410875523784452ull [H-THIN] +// 16974691537614575812ull -> 4392674394804262923ull [H-THIN] +// 1426687806059238508ull -> 5769985102691339939ull [H-THIN] +// 17375680129214845717ull -> 11070202918036379862ull [H-THIN] +// 12487972796056251866ull -> 18160274419371647815ull [H-THIN] +// 8080964710494461719ull -> 8829636490162765266ull [H-THIN] +// 1415692574356635417ull -> 6601601345405395420ull [H-THIN] +// 9772128015242412363ull -> 2898341262682614174ull [H-THIN] +// 18331800931612105183ull -> 16796631727010079735ull [H-THIN] +// 15912555856562133268ull -> 15187481886734015430ull [H-THIN] +// 16674262793511329036ull -> 18259141933065442106ull [H-THIN] +// 12709672117837958972ull -> 7779036192932713702ull [H-THIN] +// 533937075939181997ull -> 16368000199937753609ull [H-THIN] +// 13997036310862179778ull -> 6523761455165153646ull [H-THIN] +// 2303353295064390890ull -> 7426464792584462270ull [H-THIN] +// 13136856999282398145ull -> 5887376615432929807ull [H-THIN] +// 9661353626625839895ull -> 6126763591804873111ull [H-THIN] +// 7809505236198215515ull -> 7982613224094165644ull [H-THIN] +// 9689360779147915768ull -> 3510856804336884675ull [H-THIN] +// 7546140302872708152ull -> 14582091139041748587ull [H-THIN] +// 11057158687385367268ull -> 1120801175490593191ull [H-THIN] +// 930681743930080351ull -> 2552028032359677900ull [H-THIN] constexpr std::uint64_t kChains_reads[] = { - 9570007664485953953ull, 1562834205308840657ull, 9925417414736888460ull, - 16797489091140150745ull, 8864771617498668987ull, 10813620960703012830ull, - 9917769484038902883ull, 11399566133788975554ull, 13065799061037800748ull, - 10473842100614025729ull, 10356315431649319903ull, 6718697686702398031ull, - 3085131610026641094ull, 7920913286497161545ull, 6965112488735434783ull, - 17824918572352876790ull, 431137757590538768ull, 5096278457151630514ull, - 16629172470762388288ull, 11652117991390508417ull, 2618729299425190784ull, - 212332156515024145ull, 12246593551859292419ull, 12545038087661601474ull, - 1460766259544383584ull, 10861778317578249995ull, 20932238954851345ull, - 18286529377979065696ull, 5666326454301174821ull, 6228635006760178710ull, - 9906791842642905532ull, 2721687798792728445ull, 16653381899846261400ull, - 16503363491014194836ull, 5987209830733009584ull, 11879508822085901633ull, - 4428380683177019699ull, 6702580677339138469ull, 12470873918402201930ull, - 7755601604835325521ull, 17100550686359839522ull, 9982417146672614117ull, - 16219053929893216833ull, 8335500495109512692ull, 17499407126158105512ull, - 8520742716124176357ull, 6969237881725548098ull, 6167898056600340972ull, - 10796080984723706778ull, 2659129401831175873ull, 4113136533365713821ull, - 16959859587150714548ull, 11977596023171988133ull, 3997121899712306414ull, - 302851387997930277ull, 7004784366449869436ull, 17731084291599622060ull, - 2201184572610804096ull, 15700916709431740066ull, 4158286781895941687ull, - 5992524740679929691ull, 1449426275268650261ull, 18162824504997853425ull, - 14899520499454266758ull, 13393817499128108248ull, 10711743217016099970ull, - 2691444242389935931ull, 8294060884410393919ull, 12352805812450672776ull, - 696110624400704576ull, 10843628086540515552ull, 8971856522667982633ull, - 11210433033614009510ull, 8384649526502906040ull, 2393883974530973309ull, - 4665626305905607638ull, 8867654732330596907ull, 4416041359039881293ull, - 13719154741412155838ull, 7098510039070530028ull, 10831403092179419039ull, - 8578164481500217838ull, 14156042948797799583ull, 18079113731505369907ull, - 11217579006112077760ull, 578274504487938291ull, 12858268482564452648ull, - 13457093904989238219ull, 7535108913864740263ull, 4941944547036948489ull, - 15143761260194159051ull, 3169992748722096748ull, 2935645212104261694ull, - 1503156579032907045ull, 12825108615254555255ull, 11152144432285654023ull, - 14374425704408319362ull, 13181125330469706258ull, 8173826051536434519ull, - 3445997924383545490ull, 6587016227990886659ull, 4779228536467194685ull, - 13151647749864074127ull, 9173722582032837076ull, 14434113364655604703ull, - 749323963540978962ull, 9886586197280394206ull, 17593161773407266268ull, - 16651394056260196842ull, 5523306718191251504ull, 12137483147531194660ull, - 1385603333851191898ull, 4496586139598675537ull, 8942420997722419200ull, - 16186100995544900609ull, 10571664598865577953ull, 9783208691725612411ull, - 17727619977969867138ull, 12308607250191287290ull, 13136936254003785978ull, - 4799706175857105538ull, 15342824296389601806ull, 10278727004657844890ull, - 40192321974480982ull, 9783890578221359931ull, 11215532213811366070ull, - 14480169342549825019ull, 15414792187977659182ull, 8493082597081956684ull, - 8564012831232873370ull, 5491576330870414255ull, 14022487705176382100ull, - 4124805857279107720ull, 15467889468829316615ull, 7295497316328640107ull, - 15082002607429366135ull, 12499158975010305465ull, 3208794190730501637ull, - 15064216799612457293ull, 16974691537614575812ull, 1426687806059238508ull, - 17375680129214845717ull, 12487972796056251866ull, 8080964710494461719ull, - 1415692574356635417ull, 9772128015242412363ull, 18331800931612105183ull, - 15912555856562133268ull, 16674262793511329036ull, 12709672117837958972ull, - 533937075939181997ull, 13997036310862179778ull, 2303353295064390890ull, - 13136856999282398145ull, 9661353626625839895ull, 7809505236198215515ull, - 9689360779147915768ull, 7546140302872708152ull, 11057158687385367268ull, - 930681743930080351ull, + 10110664357585261028ull, 5809989452861246834ull, 8219150533614246984ull, + 9160394247003716585ull, 8016378449870904253ull, 9413734555269737202ull, + 10408793874733580811ull, 2285942971403387014ull, 7421889705292008284ull, + 17682777079617910737ull, 4481707948696649371ull, 7827336907453999164ull, + 14974364552734208895ull, 13128401401658011940ull, 17827869807111927604ull, + 9719840719700136041ull, 2665859956187061075ull, 9254561921243745847ull, + 2360259598999100592ull, 12205711579178689313ull, 14232976407316743690ull, + 5965032367641075215ull, 14573502756353459737ull, 15400971490512734092ull, + 4098087993772423194ull, 6112934256622287033ull, 15862596503276202833ull, + 13670568930995334427ull, 9788130664428183490ull, 16335392583796627193ull, + 4410190916080756281ull, 12834531680111998344ull, 14109653350817882099ull, + 7442946405223661239ull, 4479757411535168501ull, 1056910844796346930ull, + 10866647835240895022ull, 8626263364100578116ull, 5095382084298433023ull, + 2483955861156609744ull, 10313301897794116551ull, 8562911020376654396ull, + 8063731069482460736ull, 11518609544766773477ull, 5810028710378801057ull, + 61058730138114860ull, 2100804384988582495ull, 7209163061824649505ull, + 4878378036811209549ull, 4404719757894447566ull, 4720502478547523234ull, + 1753398606656246895ull, 16246112928648551635ull, 10657174667652890190ull, + 14743154045868530737ull, 6268694762545713086ull, 16976140361266671940ull, + 10119949631905980432ull, 11203266787989620834ull, 17512206047802952924ull, + 13293769686508049256ull, 8545075081067204330ull, 12091509383956641936ull, + 9675593530383755541ull, 8212271365170382599ull, 16962447560998690087ull, + 1232604283450205942ull, 2707154508237646712ull, 11313850582244987544ull, + 14369487380029217132ull, 11921066809935672874ull, 14007992411558869707ull, + 6598319271298697372ull, 5417331013900840530ull, 13345232768434677145ull, + 1935249115057575465ull, 13220457444093362632ull, 13358973226079502740ull, + 2889830699990711661ull, 1211171047890347763ull, 18014776936753683306ull, + 14390427333071081721ull, 7011925151867020864ull, 3169302683092777636ull, + 8884683607532542707ull, 1220390021601311746ull, 4563751557195908857ull, + 9371430556201687572ull, 8755857123799478610ull, 11347889350981525264ull, + 2948337259249410352ull, 16392540589482694957ull, 13084006063425074665ull, + 13166369752734976728ull, 751990060505666340ull, 5970943418956605684ull, + 10521883552184644999ull, 16052318444984852327ull, 11187570176199389997ull, + 15460763283392291862ull, 10338663625416646013ull, 4881396789477267769ull, + 4212938202001384025ull, 14988355991036891650ull, 17837779332496626471ull, + 11682505807010385846ull, 17870499886893847586ull, 679199023029947373ull, + 3970199140902816509ull, 17401534532171766295ull, 775181258658200153ull, + 16283811115647142047ull, 1589955952552870168ull, 1739626929334374295ull, + 14787527140441919317ull, 9655802592323111739ull, 5654513164848938839ull, + 10659820542718417314ull, 7500865454428219602ull, 14944030626459508318ull, + 8358528417073095116ull, 12655664015256093170ull, 13209887606846213484ull, + 6069560199676759525ull, 6696082621071745728ull, 757716102432190821ull, + 15487397720156294866ull, 884148487135854651ull, 3880366816499564477ull, + 15047621949732128129ull, 9322903483901568796ull, 687002260497979135ull, + 9491707471648859049ull, 8185964782213665656ull, 1858930161053607088ull, + 1233846040989093404ull, 6120927370816337024ull, 4886803687938088384ull, + 16477410875523784452ull, 4392674394804262923ull, 5769985102691339939ull, + 11070202918036379862ull, 18160274419371647815ull, 8829636490162765266ull, + 6601601345405395420ull, 2898341262682614174ull, 16796631727010079735ull, + 15187481886734015430ull, 18259141933065442106ull, 7779036192932713702ull, + 16368000199937753609ull, 6523761455165153646ull, 7426464792584462270ull, + 5887376615432929807ull, 6126763591804873111ull, 7982613224094165644ull, + 3510856804336884675ull, 14582091139041748587ull, 1120801175490593191ull, + 2552028032359677900ull, }; // expectation corrected: kChains_final 9932486288748772686ull -> 1521244548062449214ull, because v19's broker-state hash folds the closed rows as their count and a running digest, each row once when it is final (pineforge-broker-state/v19); this host projects one fixed execution hash, so the continuation does not enter; the trades did not move; re-harvested the same way on INT19's tree; V19-A's tip (6211dc94) gives the same rows. // expectation corrected (v19-E): kChains_final 1521244548062449214ull -> 7955431514292085542ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kChains_final 7955431514292085542ull -> 13869421282421271494ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kChains_final = 13869421282421271494ull; +// expectation corrected (INT26 v19 hash re-pin): kChains_final 13869421282421271494ull -> 14563213551732184257ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move. +constexpr std::uint64_t kChains_final = 14563213551732184257ull; constexpr Trade kChains_trades[] = { {1736121720000LL, 1736122260000LL, 102.75, 111, 1, 0}, {1736122680000LL, 1736123220000LL, 113.75, 118, 1, 0}, @@ -3268,39 +4029,136 @@ constexpr Trade kChains_trades[] = { // 11285362832740584634ull -> 2471724750358094554ull // 1235712972626989438ull -> 11160534888434597406ull // 7666575660803139734ull -> 10114708526056156918ull +// expectation corrected (INT26 v19 hash re-pin, kMargin_rows, 96 of 96 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 9541482286738450374ull -> 15705115564979405549ull [H-THIN] +// 10604897191185230729ull -> 2899650352150822234ull [H-THIN] +// 10430957227712956259ull -> 7276138441216849650ull [H-THIN] +// 17038118833282111132ull -> 10214260769585446445ull [H-THIN] +// 6880506736019270193ull -> 12785408784277752740ull [H-THIN] +// 2574896616226754364ull -> 1444456431648962701ull [H-THIN] +// 14467806575653580994ull -> 10436554947470791241ull [H-THIN] +// 13935231533587438999ull -> 2225406636414512896ull [H-THIN] +// 1848048834540881643ull -> 15643906313119105108ull [H-THIN] +// 18329579972079141513ull -> 9422865728073085008ull [H-THIN] +// 12355688239988236079ull -> 3330174464320359216ull [H-THIN] +// 10991245773765040855ull -> 6235550293720256144ull [H-THIN] +// 5685214256158276764ull -> 2776878453603156051ull [H-THIN] +// 17160692497390069334ull -> 10067815237117653281ull [H-THIN] +// 4009833773659380105ull -> 13418795566150795168ull [H-THIN] +// 13777884366772416026ull -> 16965503610107780005ull [H-THIN] +// 8958056086241362995ull -> 15857294378993443508ull [H-THIN] +// 12207636356642772400ull -> 14981195424080413801ull [H-THIN] +// 3459076114230420156ull -> 4436574227947160453ull [H-THIN] +// 14921426272357766472ull -> 3461714965409448097ull [H-THIN] +// 2345813046963814958ull -> 17713706949036740901ull [H-THIN] +// 285001323442461930ull -> 11924616437869767087ull [H-THIN] +// 16917342746817816499ull -> 10848033720136413570ull [H-THIN] +// 18052258256818146447ull -> 2042183915997550400ull [H-THIN] +// 4781946747460841696ull -> 2178193614679359335ull [H-THIN] +// 8888939313581979724ull -> 17907135566382286865ull [H-THIN] +// 17314806764712940533ull -> 3689997758302326782ull [H-THIN] +// 4508121597259689478ull -> 15415188862026868395ull [H-THIN] +// 11060315425557244321ull -> 2445419420491494904ull [H-THIN] +// 14574236380430121079ull -> 5012856126461730798ull [H-THIN] +// 12392320350472046194ull -> 8792557501267046029ull [H-THIN] +// 12250313226238695007ull -> 16418141106963966556ull [H-THIN] +// 11116092899196726058ull -> 4540186183786023695ull [H-THIN] +// 3440822300002062498ull -> 4565832655366886747ull [H-THIN] +// 6172478543095956935ull -> 16628078784743176294ull [H-THIN] +// 11864943854655903053ull -> 5822402081127036958ull [H-THIN] +// 7350714231396417061ull -> 15419312725777085722ull [H-THIN] +// 18236477525793941949ull -> 2939233122312242652ull [H-THIN] +// 18289535579996812515ull -> 6678780917008540002ull [H-THIN] +// 15597457755908201189ull -> 16097482011424548464ull [H-THIN] +// 11910235219191833807ull -> 30760358683421794ull [H-THIN] +// 9451964279490960683ull -> 14579653650355389700ull [H-THIN] +// 4710825118275752110ull -> 10818557034142368397ull [H-THIN] +// 12851808306320095878ull -> 5905237345792316559ull [H-THIN] +// 8820052433058937619ull -> 10793423326720317090ull [H-THIN] +// 15533164234013109494ull -> 880325936979180181ull [H-THIN] +// 16547449805254710983ull -> 15185473436805269358ull [H-THIN] +// 2596701226294842163ull -> 15710388520607918584ull [H-THIN] +// 17704360989892006448ull -> 3850576270060304957ull [H-THIN] +// 5312178085029999460ull -> 1178190546578254837ull [H-THIN] +// 681706230469294026ull -> 13171343461724749261ull [H-THIN] +// 11208354125027713564ull -> 11067978420491083409ull [H-THIN] +// 13848046050944922829ull -> 11720479467561892020ull [H-THIN] +// 16629321510727683252ull -> 15138731064688668601ull [H-THIN] +// 489654745066395968ull -> 8511910500217359553ull [H-THIN] +// 17259859321488242835ull -> 8297079889818538858ull [H-THIN] +// 15549072595885790096ull -> 11644861686947971857ull [H-THIN] +// 1505011103482109555ull -> 17004698771171900722ull [H-THIN] +// 15289097285747984347ull -> 18287878227163962874ull [H-THIN] +// 1572915281639359538ull -> 894010446558868231ull [H-THIN] +// 11273305892586396472ull -> 17837072773009990909ull [H-THIN] +// 13328838480036624025ull -> 8467609174260254948ull [H-THIN] +// 13633704569586434331ull -> 2211154383026824986ull [H-THIN] +// 17431948770960738100ull -> 9489292208093108441ull [H-THIN] +// 12155616429848711764ull -> 7841002755427896445ull [H-THIN] +// 14869620479028624382ull -> 12404306619029088135ull [H-THIN] +// 6849688694600040229ull -> 12933743527246336464ull [H-THIN] +// 11485893754569323922ull -> 8414768416822022107ull [H-THIN] +// 8360410264576159919ull -> 2802251700686834430ull [H-THIN] +// 461812385935867019ull -> 15879102663736672598ull [H-THIN] +// 2682358547448593291ull -> 5173606717929774458ull [H-THIN] +// 9291455346649087450ull -> 6649901201650792327ull [H-THIN] +// 13216043467298716443ull -> 10171970172678289686ull [H-THIN] +// 359821378104950531ull -> 7545775745369484286ull [H-THIN] +// 16017291574242025244ull -> 13536131498902788541ull [H-THIN] +// 18391126290698081198ull -> 16093469128393328519ull [H-THIN] +// 9855977842531731938ull -> 13705061326497871495ull [H-THIN] +// 5556828515909880503ull -> 12212848506533549122ull [H-THIN] +// 2022314578154758976ull -> 15857272799701280033ull [H-THIN] +// 9416710584143900557ull -> 17560839342008353738ull [H-THIN] +// 6983558299012049585ull -> 7667506666186671346ull [H-THIN] +// 5499696029215209460ull -> 4473219031176272397ull [H-THIN] +// 10875028956400699532ull -> 8077639951555082143ull [H-THIN] +// 2974831859594208707ull -> 14996152118360177682ull [H-THIN] +// 9582163290358494515ull -> 16878410184721456110ull [H-THIN] +// 9192322421103943129ull -> 18147635800513569660ull [H-THIN] +// 11542997064059872401ull -> 2980679299199661728ull [H-THIN] +// 1957400908434967464ull -> 17470380748409866759ull [H-THIN] +// 12038816259661624215ull -> 166277892836680540ull [H-THIN] +// 14854368408513559808ull -> 17231939775446510753ull [H-THIN] +// 4246614380518911616ull -> 42816067293212027ull [H-THIN] +// 2355707666069181745ull -> 3846323461282028320ull [H-THIN] +// 5599822345970336136ull -> 8768462657599765139ull [H-THIN] +// 2471724750358094554ull -> 17231142656948881527ull [H-THIN] +// 11160534888434597406ull -> 10746568987715196899ull [H-THIN] +// 10114708526056156918ull -> 15724008703225232301ull [H-THIN] constexpr std::uint64_t kMargin_rows[] = { - 9541482286738450374ull, 10604897191185230729ull, 10430957227712956259ull, - 17038118833282111132ull, 6880506736019270193ull, 2574896616226754364ull, - 14467806575653580994ull, 13935231533587438999ull, 1848048834540881643ull, - 18329579972079141513ull, 12355688239988236079ull, 10991245773765040855ull, - 5685214256158276764ull, 17160692497390069334ull, 4009833773659380105ull, - 13777884366772416026ull, 8958056086241362995ull, 12207636356642772400ull, - 3459076114230420156ull, 14921426272357766472ull, 2345813046963814958ull, - 285001323442461930ull, 16917342746817816499ull, 18052258256818146447ull, - 4781946747460841696ull, 8888939313581979724ull, 17314806764712940533ull, - 4508121597259689478ull, 11060315425557244321ull, 14574236380430121079ull, - 12392320350472046194ull, 12250313226238695007ull, 11116092899196726058ull, - 3440822300002062498ull, 6172478543095956935ull, 11864943854655903053ull, - 7350714231396417061ull, 18236477525793941949ull, 18289535579996812515ull, - 15597457755908201189ull, 11910235219191833807ull, 9451964279490960683ull, - 4710825118275752110ull, 12851808306320095878ull, 8820052433058937619ull, - 15533164234013109494ull, 16547449805254710983ull, 2596701226294842163ull, - 17704360989892006448ull, 5312178085029999460ull, 681706230469294026ull, - 11208354125027713564ull, 13848046050944922829ull, 16629321510727683252ull, - 489654745066395968ull, 17259859321488242835ull, 15549072595885790096ull, - 1505011103482109555ull, 15289097285747984347ull, 1572915281639359538ull, - 11273305892586396472ull, 13328838480036624025ull, 13633704569586434331ull, - 17431948770960738100ull, 12155616429848711764ull, 14869620479028624382ull, - 6849688694600040229ull, 11485893754569323922ull, 8360410264576159919ull, - 461812385935867019ull, 2682358547448593291ull, 9291455346649087450ull, - 13216043467298716443ull, 359821378104950531ull, 16017291574242025244ull, - 18391126290698081198ull, 9855977842531731938ull, 5556828515909880503ull, - 2022314578154758976ull, 9416710584143900557ull, 6983558299012049585ull, - 5499696029215209460ull, 10875028956400699532ull, 2974831859594208707ull, - 9582163290358494515ull, 9192322421103943129ull, 11542997064059872401ull, - 1957400908434967464ull, 12038816259661624215ull, 14854368408513559808ull, - 4246614380518911616ull, 2355707666069181745ull, 5599822345970336136ull, - 2471724750358094554ull, 11160534888434597406ull, 10114708526056156918ull, + 15705115564979405549ull, 2899650352150822234ull, 7276138441216849650ull, + 10214260769585446445ull, 12785408784277752740ull, 1444456431648962701ull, + 10436554947470791241ull, 2225406636414512896ull, 15643906313119105108ull, + 9422865728073085008ull, 3330174464320359216ull, 6235550293720256144ull, + 2776878453603156051ull, 10067815237117653281ull, 13418795566150795168ull, + 16965503610107780005ull, 15857294378993443508ull, 14981195424080413801ull, + 4436574227947160453ull, 3461714965409448097ull, 17713706949036740901ull, + 11924616437869767087ull, 10848033720136413570ull, 2042183915997550400ull, + 2178193614679359335ull, 17907135566382286865ull, 3689997758302326782ull, + 15415188862026868395ull, 2445419420491494904ull, 5012856126461730798ull, + 8792557501267046029ull, 16418141106963966556ull, 4540186183786023695ull, + 4565832655366886747ull, 16628078784743176294ull, 5822402081127036958ull, + 15419312725777085722ull, 2939233122312242652ull, 6678780917008540002ull, + 16097482011424548464ull, 30760358683421794ull, 14579653650355389700ull, + 10818557034142368397ull, 5905237345792316559ull, 10793423326720317090ull, + 880325936979180181ull, 15185473436805269358ull, 15710388520607918584ull, + 3850576270060304957ull, 1178190546578254837ull, 13171343461724749261ull, + 11067978420491083409ull, 11720479467561892020ull, 15138731064688668601ull, + 8511910500217359553ull, 8297079889818538858ull, 11644861686947971857ull, + 17004698771171900722ull, 18287878227163962874ull, 894010446558868231ull, + 17837072773009990909ull, 8467609174260254948ull, 2211154383026824986ull, + 9489292208093108441ull, 7841002755427896445ull, 12404306619029088135ull, + 12933743527246336464ull, 8414768416822022107ull, 2802251700686834430ull, + 15879102663736672598ull, 5173606717929774458ull, 6649901201650792327ull, + 10171970172678289686ull, 7545775745369484286ull, 13536131498902788541ull, + 16093469128393328519ull, 13705061326497871495ull, 12212848506533549122ull, + 15857272799701280033ull, 17560839342008353738ull, 7667506666186671346ull, + 4473219031176272397ull, 8077639951555082143ull, 14996152118360177682ull, + 16878410184721456110ull, 18147635800513569660ull, 2980679299199661728ull, + 17470380748409866759ull, 166277892836680540ull, 17231939775446510753ull, + 42816067293212027ull, 3846323461282028320ull, 8768462657599765139ull, + 17231142656948881527ull, 10746568987715196899ull, 15724008703225232301ull, }; // expectation corrected (kMargin_reads, 96 of 96 values), because v19's broker-state hash folds the closed rows as their count and a running digest, each row once when it is final (pineforge-broker-state/v19); this host projects one fixed execution hash, so the continuation does not enter; the trades did not move; re-harvested the same way on INT19's tree; V19-A's tip (6211dc94) gives the same rows: // 1986479459587857117ull -> 16598024422536402124ull @@ -3593,44 +4451,142 @@ constexpr std::uint64_t kMargin_rows[] = { // 2750403077460828887ull -> 14397269933515551863ull // 2808017024047588845ull -> 12219341892567703629ull // 8904074757957821441ull -> 2752726173101561857ull +// expectation corrected (INT26 v19 hash re-pin, kMargin_reads, 96 of 96 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 11431893337342736571ull -> 8953842992177039580ull [H-THIN] +// 10771769035459469695ull -> 5012328506798440776ull [H-THIN] +// 1401088416158264207ull -> 9514401216297696498ull [H-THIN] +// 5990666036994939969ull -> 15149183047650585612ull [H-THIN] +// 10588539850905902429ull -> 11881899715350987928ull [H-THIN] +// 9641395323288135458ull -> 1425710761276111275ull [H-THIN] +// 16415676128453702962ull -> 13395351882243880097ull [H-THIN] +// 3214841672955981307ull -> 8260038522419186580ull [H-THIN] +// 9140481207117864186ull -> 10260307603270775333ull [H-THIN] +// 6758874244345350527ull -> 12973871011726024370ull [H-THIN] +// 7448761481640431279ull -> 10839401170472448604ull [H-THIN] +// 11899269473854959745ull -> 10533296012194166894ull [H-THIN] +// 17623756564591150437ull -> 5742434260673778054ull [H-THIN] +// 3283115927808803070ull -> 4811301513092974085ull [H-THIN] +// 18313686748628355321ull -> 6154676715145880256ull [H-THIN] +// 10517443878246423516ull -> 11718623329127978039ull [H-THIN] +// 14391554715371743723ull -> 6950038654804808040ull [H-THIN] +// 7815903094041248998ull -> 13805316801444424371ull [H-THIN] +// 11128615458197398484ull -> 414578511773924817ull [H-THIN] +// 16868017582914550674ull -> 9193643862650800159ull [H-THIN] +// 17318292280187111457ull -> 12190847399375331806ull [H-THIN] +// 236539664791149342ull -> 3867367786885611091ull [H-THIN] +// 12561323379654259441ull -> 6847694618925606604ull [H-THIN] +// 17901584105989623670ull -> 6840674894831935501ull [H-THIN] +// 1819495894130837698ull -> 4507552530758797165ull [H-THIN] +// 12913364709271178836ull -> 3571654135135270865ull [H-THIN] +// 4003529127479307250ull -> 17242678907144422297ull [H-THIN] +// 17850722142819534182ull -> 14611540648022352171ull [H-THIN] +// 10755120431723778073ull -> 5835653396943151368ull [H-THIN] +// 1731312764757835608ull -> 3378973123396229197ull [H-THIN] +// 13748410692460811819ull -> 2553190776568274428ull [H-THIN] +// 12896133474356421192ull -> 13290865301483451699ull [H-THIN] +// 9031531055096256852ull -> 5500621162831131901ull [H-THIN] +// 8322755136826696671ull -> 15654765185370989222ull [H-THIN] +// 8233453449656235425ull -> 15762424456997007744ull [H-THIN] +// 18274892380474204343ull -> 14487448570790088836ull [H-THIN] +// 9098889810842207669ull -> 4035614041046029754ull [H-THIN] +// 11981003970545376949ull -> 15812215809727058388ull [H-THIN] +// 5208192149483445791ull -> 16715990192788820494ull [H-THIN] +// 13826614122871749667ull -> 37321428960284898ull [H-THIN] +// 16775818024782332415ull -> 12666941559515237858ull [H-THIN] +// 15803353690065673036ull -> 1010686153406847467ull [H-THIN] +// 8607451910366293802ull -> 7352920967322298933ull [H-THIN] +// 15510545365186449286ull -> 15055048281012200503ull [H-THIN] +// 5406260086936373546ull -> 5206129815205501175ull [H-THIN] +// 2510786366313163875ull -> 10700009383247120472ull [H-THIN] +// 15538776584588985979ull -> 4162766575253458174ull [H-THIN] +// 12229521714344207135ull -> 9893952837417789690ull [H-THIN] +// 2212599425252585470ull -> 58686700216684675ull [H-THIN] +// 10390126991709196687ull -> 8699555160482580650ull [H-THIN] +// 13200005853939750424ull -> 8130419440091896455ull [H-THIN] +// 6847492913241704581ull -> 4851947396057958324ull [H-THIN] +// 15796662482477301615ull -> 16202831428428662870ull [H-THIN] +// 5118863315631314158ull -> 5121408789427822267ull [H-THIN] +// 4129944798799153557ull -> 11023335017679833516ull [H-THIN] +// 11319393888042736100ull -> 18170820144655294041ull [H-THIN] +// 14187452542143074307ull -> 15954646464954318706ull [H-THIN] +// 1074727836785604464ull -> 18242003564237332097ull [H-THIN] +// 17195817214176484787ull -> 17443602250907707410ull [H-THIN] +// 1825647699469508785ull -> 17983462397568604996ull [H-THIN] +// 4268279429926179501ull -> 7802923710060199472ull [H-THIN] +// 16957209788610236342ull -> 12722038537362695175ull [H-THIN] +// 3395896204678737755ull -> 13698029093670433558ull [H-THIN] +// 3101221902009845683ull -> 13959755018533544898ull [H-THIN] +// 14178655193129027029ull -> 7345845768217414220ull [H-THIN] +// 11497559614257964788ull -> 1775914585091981109ull [H-THIN] +// 2301625628626628663ull -> 11374578665796930850ull [H-THIN] +// 16134456750164415938ull -> 4643448624875098475ull [H-THIN] +// 7927341186129459600ull -> 7678477935539233885ull [H-THIN] +// 11762885871454101194ull -> 7822693233579776275ull [H-THIN] +// 3662730897532267179ull -> 17668989095822341662ull [H-THIN] +// 5485569566800147480ull -> 4575423088079055321ull [H-THIN] +// 11083493162941664231ull -> 1679134401260872502ull [H-THIN] +// 4553141082613530877ull -> 13201168357824320508ull [H-THIN] +// 9461970734864272828ull -> 18041290181318436105ull [H-THIN] +// 14341766711532273518ull -> 4516335812849986987ull [H-THIN] +// 11087165755233401455ull -> 6621116709511450086ull [H-THIN] +// 5001440655061293740ull -> 5258952806381761949ull [H-THIN] +// 15047261415908188989ull -> 6941350654943584452ull [H-THIN] +// 12156664521037383764ull -> 5840696783726564867ull [H-THIN] +// 8898477763532118925ull -> 13717359001813774118ull [H-THIN] +// 17667654592610722150ull -> 17092887286647179443ull [H-THIN] +// 6706429179434019306ull -> 10689502582681467121ull [H-THIN] +// 1344134952986164358ull -> 15622799582419499315ull [H-THIN] +// 7134564545056141913ull -> 1793938846847329952ull [H-THIN] +// 755502931104769977ull -> 8001896748571570600ull [H-THIN] +// 18177522707802576037ull -> 7347984521559791464ull [H-THIN] +// 1635613291101838741ull -> 3770456150226756214ull [H-THIN] +// 15997917115278936545ull -> 10283041979086499618ull [H-THIN] +// 17554126746769759592ull -> 11525649179628871169ull [H-THIN] +// 3806995878355289449ull -> 15518453503092621102ull [H-THIN] +// 15405908675426402054ull -> 4288437931908199143ull [H-THIN] +// 1441245154396427777ull -> 3161816617208773754ull [H-THIN] +// 14397269933515551863ull -> 7892138492494137338ull [H-THIN] +// 12219341892567703629ull -> 8517423913411631616ull [H-THIN] +// 2752726173101561857ull -> 10266993678345749518ull [H-THIN] constexpr std::uint64_t kMargin_reads[] = { - 11431893337342736571ull, 10771769035459469695ull, 1401088416158264207ull, - 5990666036994939969ull, 10588539850905902429ull, 9641395323288135458ull, - 16415676128453702962ull, 3214841672955981307ull, 9140481207117864186ull, - 6758874244345350527ull, 7448761481640431279ull, 11899269473854959745ull, - 17623756564591150437ull, 3283115927808803070ull, 18313686748628355321ull, - 10517443878246423516ull, 14391554715371743723ull, 7815903094041248998ull, - 11128615458197398484ull, 16868017582914550674ull, 17318292280187111457ull, - 236539664791149342ull, 12561323379654259441ull, 17901584105989623670ull, - 1819495894130837698ull, 12913364709271178836ull, 4003529127479307250ull, - 17850722142819534182ull, 10755120431723778073ull, 1731312764757835608ull, - 13748410692460811819ull, 12896133474356421192ull, 9031531055096256852ull, - 8322755136826696671ull, 8233453449656235425ull, 18274892380474204343ull, - 9098889810842207669ull, 11981003970545376949ull, 5208192149483445791ull, - 13826614122871749667ull, 16775818024782332415ull, 15803353690065673036ull, - 8607451910366293802ull, 15510545365186449286ull, 5406260086936373546ull, - 2510786366313163875ull, 15538776584588985979ull, 12229521714344207135ull, - 2212599425252585470ull, 10390126991709196687ull, 13200005853939750424ull, - 6847492913241704581ull, 15796662482477301615ull, 5118863315631314158ull, - 4129944798799153557ull, 11319393888042736100ull, 14187452542143074307ull, - 1074727836785604464ull, 17195817214176484787ull, 1825647699469508785ull, - 4268279429926179501ull, 16957209788610236342ull, 3395896204678737755ull, - 3101221902009845683ull, 14178655193129027029ull, 11497559614257964788ull, - 2301625628626628663ull, 16134456750164415938ull, 7927341186129459600ull, - 11762885871454101194ull, 3662730897532267179ull, 5485569566800147480ull, - 11083493162941664231ull, 4553141082613530877ull, 9461970734864272828ull, - 14341766711532273518ull, 11087165755233401455ull, 5001440655061293740ull, - 15047261415908188989ull, 12156664521037383764ull, 8898477763532118925ull, - 17667654592610722150ull, 6706429179434019306ull, 1344134952986164358ull, - 7134564545056141913ull, 755502931104769977ull, 18177522707802576037ull, - 1635613291101838741ull, 15997917115278936545ull, 17554126746769759592ull, - 3806995878355289449ull, 15405908675426402054ull, 1441245154396427777ull, - 14397269933515551863ull, 12219341892567703629ull, 2752726173101561857ull, + 8953842992177039580ull, 5012328506798440776ull, 9514401216297696498ull, + 15149183047650585612ull, 11881899715350987928ull, 1425710761276111275ull, + 13395351882243880097ull, 8260038522419186580ull, 10260307603270775333ull, + 12973871011726024370ull, 10839401170472448604ull, 10533296012194166894ull, + 5742434260673778054ull, 4811301513092974085ull, 6154676715145880256ull, + 11718623329127978039ull, 6950038654804808040ull, 13805316801444424371ull, + 414578511773924817ull, 9193643862650800159ull, 12190847399375331806ull, + 3867367786885611091ull, 6847694618925606604ull, 6840674894831935501ull, + 4507552530758797165ull, 3571654135135270865ull, 17242678907144422297ull, + 14611540648022352171ull, 5835653396943151368ull, 3378973123396229197ull, + 2553190776568274428ull, 13290865301483451699ull, 5500621162831131901ull, + 15654765185370989222ull, 15762424456997007744ull, 14487448570790088836ull, + 4035614041046029754ull, 15812215809727058388ull, 16715990192788820494ull, + 37321428960284898ull, 12666941559515237858ull, 1010686153406847467ull, + 7352920967322298933ull, 15055048281012200503ull, 5206129815205501175ull, + 10700009383247120472ull, 4162766575253458174ull, 9893952837417789690ull, + 58686700216684675ull, 8699555160482580650ull, 8130419440091896455ull, + 4851947396057958324ull, 16202831428428662870ull, 5121408789427822267ull, + 11023335017679833516ull, 18170820144655294041ull, 15954646464954318706ull, + 18242003564237332097ull, 17443602250907707410ull, 17983462397568604996ull, + 7802923710060199472ull, 12722038537362695175ull, 13698029093670433558ull, + 13959755018533544898ull, 7345845768217414220ull, 1775914585091981109ull, + 11374578665796930850ull, 4643448624875098475ull, 7678477935539233885ull, + 7822693233579776275ull, 17668989095822341662ull, 4575423088079055321ull, + 1679134401260872502ull, 13201168357824320508ull, 18041290181318436105ull, + 4516335812849986987ull, 6621116709511450086ull, 5258952806381761949ull, + 6941350654943584452ull, 5840696783726564867ull, 13717359001813774118ull, + 17092887286647179443ull, 10689502582681467121ull, 15622799582419499315ull, + 1793938846847329952ull, 8001896748571570600ull, 7347984521559791464ull, + 3770456150226756214ull, 10283041979086499618ull, 11525649179628871169ull, + 15518453503092621102ull, 4288437931908199143ull, 3161816617208773754ull, + 7892138492494137338ull, 8517423913411631616ull, 10266993678345749518ull, }; // expectation corrected: kMargin_final 11171469731695420230ull -> 1252183101027139840ull, because v19's broker-state hash folds the closed rows as their count and a running digest, each row once when it is final (pineforge-broker-state/v19); this host projects one fixed execution hash, so the continuation does not enter; the trades did not move; re-harvested the same way on INT19's tree; V19-A's tip (6211dc94) gives the same rows. // expectation corrected (v19-E): kMargin_final 1252183101027139840ull -> 7666575660803139734ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kMargin_final 7666575660803139734ull -> 10114708526056156918ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kMargin_final = 10114708526056156918ull; +// expectation corrected (INT26 v19 hash re-pin): kMargin_final 10114708526056156918ull -> 15724008703225232301ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move. +constexpr std::uint64_t kMargin_final = 15724008703225232301ull; constexpr Trade kMargin_trades[] = { {1736121780000LL, 1736124420000LL, 103, 92, 34.782608695652172, 0}, {1736121780000LL, 1736124600000LL, 103, 87.5, 54.064761904761625, 0}, diff --git a/tests/test_adapter_quiet_bar_pinned.inc b/tests/test_adapter_quiet_bar_pinned.inc index 6135cc32..a85de2e4 100644 --- a/tests/test_adapter_quiet_bar_pinned.inc +++ b/tests/test_adapter_quiet_bar_pinned.inc @@ -1147,277 +1147,550 @@ // Random46/0/0: trades 0x8fe7667934a81f87 -> 0xfaba8060a07d20fd // Random46/1/1: trades 0x8fe7667934a81f87 -> 0xfaba8060a07d20fd // Random46/1/0: trades 0x8fe7667934a81f87 -> 0xfaba8060a07d20fd +// expectation corrected (INT26 v19 hash re-pin, 680 values over 272 rows), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; this host projects one fixed execution hash, so the continuation does not enter; the trades, the trade digest and the three counts did not move: +// Quiet/0/1: boundary 0x1c2b2e37a44f6877ull -> 0xcd8d3ecb4d2c740aull, rows 0x81799b58f0e052b1ull -> 0xe6da4342a82c460cull, final_hash 0xe0aa4c0240939bcfull -> 0x458f1ecf00ef1d5cull [H-THIN] +// Quiet/0/0: boundary 0x1c2b2e37a44f6877ull -> 0xcd8d3ecb4d2c740aull, final_hash 0xe0aa4c0240939bcfull -> 0x458f1ecf00ef1d5cull [H-THIN] +// Quiet/1/1: boundary 0x0a542471817c0616ull -> 0x2a102759d268d84dull, rows 0x4210b9ca5a7d77e6ull -> 0x923a578d640d7fdbull, final_hash 0x2e91db500a83cbe9ull -> 0x6ca064a309be6bfaull [H-THIN] +// Quiet/1/0: boundary 0x0a542471817c0616ull -> 0x2a102759d268d84dull, final_hash 0x2e91db500a83cbe9ull -> 0x6ca064a309be6bfaull [H-THIN] +// HoldLongFull/0/1: boundary 0xbb82b8cd85ffaae1ull -> 0x2be6111eb08aff44ull, rows 0xae96abef4356f11full -> 0x37135fdf24860586ull, final_hash 0xbe87239166611559ull -> 0x6caa6506b43c5228ull [H-THIN] +// HoldLongFull/0/0: boundary 0xbb82b8cd85ffaae1ull -> 0x2be6111eb08aff44ull, final_hash 0xbe87239166611559ull -> 0x6caa6506b43c5228ull [H-THIN] +// HoldLongFull/1/1: boundary 0x83f82e6a0b0a5677ull -> 0x6da42e25a0445e57ull, rows 0x7700c6fdca38234full -> 0xa90a9669249de5f2ull, final_hash 0xb6f097b6a6318731ull -> 0x1fdb374b13f0dd9cull [H-THIN] +// HoldLongFull/1/0: boundary 0x83f82e6a0b0a5677ull -> 0x6da42e25a0445e57ull, final_hash 0xb6f097b6a6318731ull -> 0x1fdb374b13f0dd9cull [H-THIN] +// MarginCalls/0/1: boundary 0x3633b5fd46e3ee1eull -> 0x9ffada7afff423a5ull, rows 0x35e47773037c0f4dull -> 0xf1451bf046aea2aeull, final_hash 0x64d4054219e25f2full -> 0xd58658d95c54acbeull [H-THIN] +// MarginCalls/0/0: boundary 0x3633b5fd46e3ee1eull -> 0x9ffada7afff423a5ull, final_hash 0x64d4054219e25f2full -> 0xd58658d95c54acbeull [H-THIN] +// MarginCalls/1/1: boundary 0x96ca6ef83164c685ull -> 0x548dd443b38f74b0ull, rows 0x4b1bc134021b04cbull -> 0xfdd852b37b07d408ull, final_hash 0xc10c1fc9e88e7c23ull -> 0x76f99b20ecd7e68cull [H-THIN] +// MarginCalls/1/0: boundary 0x96ca6ef83164c685ull -> 0x548dd443b38f74b0ull, final_hash 0xc10c1fc9e88e7c23ull -> 0x76f99b20ecd7e68cull [H-THIN] +// MarginCallCycles/0/1: boundary 0x38656d5e12929625ull -> 0x53630c222ae9b1a8ull, rows 0x5c4aa443e8e76ef9ull -> 0x26e80eef46f3b822ull, final_hash 0x55f80f5340e670b7ull -> 0x9dea3ea1d3cbc012ull [H-THIN] +// MarginCallCycles/0/0: boundary 0x38656d5e12929625ull -> 0x53630c222ae9b1a8ull, final_hash 0x55f80f5340e670b7ull -> 0x9dea3ea1d3cbc012ull [H-THIN] +// MarginCallCycles/1/1: boundary 0xc969906a8f7b57bbull -> 0xd66af1875ed2e747ull, rows 0x67e6e63eb71a661cull -> 0xffa2470471097d1dull, final_hash 0x8e9e930063b52698ull -> 0x22ec1fab2742fa75ull [H-THIN] +// MarginCallCycles/1/0: boundary 0xc969906a8f7b57bbull -> 0xd66af1875ed2e747ull, final_hash 0x8e9e930063b52698ull -> 0x22ec1fab2742fa75ull [H-THIN] +// Bracket/0/1: boundary 0x5ceffdf162a625baull -> 0xaeddc0d29a30f3f7ull, rows 0x2c0b9b25e51c3426ull -> 0xd324958e0a8a53deull, final_hash 0xb62be9f335a690fbull -> 0xef163ce5ad81ccceull [H-THIN] +// Bracket/0/0: boundary 0x5ceffdf162a625baull -> 0xaeddc0d29a30f3f7ull, final_hash 0xb62be9f335a690fbull -> 0xef163ce5ad81ccceull [H-THIN] +// Bracket/1/1: boundary 0x53093fd52be58e6aull -> 0xdb519033b2a6887bull, rows 0x26c6fc455e7a9e3full -> 0xb2b3be51c614016dull, final_hash 0x2fbb7d2d1228c959ull -> 0x488b102fb534087cull [H-THIN] +// Bracket/1/0: boundary 0x53093fd52be58e6aull -> 0xdb519033b2a6887bull, final_hash 0x2fbb7d2d1228c959ull -> 0x488b102fb534087cull [H-THIN] +// Trail/0/1: boundary 0xa09d959161ab83b5ull -> 0xa901a158196e8e90ull, rows 0x6db293f4d9ba407eull -> 0x3bf9ee1972d44776ull, final_hash 0xf9e526fb6a079950ull -> 0xf1d8827b0b6f8fcfull [H-THIN] +// Trail/0/0: boundary 0xa09d959161ab83b5ull -> 0xa901a158196e8e90ull, final_hash 0xf9e526fb6a079950ull -> 0xf1d8827b0b6f8fcfull [H-THIN] +// Trail/1/1: boundary 0x2a39cc8c834a5287ull -> 0xb23147b5951428e2ull, rows 0xe23d175bdbe6d254ull -> 0x4f409e92b7a0d98aull, final_hash 0xffac0df8a2dbc19eull -> 0x2716f35c358b7145ull [H-THIN] +// Trail/1/0: boundary 0x2a39cc8c834a5287ull -> 0xb23147b5951428e2ull, final_hash 0xffac0df8a2dbc19eull -> 0x2716f35c358b7145ull [H-THIN] +// DualStops/0/1: boundary 0xf53e1aa110098577ull -> 0x29701bba6d7182b5ull, rows 0xa45979a387e09947ull -> 0xf109235082c1860eull, final_hash 0x9d20a3a0e3c4b991ull -> 0x34452d46a85f96c6ull [H-THIN] +// DualStops/0/0: boundary 0xf53e1aa110098577ull -> 0x29701bba6d7182b5ull, final_hash 0x9d20a3a0e3c4b991ull -> 0x34452d46a85f96c6ull [H-THIN] +// DualStops/1/1: boundary 0xd56451041c2c2c4full -> 0x86f9654dc6f8ed22ull, rows 0x3525fdbfa98d36bcull -> 0x4393ede3dede0436ull, final_hash 0x0e2c1cdfb96b6301ull -> 0x1bfce4a56d0314f2ull [H-THIN] +// DualStops/1/0: boundary 0xd56451041c2c2c4full -> 0x86f9654dc6f8ed22ull, final_hash 0x0e2c1cdfb96b6301ull -> 0x1bfce4a56d0314f2ull [H-THIN] +// InvertedStops/0/1: boundary 0xa3daa491b43f22e9ull -> 0xeb8c3dfbd9d273a2ull, rows 0x054c4b8ef1709dc3ull -> 0xb7ecf902b2ff0befull, final_hash 0x056329caffb5a22eull -> 0x1993f824c35e154dull [H-THIN] +// InvertedStops/0/0: boundary 0xa3daa491b43f22e9ull -> 0xeb8c3dfbd9d273a2ull, final_hash 0x056329caffb5a22eull -> 0x1993f824c35e154dull [H-THIN] +// InvertedStops/1/1: boundary 0x9b50fac734e90657ull -> 0x976cbd72b16fcb6full, rows 0x2d65179a434eebf0ull -> 0xe49f3d6c9fe39f5bull, final_hash 0xab655e08a941e123ull -> 0x69e468caa11329ccull [H-THIN] +// InvertedStops/1/0: boundary 0x9b50fac734e90657ull -> 0x976cbd72b16fcb6full, final_hash 0xab655e08a941e123ull -> 0x69e468caa11329ccull [H-THIN] +// PendingParent/0/1: boundary 0x47212775031c61c2ull -> 0xc2b9247123313aceull, rows 0xfd5dbde93e8d520aull -> 0xa8f42cb39e1aeff3ull, final_hash 0xe74974df53ef097aull -> 0xfbf54246d7baccdaull [H-THIN] +// PendingParent/0/0: boundary 0x47212775031c61c2ull -> 0xc2b9247123313aceull, final_hash 0xe74974df53ef097aull -> 0xfbf54246d7baccdaull [H-THIN] +// PendingParent/1/1: boundary 0x08f4fa130f86a0daull -> 0xcb795828e625815full, rows 0xbd725b4c1d8be787ull -> 0x808c94697e2231bdull, final_hash 0x27cdc3dedee10a56ull -> 0x0f9d2bb1d6492d5aull [H-THIN] +// PendingParent/1/0: boundary 0x08f4fa130f86a0daull -> 0xcb795828e625815full, final_hash 0x27cdc3dedee10a56ull -> 0x0f9d2bb1d6492d5aull [H-THIN] +// GappedAdd/0/1: boundary 0x8bd71987fffc51e9ull -> 0x2aa05060cf893d51ull, rows 0x42f9dd5f352155d8ull -> 0xca15382fd8d71504ull, final_hash 0xaa98c069e5a019a7ull -> 0x53a5ed59712f1502ull [H-THIN] +// GappedAdd/0/0: boundary 0x8bd71987fffc51e9ull -> 0x2aa05060cf893d51ull, final_hash 0xaa98c069e5a019a7ull -> 0x53a5ed59712f1502ull [H-THIN] +// GappedAdd/1/1: boundary 0x7b93202bd79c1393ull -> 0x1de2b53c2e69ae04ull, rows 0xa4421baf09234dd7ull -> 0xb77b870b1ef756a3ull, final_hash 0xe086e35f4f128b46ull -> 0xc471e2ce0ce5b5b7ull [H-THIN] +// GappedAdd/1/0: boundary 0x7b93202bd79c1393ull -> 0x1de2b53c2e69ae04ull, final_hash 0xe086e35f4f128b46ull -> 0xc471e2ce0ce5b5b7ull [H-THIN] +// DeclinedReversal/0/1: boundary 0xfcb9145b39ec7b5aull -> 0x5dc519d25cd8e456ull, rows 0xbbddf0514f17aef7ull -> 0xcd770d65bdeb5e28ull, final_hash 0x764450acb0596bfcull -> 0xacfb010f8ef2c1dbull [H-THIN] +// DeclinedReversal/0/0: boundary 0xfcb9145b39ec7b5aull -> 0x5dc519d25cd8e456ull, final_hash 0x764450acb0596bfcull -> 0xacfb010f8ef2c1dbull [H-THIN] +// DeclinedReversal/1/1: boundary 0x4a1378eba012ef40ull -> 0x506c2023d62a4e6full, rows 0x16ea86de895d5b40ull -> 0xe0eb2dae405533f4ull, final_hash 0x14734f0e113371e4ull -> 0x980a839868ba5b97ull [H-THIN] +// DeclinedReversal/1/0: boundary 0x4a1378eba012ef40ull -> 0x506c2023d62a4e6full, final_hash 0x14734f0e113371e4ull -> 0x980a839868ba5b97ull [H-THIN] +// CalcOnFills/0/1: boundary 0x7641907936993779ull -> 0xbd9feba11a355c84ull, rows 0x3777fe6ece242880ull -> 0xbd62a54cd2c27075ull, final_hash 0x2583ce2bc6a3c1d9ull -> 0x8e716ca55979f5e4ull [PAR-ORDERS-2, H-THIN] +// CalcOnFills/0/0: boundary 0x7641907936993779ull -> 0xbd9feba11a355c84ull, final_hash 0x2583ce2bc6a3c1d9ull -> 0x8e716ca55979f5e4ull [PAR-ORDERS-2, H-THIN] +// CalcOnFills/1/1: boundary 0xd120e8fefb0dc236ull -> 0x0e2860d938b1d36dull, rows 0xdbb28de374d30bc3ull -> 0xfe826221acbd42c3ull, final_hash 0xad6e26cf69199431ull -> 0x1ae91d50d6762fd0ull [PAR-ORDERS-2, H-THIN] +// CalcOnFills/1/0: boundary 0xd120e8fefb0dc236ull -> 0x0e2860d938b1d36dull, final_hash 0xad6e26cf69199431ull -> 0x1ae91d50d6762fd0ull [PAR-ORDERS-2, H-THIN] +// CloseOrders/0/1: boundary 0x78ff3352f7085436ull -> 0xb27e9cfcbfee41d3ull, rows 0x896bcdcf15c8a4c6ull -> 0x0033c4a6a644f0ddull, final_hash 0x8e67de695bfb2c60ull -> 0xbedbc1183b7526a1ull [H-THIN] +// CloseOrders/0/0: boundary 0x78ff3352f7085436ull -> 0xb27e9cfcbfee41d3ull, final_hash 0x8e67de695bfb2c60ull -> 0xbedbc1183b7526a1ull [H-THIN] +// CloseOrders/1/1: boundary 0xe00ae8be3caf3315ull -> 0xff96b9591f72f309ull, rows 0x2584350c36f68309ull -> 0x5aaa3b2f562a501dull, final_hash 0xa998448999ff9b1eull -> 0xc542c7fb8bb5559dull [H-THIN] +// CloseOrders/1/0: boundary 0xe00ae8be3caf3315ull -> 0xff96b9591f72f309ull, final_hash 0xa998448999ff9b1eull -> 0xc542c7fb8bb5559dull [H-THIN] +// CloseOrdersSlipped/0/1: boundary 0x58e9160429c03bcaull -> 0xc2834ef9bd74b55full, rows 0xd3632e32e35ef626ull -> 0xb1042b50a92de9a0ull, final_hash 0x13f82da0279b5836ull -> 0x6c12422a13b3f077ull [H-THIN] +// CloseOrdersSlipped/0/0: boundary 0x58e9160429c03bcaull -> 0xc2834ef9bd74b55full, final_hash 0x13f82da0279b5836ull -> 0x6c12422a13b3f077ull [H-THIN] +// CloseOrdersSlipped/1/1: boundary 0x3507072a3e435b83ull -> 0x71906fcd647a169full, rows 0xc5d77a9b36fdec85ull -> 0xa2710dbb1c8a57edull, final_hash 0xc7b132391c547427ull -> 0x0330185649c756c2ull [H-THIN] +// CloseOrdersSlipped/1/0: boundary 0x3507072a3e435b83ull -> 0x71906fcd647a169full, final_hash 0xc7b132391c547427ull -> 0x0330185649c756c2ull [H-THIN] +// IntradayLoss/0/1: boundary 0x644e48072a025ba8ull -> 0x8a9bd5dd1429352eull, rows 0x0421711a90fdf429ull -> 0x616b96966292e8adull, final_hash 0xecca5625486ab2e1ull -> 0xc14ffe9c379fbee2ull [H-THIN] +// IntradayLoss/0/0: boundary 0x644e48072a025ba8ull -> 0x8a9bd5dd1429352eull, final_hash 0xecca5625486ab2e1ull -> 0xc14ffe9c379fbee2ull [H-THIN] +// IntradayLoss/1/1: boundary 0xce3032bd544d7840ull -> 0x1a92d1a03b233c6cull, rows 0x4e9d2d333bd5f470ull -> 0x3382648892a694c4ull, final_hash 0x4e071c66b57dfeaeull -> 0x66f4f2fe8b455019ull [H-THIN] +// IntradayLoss/1/0: boundary 0xce3032bd544d7840ull -> 0x1a92d1a03b233c6cull, final_hash 0x4e071c66b57dfeaeull -> 0x66f4f2fe8b455019ull [H-THIN] +// IntradayCapDeferred/0/1: boundary 0x93a814e5f6e8feb7ull -> 0xde19edf4087b930dull, rows 0xbbbb2712dbbd9ac8ull -> 0x560de539b138ea41ull, final_hash 0x77c142bb11eab624ull -> 0x7730e7fe703663edull [H-THIN] +// IntradayCapDeferred/0/0: boundary 0x93a814e5f6e8feb7ull -> 0xde19edf4087b930dull, final_hash 0x77c142bb11eab624ull -> 0x7730e7fe703663edull [H-THIN] +// IntradayCapDeferred/1/1: boundary 0xe23e69831f88d404ull -> 0x293bdcc491ae22d7ull, rows 0x63469b00b6747db8ull -> 0xd165d92830916a1cull, final_hash 0x72de8fef40c4c89bull -> 0x7ba4fe8defa15d4aull [H-THIN] +// IntradayCapDeferred/1/0: boundary 0xe23e69831f88d404ull -> 0x293bdcc491ae22d7ull, final_hash 0x72de8fef40c4c89bull -> 0x7ba4fe8defa15d4aull [H-THIN] +// PreopenSlice/0/1: boundary 0x9f401876dd941c1full -> 0xcbc8fb1ba9d83481ull, rows 0xbbce87ebf0d3f638ull -> 0xbe57d9275ae9e2bbull, final_hash 0xf73c6812e8dd0f92ull -> 0x62f97adf60eb373full [H-THIN] +// PreopenSlice/0/0: boundary 0x9f401876dd941c1full -> 0xcbc8fb1ba9d83481ull, final_hash 0xf73c6812e8dd0f92ull -> 0x62f97adf60eb373full [H-THIN] +// PreopenSlice/1/1: boundary 0x98345cf423b84f80ull -> 0xadcfe58d853cedfcull, rows 0x36db489e43fd8218ull -> 0xecc971145edced41ull, final_hash 0xa5916c1cffd7bae0ull -> 0x94fff9b38c7f1bd5ull [H-THIN] +// PreopenSlice/1/0: boundary 0x98345cf423b84f80ull -> 0xadcfe58d853cedfcull, final_hash 0xa5916c1cffd7bae0ull -> 0x94fff9b38c7f1bd5ull [H-THIN] +// SameBarBatch/0/1: boundary 0xcfa0ce2dfed4b2e4ull -> 0x85b57cf2c67fbdd6ull, rows 0x7de7b618b505f51eull -> 0xdf711eb77d489011ull, final_hash 0x0bb34499121582afull -> 0x77819f662ca36048ull [H-THIN] +// SameBarBatch/0/0: boundary 0xcfa0ce2dfed4b2e4ull -> 0x85b57cf2c67fbdd6ull, final_hash 0x0bb34499121582afull -> 0x77819f662ca36048ull [H-THIN] +// SameBarBatch/1/1: boundary 0x4557f9683348bf23ull -> 0x5d280dcf47a7af5bull, rows 0x79d43d1295ddc801ull -> 0x987f9a54f93d934eull, final_hash 0x26a5e7a6d5104dbcull -> 0x6a0ac704a36bef57ull [H-THIN] +// SameBarBatch/1/0: boundary 0x4557f9683348bf23ull -> 0x5d280dcf47a7af5bull, final_hash 0x26a5e7a6d5104dbcull -> 0x6a0ac704a36bef57ull [H-THIN] +// CommissionedShort/0/1: boundary 0x18b476bd2a3db96dull -> 0xc73e33b30bf09c29ull, rows 0x54b6ba2e7a2e11c2ull -> 0x26a3e6d04263ad27ull, final_hash 0x48a9193b2c1c1118ull -> 0xeb0d9ea9f0bdfd4bull [H-THIN] +// CommissionedShort/0/0: boundary 0x18b476bd2a3db96dull -> 0xc73e33b30bf09c29ull, final_hash 0x48a9193b2c1c1118ull -> 0xeb0d9ea9f0bdfd4bull [H-THIN] +// CommissionedShort/1/1: boundary 0xb24ff7e17f80c347ull -> 0x350b092698bdf8fdull, rows 0x20608b72a057ada4ull -> 0x3fe25a6001c6c4feull, final_hash 0x3ef1eaec89e6430dull -> 0xd73d8e053dbf5cd2ull [H-THIN] +// CommissionedShort/1/0: boundary 0xb24ff7e17f80c347ull -> 0x350b092698bdf8fdull, final_hash 0x3ef1eaec89e6430dull -> 0xd73d8e053dbf5cd2ull [H-THIN] +// AccountFx/0/1: boundary 0xaeec7af5859fe6b3ull -> 0xd27340484425ff52ull, rows 0x37039a16e8c77a7bull -> 0x4e596d61c552597eull, final_hash 0x2a28b4e61cd5cbbcull -> 0xea884b09a27ea73bull [H-THIN] +// AccountFx/0/0: boundary 0xaeec7af5859fe6b3ull -> 0xd27340484425ff52ull, final_hash 0x2a28b4e61cd5cbbcull -> 0xea884b09a27ea73bull [H-THIN] +// Stream/0/1: boundary 0x49574a22705f52c0ull -> 0x4f42bff80af50865ull, rows 0x3cade1c0a6b65b75ull -> 0xca31c77c5a63ae01ull, final_hash 0x8c9bb935bf0c32daull -> 0xeb75594695696b2full [H-THIN] +// Stream/0/0: boundary 0x49574a22705f52c0ull -> 0x4f42bff80af50865ull, final_hash 0x8c9bb935bf0c32daull -> 0xeb75594695696b2full [H-THIN] +// Random01/0/1: boundary 0xfa3864e9f5f3a619ull -> 0xe7fdfddc42a2ae75ull, rows 0x22da84303f34b121ull -> 0x0b38785d82bc8fe6ull, final_hash 0xa142013345fb25ddull -> 0x16fe00b5a88e8ff2ull [H-THIN] +// Random01/0/0: boundary 0xfa3864e9f5f3a619ull -> 0xe7fdfddc42a2ae75ull, final_hash 0xa142013345fb25ddull -> 0x16fe00b5a88e8ff2ull [H-THIN] +// Random01/1/1: boundary 0xfdafd368c22a3bb4ull -> 0x64e7087d99c04c46ull, rows 0x30ace005c5ea2e1bull -> 0x1c0e119f02f64ddeull, final_hash 0xb44a05ed46f5109dull -> 0xb0114372fd93a01cull [H-THIN] +// Random01/1/0: boundary 0xfdafd368c22a3bb4ull -> 0x64e7087d99c04c46ull, final_hash 0xb44a05ed46f5109dull -> 0xb0114372fd93a01cull [H-THIN] +// Random02/0/1: boundary 0xf933c58464603966ull -> 0x8a2b805556d22659ull, rows 0x8549ba7d7e735482ull -> 0xce086fbf3b8296e5ull, final_hash 0xd9870babe5d18626ull -> 0x5221fce916020c33ull [PAR-ORDERS-2, H-THIN] +// Random02/0/0: boundary 0xf933c58464603966ull -> 0x8a2b805556d22659ull, final_hash 0xd9870babe5d18626ull -> 0x5221fce916020c33ull [PAR-ORDERS-2, H-THIN] +// Random02/1/1: boundary 0x25ba5f1040df3b56ull -> 0x844819f3aaccd232ull, rows 0x1593a8e96a0c90a9ull -> 0x74fc5a6e6a256e36ull, final_hash 0x0fb50af38c25c54dull -> 0x089086fe02b53049ull [PAR-ORDERS-2, H-THIN] +// Random02/1/0: boundary 0x25ba5f1040df3b56ull -> 0x844819f3aaccd232ull, final_hash 0x0fb50af38c25c54dull -> 0x089086fe02b53049ull [PAR-ORDERS-2, H-THIN] +// Random03/0/1: boundary 0xb5a3f885ba0143fdull -> 0x2dad9d24e763c932ull, rows 0xd91b7712cc62209bull -> 0xa3f6fa59cf96ba52ull, final_hash 0xd93ef819aff693f3ull -> 0xeb202b23d902f8b8ull [H-THIN] +// Random03/0/0: boundary 0xb5a3f885ba0143fdull -> 0x2dad9d24e763c932ull, final_hash 0xd93ef819aff693f3ull -> 0xeb202b23d902f8b8ull [H-THIN] +// Random03/1/1: boundary 0x36230659e7bf570eull -> 0x7196371dde7bb130ull, rows 0xdb83075a5690e996ull -> 0xbdf7be966e72b156ull, final_hash 0x23acbc0041308125ull -> 0x92f95865ffb213eeull [H-THIN] +// Random03/1/0: boundary 0x36230659e7bf570eull -> 0x7196371dde7bb130ull, final_hash 0x23acbc0041308125ull -> 0x92f95865ffb213eeull [H-THIN] +// Random04/0/1: boundary 0x0c41be29e4497a7cull -> 0x9d74676f534d8b76ull, rows 0x1fdaabb2a1c312cdull -> 0xdb1c5eb0cb9f6c32ull, final_hash 0x542f1b01ef32d535ull -> 0x82a419ac37909af3ull [H-THIN] +// Random04/0/0: boundary 0x0c41be29e4497a7cull -> 0x9d74676f534d8b76ull, final_hash 0x542f1b01ef32d535ull -> 0x82a419ac37909af3ull [H-THIN] +// Random04/1/1: boundary 0x93ade030662e0b77ull -> 0x19cdc09e2267df5aull, rows 0x5beecc5dc910feb8ull -> 0x7eca887896ce161bull, final_hash 0x254f4f4933be69e0ull -> 0x350e10b2e19f0b44ull [H-THIN] +// Random04/1/0: boundary 0x93ade030662e0b77ull -> 0x19cdc09e2267df5aull, final_hash 0x254f4f4933be69e0ull -> 0x350e10b2e19f0b44ull [H-THIN] +// Random05/0/1: boundary 0x3e8e6495fe2e6dc1ull -> 0xac397a37be0d4789ull, rows 0x2703dbc59d3516a6ull -> 0x64b7bb57706476b6ull, final_hash 0xb72557b7f6894eafull -> 0x85274de8bea42d05ull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] +// Random05/0/0: boundary 0x3e8e6495fe2e6dc1ull -> 0xac397a37be0d4789ull, final_hash 0xb72557b7f6894eafull -> 0x85274de8bea42d05ull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] +// Random05/1/1: boundary 0x4be132773d14c0e0ull -> 0xbd96a4fdd0383edbull, rows 0xa6803dead66d3343ull -> 0xdcb784e78fba749eull, final_hash 0xb3561169b361d8bfull -> 0x919116731d265d3dull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] +// Random05/1/0: boundary 0x4be132773d14c0e0ull -> 0xbd96a4fdd0383edbull, final_hash 0xb3561169b361d8bfull -> 0x919116731d265d3dull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] +// Random06/0/1: boundary 0xecb44da2cbdb5c42ull -> 0x7deef79bd0217f64ull, rows 0x5485171eedfc86cfull -> 0xd166805cc595f38eull, final_hash 0xff2efe1925e45a9dull -> 0x4c7c821f90e38cccull [H-THIN, PAR-CASHFEE] +// Random06/0/0: boundary 0xecb44da2cbdb5c42ull -> 0x7deef79bd0217f64ull, final_hash 0xff2efe1925e45a9dull -> 0x4c7c821f90e38cccull [H-THIN, PAR-CASHFEE] +// Random06/1/1: boundary 0x65cc03efd59ec6b5ull -> 0x8a1bb21a5e64fed9ull, rows 0x2dce2153546130dbull -> 0x7d881d1a93512848ull, final_hash 0x1130940f86f108d2ull -> 0xaa8716ffbdfd11b2ull [H-THIN, PAR-CASHFEE] +// Random06/1/0: boundary 0x65cc03efd59ec6b5ull -> 0x8a1bb21a5e64fed9ull, final_hash 0x1130940f86f108d2ull -> 0xaa8716ffbdfd11b2ull [H-THIN, PAR-CASHFEE] +// Random07/0/1: boundary 0x7bf47df91573ab9cull -> 0xe6120913fe5da50dull, rows 0x3c1feb47a4d6d81eull -> 0xe90b62d77e4625b7ull, final_hash 0x438a828933e58c5bull -> 0xbc6f67df5b0f0b2full [H-THIN] +// Random07/0/0: boundary 0x7bf47df91573ab9cull -> 0xe6120913fe5da50dull, final_hash 0x438a828933e58c5bull -> 0xbc6f67df5b0f0b2full [H-THIN] +// Random07/1/1: boundary 0x29fcc776be13bdf5ull -> 0xe2c3492a3155bf7dull, rows 0x9a5ab1b2b00efff1ull -> 0x0e5f1aa6cabc3a32ull, final_hash 0x353c5f0e5bc1c22bull -> 0xbad1ae57e202b2d0ull [H-THIN] +// Random07/1/0: boundary 0x29fcc776be13bdf5ull -> 0xe2c3492a3155bf7dull, final_hash 0x353c5f0e5bc1c22bull -> 0xbad1ae57e202b2d0ull [H-THIN] +// Random08/0/1: boundary 0xc2f66d9e5f4943c7ull -> 0x0a3af182d33c479aull, rows 0xf4a38750150093e6ull -> 0xc91b237d10b90ecbull, final_hash 0x9ce7eb6ee2b122baull -> 0x1930dc38f4f605fbull [PAR-ORDERS-2, H-THIN] +// Random08/0/0: boundary 0xc2f66d9e5f4943c7ull -> 0x0a3af182d33c479aull, final_hash 0x9ce7eb6ee2b122baull -> 0x1930dc38f4f605fbull [PAR-ORDERS-2, H-THIN] +// Random08/1/1: boundary 0xbc7e1d017519ee12ull -> 0xc981400f75b65614ull, rows 0xc18ef29ec0f4fa8eull -> 0xb37d8f41ed349758ull, final_hash 0xbd83191789f76873ull -> 0xe48afc5e8a67e872ull [PAR-ORDERS-2, H-THIN] +// Random08/1/0: boundary 0xbc7e1d017519ee12ull -> 0xc981400f75b65614ull, final_hash 0xbd83191789f76873ull -> 0xe48afc5e8a67e872ull [PAR-ORDERS-2, H-THIN] +// Random09/0/1: boundary 0x400c46d70e6e84bcull -> 0xaaff8dcd840f16ceull, rows 0xc58cc1d4b5bd3580ull -> 0x9021538022cf9b9bull, final_hash 0x1f6cdd2e111e016aull -> 0xbbe3c07366b0cbeaull [H-THIN] +// Random09/0/0: boundary 0x400c46d70e6e84bcull -> 0xaaff8dcd840f16ceull, final_hash 0x1f6cdd2e111e016aull -> 0xbbe3c07366b0cbeaull [H-THIN] +// Random09/1/1: boundary 0x32d4b35f029bad56ull -> 0x026e829145cad6f5ull, rows 0x447742874dde46e0ull -> 0x32ca6bae685cea43ull, final_hash 0x3d99e7abdd92a773ull -> 0xc55c597c6508d557ull [H-THIN] +// Random09/1/0: boundary 0x32d4b35f029bad56ull -> 0x026e829145cad6f5ull, final_hash 0x3d99e7abdd92a773ull -> 0xc55c597c6508d557ull [H-THIN] +// Random10/0/1: boundary 0x998ee74ea73334c6ull -> 0x82933908492fe6aeull, rows 0x5a3104007433abc0ull -> 0x839a10fa6478c692ull, final_hash 0x0a7888fb9b890fb4ull -> 0x6907e89e2f502bd7ull [PAR-ORDERS-2, H-THIN] +// Random10/0/0: boundary 0x998ee74ea73334c6ull -> 0x82933908492fe6aeull, final_hash 0x0a7888fb9b890fb4ull -> 0x6907e89e2f502bd7ull [PAR-ORDERS-2, H-THIN] +// Random10/1/1: boundary 0xde7dbd52ac33a7ffull -> 0x3af283057bf553afull, rows 0xaf8f0ca763b06949ull -> 0xd948d5022e691361ull, final_hash 0xfbe70c357ac296cfull -> 0x794b361143554e09ull [PAR-ORDERS-2, H-THIN] +// Random10/1/0: boundary 0xde7dbd52ac33a7ffull -> 0x3af283057bf553afull, final_hash 0xfbe70c357ac296cfull -> 0x794b361143554e09ull [PAR-ORDERS-2, H-THIN] +// Random11/0/1: boundary 0x805038d1cb0a3848ull -> 0x9f99f3ad7d35ba23ull, rows 0xebc3fde6db4a37f3ull -> 0x29a35fd6705cc48aull, final_hash 0x27bc15c5c142c728ull -> 0x8235cad2a6dfb3f3ull [H-THIN] +// Random11/0/0: boundary 0x805038d1cb0a3848ull -> 0x9f99f3ad7d35ba23ull, final_hash 0x27bc15c5c142c728ull -> 0x8235cad2a6dfb3f3ull [H-THIN] +// Random11/1/1: boundary 0x4a739854a58a3db9ull -> 0xf6fb4588ed987e13ull, rows 0xc91f1e4519586a06ull -> 0x7dcd8b5314fb7562ull, final_hash 0x8af717fd3fa04097ull -> 0xb44ac10707d01de6ull [H-THIN] +// Random11/1/0: boundary 0x4a739854a58a3db9ull -> 0xf6fb4588ed987e13ull, final_hash 0x8af717fd3fa04097ull -> 0xb44ac10707d01de6ull [H-THIN] +// Random12/0/1: boundary 0xb9a9bb3fafab7349ull -> 0xc02247ff66b0dc87ull, rows 0xe93fb7b821f6a7feull -> 0xf39477bfa32bb689ull, final_hash 0x29c94b5df165789aull -> 0x5f0b7200f4ecdfe1ull [PAR-ORDERS-2, H-THIN] +// Random12/0/0: boundary 0xb9a9bb3fafab7349ull -> 0xc02247ff66b0dc87ull, final_hash 0x29c94b5df165789aull -> 0x5f0b7200f4ecdfe1ull [PAR-ORDERS-2, H-THIN] +// Random12/1/1: boundary 0x298ab491e3caa61full -> 0xfcd62d0327e0553aull, rows 0xf8e78400f1c148acull -> 0xf36949e9fad408efull, final_hash 0x927c4394206b42b2ull -> 0x84a69af72aae96d9ull [PAR-ORDERS-2, H-THIN] +// Random12/1/0: boundary 0x298ab491e3caa61full -> 0xfcd62d0327e0553aull, final_hash 0x927c4394206b42b2ull -> 0x84a69af72aae96d9ull [PAR-ORDERS-2, H-THIN] +// Random13/0/1: boundary 0x39caae56088c1db8ull -> 0x53be6a77e760fe0bull, rows 0x4028b545fb2c3ad9ull -> 0xe1b3dac90adb9f4bull, final_hash 0xc19e5f5bbefe0e7full -> 0xafd98319ef581f61ull [H-THIN] +// Random13/0/0: boundary 0x39caae56088c1db8ull -> 0x53be6a77e760fe0bull, final_hash 0xc19e5f5bbefe0e7full -> 0xafd98319ef581f61ull [H-THIN] +// Random13/1/1: boundary 0x07b714c492b5ae85ull -> 0xd238668589210027ull, rows 0xa03b3f8bed95186dull -> 0xc4b7dac2b95e01fbull, final_hash 0xa9f1d1a463925b52ull -> 0xb4b9d6f51a69342full [H-THIN] +// Random13/1/0: boundary 0x07b714c492b5ae85ull -> 0xd238668589210027ull, final_hash 0xa9f1d1a463925b52ull -> 0xb4b9d6f51a69342full [H-THIN] +// Random14/0/1: boundary 0x21085d70db4c336bull -> 0xd37abe66ab158e44ull, rows 0x394ebfbc2c15522aull -> 0x38cb950f5ceb9d9cull, final_hash 0xad1f71b1afdd37f8ull -> 0x9d488869e9756d89ull [H-THIN] +// Random14/0/0: boundary 0x21085d70db4c336bull -> 0xd37abe66ab158e44ull, final_hash 0xad1f71b1afdd37f8ull -> 0x9d488869e9756d89ull [H-THIN] +// Random14/1/1: boundary 0x5a8dbff598244e8cull -> 0xb8392469a2a0abfeull, rows 0x4c48857b5cd25816ull -> 0xfed8161637d6c938ull, final_hash 0x7f45221fb1111aa3ull -> 0x04b4096b966c585cull [H-THIN] +// Random14/1/0: boundary 0x5a8dbff598244e8cull -> 0xb8392469a2a0abfeull, final_hash 0x7f45221fb1111aa3ull -> 0x04b4096b966c585cull [H-THIN] +// Random15/0/1: boundary 0x1fe125a3fb2912dfull -> 0x1fc67d9462b82276ull, rows 0x9ef69d4200bfb809ull -> 0x167381a97aa1c870ull, final_hash 0xf8da94854e79d89eull -> 0x69f92f0d300a126eull [H-THIN] +// Random15/0/0: boundary 0x1fe125a3fb2912dfull -> 0x1fc67d9462b82276ull, final_hash 0xf8da94854e79d89eull -> 0x69f92f0d300a126eull [H-THIN] +// Random15/1/1: boundary 0x7b17bf5a43044023ull -> 0xbbbd08da5449bc19ull, rows 0xdeec1e0290d1370dull -> 0x2a9a6d807f0676baull, final_hash 0x01758143c50ac69dull -> 0xe8dd6d8df584c6d6ull [H-THIN] +// Random15/1/0: boundary 0x7b17bf5a43044023ull -> 0xbbbd08da5449bc19ull, final_hash 0x01758143c50ac69dull -> 0xe8dd6d8df584c6d6ull [H-THIN] +// Random16/0/1: boundary 0xde7f3e61caa50135ull -> 0xe8851a2e0e645003ull, rows 0x9593cb63a9a06f14ull -> 0xcfab7fb0e8c24b17ull, final_hash 0xc99a0f083d0b2507ull -> 0x5639dcc11e103f25ull [H-THIN] +// Random16/0/0: boundary 0xde7f3e61caa50135ull -> 0xe8851a2e0e645003ull, final_hash 0xc99a0f083d0b2507ull -> 0x5639dcc11e103f25ull [H-THIN] +// Random16/1/1: boundary 0xffbc34c7947c611bull -> 0x431440275998e000ull, rows 0xa2b9ee52a1338391ull -> 0x90d12ea4ed48dd05ull, final_hash 0x1f80513beefa0b1aull -> 0xf7e82ef73f56a2eeull [H-THIN] +// Random16/1/0: boundary 0xffbc34c7947c611bull -> 0x431440275998e000ull, final_hash 0x1f80513beefa0b1aull -> 0xf7e82ef73f56a2eeull [H-THIN] +// Random17/0/1: boundary 0x15bab2d913fb1d81ull -> 0xf4b303a1ef4696f7ull, rows 0x0a3d587bbe912dcfull -> 0x6417861b10d32340ull, final_hash 0x7185bfaebb406df5ull -> 0xa61332be53034674ull [PAR-ORDERS-2, H-THIN] +// Random17/0/0: boundary 0x15bab2d913fb1d81ull -> 0xf4b303a1ef4696f7ull, final_hash 0x7185bfaebb406df5ull -> 0xa61332be53034674ull [PAR-ORDERS-2, H-THIN] +// Random17/1/1: boundary 0xa978788efbcc1631ull -> 0x4fbe2d70efbe24eaull, rows 0xbcbe24594a8acfcaull -> 0xaf770da711891f5aull, final_hash 0x285eaf39ec559587ull -> 0x6665febf6fb7f354ull [PAR-ORDERS-2, H-THIN] +// Random17/1/0: boundary 0xa978788efbcc1631ull -> 0x4fbe2d70efbe24eaull, final_hash 0x285eaf39ec559587ull -> 0x6665febf6fb7f354ull [PAR-ORDERS-2, H-THIN] +// Random18/0/1: boundary 0x90ed08a2949b1fb6ull -> 0xf434360cb53f77baull, rows 0x665cda1c725bc1b2ull -> 0xff5d4060a3fafb03ull, final_hash 0xe16bbfbfa58dbeb1ull -> 0xe0f82ba48054cc9cull [PAR-ORDERS-2, H-THIN] +// Random18/0/0: boundary 0x90ed08a2949b1fb6ull -> 0xf434360cb53f77baull, final_hash 0xe16bbfbfa58dbeb1ull -> 0xe0f82ba48054cc9cull [PAR-ORDERS-2, H-THIN] +// Random18/1/1: boundary 0xbf864c04890cf915ull -> 0x7b6ad000002b1959ull, rows 0x807a82ee057fbb5dull -> 0x70472dcb4abe021full, final_hash 0x5c359872c6618dd3ull -> 0x73a5afc061a888aaull [PAR-ORDERS-2, H-THIN] +// Random18/1/0: boundary 0xbf864c04890cf915ull -> 0x7b6ad000002b1959ull, final_hash 0x5c359872c6618dd3ull -> 0x73a5afc061a888aaull [PAR-ORDERS-2, H-THIN] +// Random19/0/1: boundary 0x3c5cc975f928b775ull -> 0xa21f7b0b62d72170ull, rows 0xea691a951590ece8ull -> 0xda14d42130450ee8ull, final_hash 0x17d53da80c3a895full -> 0x0bcf8b1fde6775f8ull [H-THIN] +// Random19/0/0: boundary 0x3c5cc975f928b775ull -> 0xa21f7b0b62d72170ull, final_hash 0x17d53da80c3a895full -> 0x0bcf8b1fde6775f8ull [H-THIN] +// Random19/1/1: boundary 0x92536ad17b9fbf26ull -> 0xceae18544190aae2ull, rows 0xabc10759f5e1ffa1ull -> 0x7d8104e455ec01f8ull, final_hash 0x168791086f51c368ull -> 0x426c72d5bce82609ull [H-THIN] +// Random19/1/0: boundary 0x92536ad17b9fbf26ull -> 0xceae18544190aae2ull, final_hash 0x168791086f51c368ull -> 0x426c72d5bce82609ull [H-THIN] +// Random20/0/1: boundary 0x90b20d166e1357e0ull -> 0xad83f25bfdab1b50ull, rows 0x808745030887aa13ull -> 0x103b25042e1313deull, final_hash 0x3968c24c22199036ull -> 0xb27d00f43fad75a1ull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] +// Random20/0/0: boundary 0x90b20d166e1357e0ull -> 0xad83f25bfdab1b50ull, final_hash 0x3968c24c22199036ull -> 0xb27d00f43fad75a1ull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] +// Random20/1/1: boundary 0xa82d4e53d2192a5full -> 0x4b8bcf5ef1f015c5ull, rows 0x8e1411c055e37af3ull -> 0x6e2d790aa8a7762bull, final_hash 0x5c29f15f02417fcaull -> 0xd3e60d6fda04adbeull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] +// Random20/1/0: boundary 0xa82d4e53d2192a5full -> 0x4b8bcf5ef1f015c5ull, final_hash 0x5c29f15f02417fcaull -> 0xd3e60d6fda04adbeull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] +// Random21/0/1: boundary 0x18e96beb584671fcull -> 0xd7b52779988b5cfaull, rows 0x6c5b4e5a67209f94ull -> 0xcfddcd52fb629169ull, final_hash 0xca719be0f46f42acull -> 0xfd9b7e4dad3197b5ull [H-THIN] +// Random21/0/0: boundary 0x18e96beb584671fcull -> 0xd7b52779988b5cfaull, final_hash 0xca719be0f46f42acull -> 0xfd9b7e4dad3197b5ull [H-THIN] +// Random21/1/1: boundary 0xd32a038cc5b6ea6eull -> 0x1c515b9e6cd109a3ull, rows 0x9074957bf890f8e6ull -> 0xaa94d0f54845ee02ull, final_hash 0xb6387745e72b0075ull -> 0x29fa4e20de356d85ull [H-THIN] +// Random21/1/0: boundary 0xd32a038cc5b6ea6eull -> 0x1c515b9e6cd109a3ull, final_hash 0xb6387745e72b0075ull -> 0x29fa4e20de356d85ull [H-THIN] +// Random22/0/1: boundary 0xef05a2f01ca0c05cull -> 0x79adc16250cf8292ull, rows 0xb151ebfe9a44ebebull -> 0x2512a0afd913906aull, final_hash 0x1b1b4c42bccc2191ull -> 0x1d49ae94182c6dc3ull [PAR-ORDERS-2, H-THIN] +// Random22/0/0: boundary 0xef05a2f01ca0c05cull -> 0x79adc16250cf8292ull, final_hash 0x1b1b4c42bccc2191ull -> 0x1d49ae94182c6dc3ull [PAR-ORDERS-2, H-THIN] +// Random22/1/1: boundary 0x1624d3651edc9089ull -> 0x301434ca9762fba0ull, rows 0x2a2f5457686fdb7full -> 0x9371e473c135020full, final_hash 0xb361721c9b7f57e7ull -> 0xaadd4e3cb0a37251ull [PAR-ORDERS-2, H-THIN] +// Random22/1/0: boundary 0x1624d3651edc9089ull -> 0x301434ca9762fba0ull, final_hash 0xb361721c9b7f57e7ull -> 0xaadd4e3cb0a37251ull [PAR-ORDERS-2, H-THIN] +// Random23/0/1: boundary 0xc50cf90af093d6e9ull -> 0xbf93319ca8e8ed61ull, rows 0x629f1e11fd65d1cdull -> 0x178099cac4dd9abfull, final_hash 0x9f14134203a5c6e9ull -> 0x160121fb8a6ad628ull [PAR-ORDERS-2, H-THIN] +// Random23/0/0: boundary 0xc50cf90af093d6e9ull -> 0xbf93319ca8e8ed61ull, final_hash 0x9f14134203a5c6e9ull -> 0x160121fb8a6ad628ull [PAR-ORDERS-2, H-THIN] +// Random23/1/1: boundary 0x61be7e60a493c3efull -> 0x03336650fccfc186ull, rows 0x7182d347041d20e2ull -> 0x84fb2988132b6bdbull, final_hash 0xcac3f08a3f6d7067ull -> 0x54a3c7db6bc08316ull [PAR-ORDERS-2, H-THIN] +// Random23/1/0: boundary 0x61be7e60a493c3efull -> 0x03336650fccfc186ull, final_hash 0xcac3f08a3f6d7067ull -> 0x54a3c7db6bc08316ull [PAR-ORDERS-2, H-THIN] +// Random24/0/1: boundary 0xadbbd26ef9c19430ull -> 0x21714f0414285779ull, rows 0x7cea484a04d1bc6dull -> 0x8e07ee97e3c18592ull, final_hash 0x880c60f0ef5d5a7dull -> 0x9b4982cc0c9d5836ull [H-THIN] +// Random24/0/0: boundary 0xadbbd26ef9c19430ull -> 0x21714f0414285779ull, final_hash 0x880c60f0ef5d5a7dull -> 0x9b4982cc0c9d5836ull [H-THIN] +// Random24/1/1: boundary 0x91d34f247834f4e9ull -> 0x104927385caf1bafull, rows 0xf9fa951efcc4b1caull -> 0xb58d2d29b9934e0cull, final_hash 0xc06e2e1df8db5231ull -> 0x892eef5960b6186eull [H-THIN] +// Random24/1/0: boundary 0x91d34f247834f4e9ull -> 0x104927385caf1bafull, final_hash 0xc06e2e1df8db5231ull -> 0x892eef5960b6186eull [H-THIN] +// Random25/0/1: boundary 0x1091baf336b7e96aull -> 0x7fe75851d29b5f6aull, rows 0x5836120ce0f3d783ull -> 0xbd64625a7bcfb73cull, final_hash 0x3f0d68bfc74cf35full -> 0xdbdc12e31962344aull [H-THIN] +// Random25/0/0: boundary 0x1091baf336b7e96aull -> 0x7fe75851d29b5f6aull, final_hash 0x3f0d68bfc74cf35full -> 0xdbdc12e31962344aull [H-THIN] +// Random25/1/1: boundary 0x4c59eab8929878abull -> 0x49f7049727341b3cull, rows 0x90bf03c0ed26cd99ull -> 0x4023f4c7efa8cdacull, final_hash 0xeb9d9c36364972dfull -> 0x1e3a08e1516ae4beull [H-THIN] +// Random25/1/0: boundary 0x4c59eab8929878abull -> 0x49f7049727341b3cull, final_hash 0xeb9d9c36364972dfull -> 0x1e3a08e1516ae4beull [H-THIN] +// Random26/0/1: boundary 0x406a1ebe98ed8ee5ull -> 0xca8049a8b96e9724ull, rows 0xc4ceaa2f46b3b1c7ull -> 0xc7e39193f4b07aa7ull, final_hash 0x6d9f1512cb3c63efull -> 0xf4e31e8175400963ull [H-THIN] +// Random26/0/0: boundary 0x406a1ebe98ed8ee5ull -> 0xca8049a8b96e9724ull, final_hash 0x6d9f1512cb3c63efull -> 0xf4e31e8175400963ull [H-THIN] +// Random26/1/1: boundary 0x28ce140c5a29687dull -> 0x9c9ac48945130531ull, rows 0x383f34833e879b1bull -> 0xeb4d985eb31f93e0ull, final_hash 0x500ef2279c277ba0ull -> 0xecd913dc1c2bb6edull [H-THIN] +// Random26/1/0: boundary 0x28ce140c5a29687dull -> 0x9c9ac48945130531ull, final_hash 0x500ef2279c277ba0ull -> 0xecd913dc1c2bb6edull [H-THIN] +// Random27/0/1: boundary 0xeafcaf72c5794d88ull -> 0xa5ad84fe83acddfcull, rows 0xfbb77c6f11f0944full -> 0x4da695a0a63ae358ull, final_hash 0xf2d844781c8e9819ull -> 0x70f7b127c522e392ull [H-THIN] +// Random27/0/0: boundary 0xeafcaf72c5794d88ull -> 0xa5ad84fe83acddfcull, final_hash 0xf2d844781c8e9819ull -> 0x70f7b127c522e392ull [H-THIN] +// Random27/1/1: boundary 0x433fe5d0340138d8ull -> 0x67f5b41cf936747aull, rows 0x80e409be19d6131dull -> 0x06665f5393126836ull, final_hash 0x2a459b69d95400b1ull -> 0x94de1904cefde26dull [H-THIN] +// Random27/1/0: boundary 0x433fe5d0340138d8ull -> 0x67f5b41cf936747aull, final_hash 0x2a459b69d95400b1ull -> 0x94de1904cefde26dull [H-THIN] +// Random28/0/1: boundary 0xa0bb8c1925814fe3ull -> 0x8b9cc3d4febe4c62ull, rows 0x8e8763454e7d2dcaull -> 0x69471bc0f4097ac1ull, final_hash 0xe51c2406011cd87dull -> 0x373d1983261f87caull [H-THIN] +// Random28/0/0: boundary 0xa0bb8c1925814fe3ull -> 0x8b9cc3d4febe4c62ull, final_hash 0xe51c2406011cd87dull -> 0x373d1983261f87caull [H-THIN] +// Random28/1/1: boundary 0xdb2802eb6d8819c7ull -> 0x57f3120e39e241ddull, rows 0x910e834a102ac932ull -> 0x0a99bf96869493a4ull, final_hash 0x7406a539e21e57ceull -> 0xe2efc2ce4ee88ff9ull [H-THIN] +// Random28/1/0: boundary 0xdb2802eb6d8819c7ull -> 0x57f3120e39e241ddull, final_hash 0x7406a539e21e57ceull -> 0xe2efc2ce4ee88ff9ull [H-THIN] +// Random29/0/1: boundary 0x2bbdb428b4a78935ull -> 0x89b631d5f5f01691ull, rows 0x6c7b12c7d5aa45e4ull -> 0x167525b5e459f6a9ull, final_hash 0xfd3d2fb6be6132d7ull -> 0xb42cb126538c6f0aull [H-THIN] +// Random29/0/0: boundary 0x2bbdb428b4a78935ull -> 0x89b631d5f5f01691ull, final_hash 0xfd3d2fb6be6132d7ull -> 0xb42cb126538c6f0aull [H-THIN] +// Random29/1/1: boundary 0x2bc2551fd2128e20ull -> 0x41cca15aeaf4b30dull, rows 0xb9728dfab890bfd1ull -> 0x5c0fe05b50ac7989ull, final_hash 0x082924a3760602daull -> 0xdaa6c53d3410e42full [H-THIN] +// Random29/1/0: boundary 0x2bc2551fd2128e20ull -> 0x41cca15aeaf4b30dull, final_hash 0x082924a3760602daull -> 0xdaa6c53d3410e42full [H-THIN] +// Random30/0/1: boundary 0x585a1a9f34476a29ull -> 0xe74142e41965c7f8ull, rows 0x5d09652b7dc7603full -> 0x7d6bfad3ec602d1eull, final_hash 0x8e3b3ec00a1454a2ull -> 0x2ff813254130dde5ull [H-THIN] +// Random30/0/0: boundary 0x585a1a9f34476a29ull -> 0xe74142e41965c7f8ull, final_hash 0x8e3b3ec00a1454a2ull -> 0x2ff813254130dde5ull [H-THIN] +// Random30/1/1: boundary 0x8de0587cbe97fc0aull -> 0xffbc3c35359e842dull, rows 0x2fc1ebe82b1783eeull -> 0xa978037a0a737ed4ull, final_hash 0x7ffde30dc93fc9d7ull -> 0xb713984b2b953de4ull [H-THIN] +// Random30/1/0: boundary 0x8de0587cbe97fc0aull -> 0xffbc3c35359e842dull, final_hash 0x7ffde30dc93fc9d7ull -> 0xb713984b2b953de4ull [H-THIN] +// Random31/0/1: boundary 0x09b545a0067e8c26ull -> 0xc1d0985632aa85e5ull, rows 0x3c6ae0150bb63629ull -> 0xb1ff4f1ff8ce7ff6ull, final_hash 0xfaf6a143c8b0d038ull -> 0x0138efa3affc4452ull [H-THIN] +// Random31/0/0: boundary 0x09b545a0067e8c26ull -> 0xc1d0985632aa85e5ull, final_hash 0xfaf6a143c8b0d038ull -> 0x0138efa3affc4452ull [H-THIN] +// Random31/1/1: boundary 0xb42bd9cfbe060a73ull -> 0x29c04e4a3f62fc66ull, rows 0x928f7cfde5bdb105ull -> 0xd37bee2bcbd5b472ull, final_hash 0xf602f077ecb2b7cfull -> 0x3e283f49a532b649ull [H-THIN] +// Random31/1/0: boundary 0xb42bd9cfbe060a73ull -> 0x29c04e4a3f62fc66ull, final_hash 0xf602f077ecb2b7cfull -> 0x3e283f49a532b649ull [H-THIN] +// Random32/0/1: boundary 0x220cd02053dbc40aull -> 0x69fb37996a1a6d8aull, rows 0x9afe2ada13350695ull -> 0x4eda1ad97257444eull, final_hash 0xe577534dd3033b52ull -> 0xf8f4a46581375bc8ull [H-THIN] +// Random32/0/0: boundary 0x220cd02053dbc40aull -> 0x69fb37996a1a6d8aull, final_hash 0xe577534dd3033b52ull -> 0xf8f4a46581375bc8ull [H-THIN] +// Random32/1/1: boundary 0xc0ca43094334f008ull -> 0x6aa8d3b227f15c4cull, rows 0xe85eb4136b8c435dull -> 0xefb2e786712329f2ull, final_hash 0xba233c9fc46444a4ull -> 0xe3b7a40e1641948dull [H-THIN] +// Random32/1/0: boundary 0xc0ca43094334f008ull -> 0x6aa8d3b227f15c4cull, final_hash 0xba233c9fc46444a4ull -> 0xe3b7a40e1641948dull [H-THIN] +// Random33/0/1: boundary 0xbfcfebe96632bd39ull -> 0x9651f8f876e48213ull, rows 0x116bc2f57887ba4bull -> 0x70cd8d3b24d08640ull, final_hash 0xf8a0e855a842d39cull -> 0x495394a49a7d3245ull [H-THIN] +// Random33/0/0: boundary 0xbfcfebe96632bd39ull -> 0x9651f8f876e48213ull, final_hash 0xf8a0e855a842d39cull -> 0x495394a49a7d3245ull [H-THIN] +// Random33/1/1: boundary 0xe17f0353f37c41f1ull -> 0x23686cd9a3fe0637ull, rows 0x0ba98d74e8b769b7ull -> 0x17ecf363cd313d8aull, final_hash 0xb76d3ca699ea4ebdull -> 0xcefd187486fda5beull [H-THIN] +// Random33/1/0: boundary 0xe17f0353f37c41f1ull -> 0x23686cd9a3fe0637ull, final_hash 0xb76d3ca699ea4ebdull -> 0xcefd187486fda5beull [H-THIN] +// Random34/0/1: boundary 0xea1b68237d95805full -> 0xb0d26d6ec62ee0b2ull, rows 0xb29d9ea005d9c282ull -> 0xe24b790bed6d8209ull, final_hash 0xb03d41aea9c00c63ull -> 0xa8799fc8644a8be8ull [H-THIN] +// Random34/0/0: boundary 0xea1b68237d95805full -> 0xb0d26d6ec62ee0b2ull, final_hash 0xb03d41aea9c00c63ull -> 0xa8799fc8644a8be8ull [H-THIN] +// Random34/1/1: boundary 0x4ecacf8ab2d85c47ull -> 0x40ad548ed9e2d639ull, rows 0x294fe821fef3729dull -> 0x2d022bb216743a64ull, final_hash 0xe4e38b6ada294505ull -> 0x0a4527b569ac8a10ull [H-THIN] +// Random34/1/0: boundary 0x4ecacf8ab2d85c47ull -> 0x40ad548ed9e2d639ull, final_hash 0xe4e38b6ada294505ull -> 0x0a4527b569ac8a10ull [H-THIN] +// Random35/0/1: boundary 0xeb16fb1a6bc80c17ull -> 0xbdcab3fd94434657ull, rows 0x7265444996669f20ull -> 0x8f07aebde3f01772ull, final_hash 0xb971c6bc6d7ac182ull -> 0x4d118037ee1626adull [H-THIN, PAR-CASHFEE] +// Random35/0/0: boundary 0xeb16fb1a6bc80c17ull -> 0xbdcab3fd94434657ull, final_hash 0xb971c6bc6d7ac182ull -> 0x4d118037ee1626adull [H-THIN, PAR-CASHFEE] +// Random35/1/1: boundary 0x193ec2cb9a163900ull -> 0x9f158f1f03765f97ull, rows 0x9233c78214742aa3ull -> 0x7ea6860a76f2a755ull, final_hash 0xa9bdcf22e28567b2ull -> 0x07820fe3390f4450ull [H-THIN, PAR-CASHFEE] +// Random35/1/0: boundary 0x193ec2cb9a163900ull -> 0x9f158f1f03765f97ull, final_hash 0xa9bdcf22e28567b2ull -> 0x07820fe3390f4450ull [H-THIN, PAR-CASHFEE] +// Random36/0/1: boundary 0xd4b5588948d4b755ull -> 0xc06367cbde7ecc9aull, rows 0x6af2dc7dd208ef5dull -> 0x9c13280fd6c4a76full, final_hash 0x1980e25b047dea78ull -> 0x4a73a58e1bf4c419ull [PAR-ORDERS-2, H-THIN] +// Random36/0/0: boundary 0xd4b5588948d4b755ull -> 0xc06367cbde7ecc9aull, final_hash 0x1980e25b047dea78ull -> 0x4a73a58e1bf4c419ull [PAR-ORDERS-2, H-THIN] +// Random36/1/1: boundary 0xd1532dd9d52ae5f4ull -> 0xf78394d86184426aull, rows 0x0e1d68d8d6c4c5e2ull -> 0xd52d837b120d0443ull, final_hash 0xe93d9e6c724efef6ull -> 0x1e90f705be3da39full [PAR-ORDERS-2, H-THIN] +// Random36/1/0: boundary 0xd1532dd9d52ae5f4ull -> 0xf78394d86184426aull, final_hash 0xe93d9e6c724efef6ull -> 0x1e90f705be3da39full [PAR-ORDERS-2, H-THIN] +// Random37/0/1: boundary 0x3a89ff92c403a3e7ull -> 0x8af4f686e8afd249ull, rows 0xddda8d77310d918full -> 0xb43a05dbbc636b7aull, final_hash 0xff9c74898bf3a72aull -> 0x8692adfc1cddb1c4ull [PAR-ORDERS-2, H-THIN] +// Random37/0/0: boundary 0x3a89ff92c403a3e7ull -> 0x8af4f686e8afd249ull, final_hash 0xff9c74898bf3a72aull -> 0x8692adfc1cddb1c4ull [PAR-ORDERS-2, H-THIN] +// Random37/1/1: boundary 0x8a36a09ee42999ecull -> 0xe82a3b0bd1861474ull, rows 0xf12dd4800e96f492ull -> 0x5ea014024a78fb97ull, final_hash 0x9f14bee9808ee8baull -> 0x1ace37e580b56141ull [PAR-ORDERS-2, H-THIN] +// Random37/1/0: boundary 0x8a36a09ee42999ecull -> 0xe82a3b0bd1861474ull, final_hash 0x9f14bee9808ee8baull -> 0x1ace37e580b56141ull [PAR-ORDERS-2, H-THIN] +// Random38/0/1: boundary 0x36ed3eee0da888c2ull -> 0xeaf05ca1aedc03d5ull, rows 0x2bf58dbe1c5bd9acull -> 0x80971876a1156ee8ull, final_hash 0x5b82e26291364fc0ull -> 0x2d7719753c10f4bbull [H-THIN, PAR-CASHFEE] +// Random38/0/0: boundary 0x36ed3eee0da888c2ull -> 0xeaf05ca1aedc03d5ull, final_hash 0x5b82e26291364fc0ull -> 0x2d7719753c10f4bbull [H-THIN, PAR-CASHFEE] +// Random38/1/1: boundary 0xa3225feaf19b98feull -> 0x8285a035265d771cull, rows 0x59c34402fcf68e2aull -> 0x0d3b927724077789ull, final_hash 0x135197c1ef34cbbdull -> 0x09336a0556a2fbb7ull [H-THIN, PAR-CASHFEE] +// Random38/1/0: boundary 0xa3225feaf19b98feull -> 0x8285a035265d771cull, final_hash 0x135197c1ef34cbbdull -> 0x09336a0556a2fbb7ull [H-THIN, PAR-CASHFEE] +// Random39/0/1: boundary 0x9bcdaebb98702055ull -> 0xc989defa2d8cff00ull, rows 0xf4194fbb3f8e63f4ull -> 0x31db41d49a36aaa6ull, final_hash 0x3026f01eb73dfcdaull -> 0xb8c88da9531702d9ull [H-THIN] +// Random39/0/0: boundary 0x9bcdaebb98702055ull -> 0xc989defa2d8cff00ull, final_hash 0x3026f01eb73dfcdaull -> 0xb8c88da9531702d9ull [H-THIN] +// Random39/1/1: boundary 0x5fa98522adc27aafull -> 0x588cc94ab848f7e6ull, rows 0x50b22f98ba965993ull -> 0x3b1478373127afc0ull, final_hash 0x123f0c5b09aeae91ull -> 0xbdab759ad2c5865cull [H-THIN] +// Random39/1/0: boundary 0x5fa98522adc27aafull -> 0x588cc94ab848f7e6ull, final_hash 0x123f0c5b09aeae91ull -> 0xbdab759ad2c5865cull [H-THIN] +// Random40/0/1: boundary 0x869959795f89f7afull -> 0xc4f976129a95a285ull, rows 0xe9919376f3542ea7ull -> 0x389162a99c89a49cull, final_hash 0xd91533e6639e22f9ull -> 0xcafbb488b92f9edaull [PAR-ORDERS-2, H-THIN] +// Random40/0/0: boundary 0x869959795f89f7afull -> 0xc4f976129a95a285ull, final_hash 0xd91533e6639e22f9ull -> 0xcafbb488b92f9edaull [PAR-ORDERS-2, H-THIN] +// Random40/1/1: boundary 0xf47407130500a9bdull -> 0x1cc6041a6a6fcdcdull, rows 0xda9b4928c023ca07ull -> 0x33a043a5f0e45941ull, final_hash 0x5a8f1ec5a18f3731ull -> 0x798fe8d7596aaa9cull [PAR-ORDERS-2, H-THIN] +// Random40/1/0: boundary 0xf47407130500a9bdull -> 0x1cc6041a6a6fcdcdull, final_hash 0x5a8f1ec5a18f3731ull -> 0x798fe8d7596aaa9cull [PAR-ORDERS-2, H-THIN] +// Random41/0/1: boundary 0x6509ace183e7d2cdull -> 0xb4b89e111611f4ceull, rows 0x957c5244d07600b9ull -> 0xea3d6d0c77bc7e76ull, final_hash 0xcc1203a69591b134ull -> 0x980f8a18b3d80a23ull [PAR-ORDERS-2, H-THIN] +// Random41/0/0: boundary 0x6509ace183e7d2cdull -> 0xb4b89e111611f4ceull, final_hash 0xcc1203a69591b134ull -> 0x980f8a18b3d80a23ull [PAR-ORDERS-2, H-THIN] +// Random41/1/1: boundary 0xd2d43ab5fdd2ef0bull -> 0xf15d27dad1575f3bull, rows 0xb907bbe1bb2da083ull -> 0x9245b7b247af4cc1ull, final_hash 0xc55d2d003a5bec69ull -> 0xc730d99532f512d8ull [PAR-ORDERS-2, H-THIN] +// Random41/1/0: boundary 0xd2d43ab5fdd2ef0bull -> 0xf15d27dad1575f3bull, final_hash 0xc55d2d003a5bec69ull -> 0xc730d99532f512d8ull [PAR-ORDERS-2, H-THIN] +// Random42/0/1: boundary 0xa0ee848263988a3dull -> 0x0da9b26d3847ab1full, rows 0x1f2ad0a937e24f39ull -> 0x9c9329bd9adc3676ull, final_hash 0xb124b9f52e8cf9f2ull -> 0x49a0634035589083ull [H-THIN] +// Random42/0/0: boundary 0xa0ee848263988a3dull -> 0x0da9b26d3847ab1full, final_hash 0xb124b9f52e8cf9f2ull -> 0x49a0634035589083ull [H-THIN] +// Random42/1/1: boundary 0x7f6282e75cc72bb0ull -> 0xbd61639c42be9cb3ull, rows 0x23bb6075acaedb2eull -> 0x5d2fe8dd7d35b222ull, final_hash 0xdb2120d5edab4d9full -> 0xa80c78dbeddc4468ull [H-THIN] +// Random42/1/0: boundary 0x7f6282e75cc72bb0ull -> 0xbd61639c42be9cb3ull, final_hash 0xdb2120d5edab4d9full -> 0xa80c78dbeddc4468ull [H-THIN] +// Random43/0/1: boundary 0x7fb8967d6d6d24d2ull -> 0xc521512ba92a54c9ull, rows 0x98cae92178fe91ebull -> 0x5993bd8ceee91044ull, final_hash 0xdc29a310e9c7d06bull -> 0x5fbe995d73a8bff0ull [H-THIN] +// Random43/0/0: boundary 0x7fb8967d6d6d24d2ull -> 0xc521512ba92a54c9ull, final_hash 0xdc29a310e9c7d06bull -> 0x5fbe995d73a8bff0ull [H-THIN] +// Random43/1/1: boundary 0xffd1c55ce888ef72ull -> 0xa9607e56c7e4b193ull, rows 0x8ac3c2c1053a4819ull -> 0x941becccb8fe75ceull, final_hash 0x45393018e93b32beull -> 0xfdc00d0d1ff0cbfbull [H-THIN] +// Random43/1/0: boundary 0xffd1c55ce888ef72ull -> 0xa9607e56c7e4b193ull, final_hash 0x45393018e93b32beull -> 0xfdc00d0d1ff0cbfbull [H-THIN] +// Random44/0/1: boundary 0xdae57d80c4d2a634ull -> 0x971e7c4a38e301ddull, rows 0xf7669a1535370da9ull -> 0x55249f0ae51722feull, final_hash 0xc555f0e88b5eb378ull -> 0x5a13570337a638b5ull [PAR-ORDERS-2, H-THIN] +// Random44/0/0: boundary 0xdae57d80c4d2a634ull -> 0x971e7c4a38e301ddull, final_hash 0xc555f0e88b5eb378ull -> 0x5a13570337a638b5ull [PAR-ORDERS-2, H-THIN] +// Random44/1/1: boundary 0x360b704190f157adull -> 0x3ee064c4f943e93aull, rows 0x5b2a75cdabd7235cull -> 0xbc62822019ded427ull, final_hash 0xde36f7f290af13a5ull -> 0xc18096028ba7be64ull [PAR-ORDERS-2, H-THIN] +// Random44/1/0: boundary 0x360b704190f157adull -> 0x3ee064c4f943e93aull, final_hash 0xde36f7f290af13a5ull -> 0xc18096028ba7be64ull [PAR-ORDERS-2, H-THIN] +// Random45/0/1: boundary 0xbf351c50f4a44f8full -> 0xa155b80e37d41cf2ull, rows 0xfee900af4573bbc1ull -> 0xbacd32f5d8b94030ull, final_hash 0x7f2867c87c78723eull -> 0x598c179a2d5c735cull [PAR-ORDERS-2, H-THIN] +// Random45/0/0: boundary 0xbf351c50f4a44f8full -> 0xa155b80e37d41cf2ull, final_hash 0x7f2867c87c78723eull -> 0x598c179a2d5c735cull [PAR-ORDERS-2, H-THIN] +// Random45/1/1: boundary 0x32a1d85e85b93f0aull -> 0x9644e4dd1f925cdfull, rows 0xebcfcdf78cd848a5ull -> 0x2cc38b54d184cf0aull, final_hash 0x56b0e295081820c0ull -> 0x290739687f68f174ull [PAR-ORDERS-2, H-THIN] +// Random45/1/0: boundary 0x32a1d85e85b93f0aull -> 0x9644e4dd1f925cdfull, final_hash 0x56b0e295081820c0ull -> 0x290739687f68f174ull [PAR-ORDERS-2, H-THIN] +// Random46/0/1: boundary 0x3ad713968f2546d9ull -> 0xa74f65268d41a1bbull, rows 0x84dd0930114cd829ull -> 0xfc4e6864d98d62c0ull, final_hash 0x6bb382cfb976e148ull -> 0x7097cd184da8a4dcull [H-THIN, PAR-CASHFEE] +// Random46/0/0: boundary 0x3ad713968f2546d9ull -> 0xa74f65268d41a1bbull, final_hash 0x6bb382cfb976e148ull -> 0x7097cd184da8a4dcull [H-THIN, PAR-CASHFEE] +// Random46/1/1: boundary 0x997f2e0ee9786d91ull -> 0xb1406e1f21710782ull, rows 0x1ce86cd2c755bea3ull -> 0xe0b910f752cbc3adull, final_hash 0x556134f8585397c9ull -> 0x2539296694529005ull [H-THIN, PAR-CASHFEE] +// Random46/1/0: boundary 0x997f2e0ee9786d91ull -> 0xb1406e1f21710782ull, final_hash 0x556134f8585397c9ull -> 0x2539296694529005ull [H-THIN, PAR-CASHFEE] +// Random47/0/1: boundary 0x8ce025f147bbdb2dull -> 0x214dc1308457a01aull, rows 0x74895c0d69d6b4a8ull -> 0xe293c7196f5c9f95ull, final_hash 0x31ec37588708d5f1ull -> 0x8176d644a621a19eull [H-THIN] +// Random47/0/0: boundary 0x8ce025f147bbdb2dull -> 0x214dc1308457a01aull, final_hash 0x31ec37588708d5f1ull -> 0x8176d644a621a19eull [H-THIN] +// Random47/1/1: boundary 0xebf0544eb982483aull -> 0xfe949f5ba003397dull, rows 0xca410d7365dca790ull -> 0xe112a9940c547399ull, final_hash 0xa8aff582382caf48ull -> 0x9ee620e1bf252639ull [H-THIN] +// Random47/1/0: boundary 0xebf0544eb982483aull -> 0xfe949f5ba003397dull, final_hash 0xa8aff582382caf48ull -> 0x9ee620e1bf252639ull [H-THIN] +// Random48/0/1: boundary 0xaa6b18e4254e2f68ull -> 0x21c0bf9ae910d3acull, rows 0x2f80186c69547a10ull -> 0xe2d98f0ffb5c1b96ull, final_hash 0xaff43b38e6bc02acull -> 0x1e560afd06471537ull [H-THIN] +// Random48/0/0: boundary 0xaa6b18e4254e2f68ull -> 0x21c0bf9ae910d3acull, final_hash 0xaff43b38e6bc02acull -> 0x1e560afd06471537ull [H-THIN] +// Random48/1/1: boundary 0x655a5637ae415d60ull -> 0x4fd90c8ca1025428ull, rows 0x13866e45be14eaeeull -> 0x7706d5e9ebfd9226ull, final_hash 0xa0e205aa768a0bc9ull -> 0xee01efa6d7b40515ull [H-THIN] +// Random48/1/0: boundary 0x655a5637ae415d60ull -> 0x4fd90c8ca1025428ull, final_hash 0xa0e205aa768a0bc9ull -> 0xee01efa6d7b40515ull [H-THIN] constexpr Pinned kPinned[] = { - {"Quiet", 0, 1, 0x1c2b2e37a44f6877ull, 0x81799b58f0e052b1ull, 0x47fe0d7eaf8e51e3ull, 0xe0aa4c0240939bcfull, 40, 40, 0}, - {"Quiet", 0, 0, 0x1c2b2e37a44f6877ull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0xe0aa4c0240939bcfull, 40, 0, 0}, - {"Quiet", 1, 1, 0x0a542471817c0616ull, 0x4210b9ca5a7d77e6ull, 0x47fe0d7eaf8e51e3ull, 0x2e91db500a83cbe9ull, 40, 40, 0}, - {"Quiet", 1, 0, 0x0a542471817c0616ull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0x2e91db500a83cbe9ull, 40, 0, 0}, - {"HoldLongFull", 0, 1, 0xbb82b8cd85ffaae1ull, 0xae96abef4356f11full, 0xa32454de68248651ull, 0xbe87239166611559ull, 40, 40, 1}, - {"HoldLongFull", 0, 0, 0xbb82b8cd85ffaae1ull, 0x14650fb0739d0383ull, 0xa32454de68248651ull, 0xbe87239166611559ull, 40, 0, 1}, - {"HoldLongFull", 1, 1, 0x83f82e6a0b0a5677ull, 0x7700c6fdca38234full, 0xa32454de68248651ull, 0xb6f097b6a6318731ull, 40, 40, 1}, - {"HoldLongFull", 1, 0, 0x83f82e6a0b0a5677ull, 0x14650fb0739d0383ull, 0xa32454de68248651ull, 0xb6f097b6a6318731ull, 40, 0, 1}, - {"MarginCalls", 0, 1, 0x3633b5fd46e3ee1eull, 0x35e47773037c0f4dull, 0x259f52c0c76aafe9ull, 0x64d4054219e25f2full, 36, 36, 3}, - {"MarginCalls", 0, 0, 0x3633b5fd46e3ee1eull, 0x14650fb0739d0383ull, 0x259f52c0c76aafe9ull, 0x64d4054219e25f2full, 36, 0, 3}, - {"MarginCalls", 1, 1, 0x96ca6ef83164c685ull, 0x4b1bc134021b04cbull, 0x92cc9a49e414ed25ull, 0xc10c1fc9e88e7c23ull, 36, 36, 2}, - {"MarginCalls", 1, 0, 0x96ca6ef83164c685ull, 0x14650fb0739d0383ull, 0x92cc9a49e414ed25ull, 0xc10c1fc9e88e7c23ull, 36, 0, 2}, - {"MarginCallCycles", 0, 1, 0x38656d5e12929625ull, 0x5c4aa443e8e76ef9ull, 0x657a702082e43f34ull, 0x55f80f5340e670b7ull, 64, 64, 1}, - {"MarginCallCycles", 0, 0, 0x38656d5e12929625ull, 0x14650fb0739d0383ull, 0x657a702082e43f34ull, 0x55f80f5340e670b7ull, 64, 0, 1}, - {"MarginCallCycles", 1, 1, 0xc969906a8f7b57bbull, 0x67e6e63eb71a661cull, 0x9b23f7fef31dec0cull, 0x8e9e930063b52698ull, 64, 64, 3}, - {"MarginCallCycles", 1, 0, 0xc969906a8f7b57bbull, 0x14650fb0739d0383ull, 0x9b23f7fef31dec0cull, 0x8e9e930063b52698ull, 64, 0, 3}, - {"Bracket", 0, 1, 0x5ceffdf162a625baull, 0x2c0b9b25e51c3426ull, 0xc17c8a18143849dcull, 0xb62be9f335a690fbull, 48, 48, 1}, - {"Bracket", 0, 0, 0x5ceffdf162a625baull, 0x14650fb0739d0383ull, 0xc17c8a18143849dcull, 0xb62be9f335a690fbull, 48, 0, 1}, - {"Bracket", 1, 1, 0x53093fd52be58e6aull, 0x26c6fc455e7a9e3full, 0xd474aeacc0bce022ull, 0x2fbb7d2d1228c959ull, 48, 48, 1}, - {"Bracket", 1, 0, 0x53093fd52be58e6aull, 0x14650fb0739d0383ull, 0xd474aeacc0bce022ull, 0x2fbb7d2d1228c959ull, 48, 0, 1}, - {"Trail", 0, 1, 0xa09d959161ab83b5ull, 0x6db293f4d9ba407eull, 0xd244c7dc0fdc94a1ull, 0xf9e526fb6a079950ull, 48, 48, 1}, - {"Trail", 0, 0, 0xa09d959161ab83b5ull, 0x14650fb0739d0383ull, 0xd244c7dc0fdc94a1ull, 0xf9e526fb6a079950ull, 48, 0, 1}, - {"Trail", 1, 1, 0x2a39cc8c834a5287ull, 0xe23d175bdbe6d254ull, 0xd244c7dc0fdc94a1ull, 0xffac0df8a2dbc19eull, 48, 48, 1}, - {"Trail", 1, 0, 0x2a39cc8c834a5287ull, 0x14650fb0739d0383ull, 0xd244c7dc0fdc94a1ull, 0xffac0df8a2dbc19eull, 48, 0, 1}, - {"DualStops", 0, 1, 0xf53e1aa110098577ull, 0xa45979a387e09947ull, 0xb19c9aea35851016ull, 0x9d20a3a0e3c4b991ull, 40, 40, 1}, - {"DualStops", 0, 0, 0xf53e1aa110098577ull, 0x14650fb0739d0383ull, 0xb19c9aea35851016ull, 0x9d20a3a0e3c4b991ull, 40, 0, 1}, - {"DualStops", 1, 1, 0xd56451041c2c2c4full, 0x3525fdbfa98d36bcull, 0xb19c9aea35851016ull, 0x0e2c1cdfb96b6301ull, 40, 40, 1}, - {"DualStops", 1, 0, 0xd56451041c2c2c4full, 0x14650fb0739d0383ull, 0xb19c9aea35851016ull, 0x0e2c1cdfb96b6301ull, 40, 0, 1}, - {"InvertedStops", 0, 1, 0xa3daa491b43f22e9ull, 0x054c4b8ef1709dc3ull, 0x089245abb04b9cb7ull, 0x056329caffb5a22eull, 40, 40, 4}, - {"InvertedStops", 0, 0, 0xa3daa491b43f22e9ull, 0x14650fb0739d0383ull, 0x089245abb04b9cb7ull, 0x056329caffb5a22eull, 40, 0, 4}, - {"InvertedStops", 1, 1, 0x9b50fac734e90657ull, 0x2d65179a434eebf0ull, 0x089245abb04b9cb7ull, 0xab655e08a941e123ull, 40, 40, 4}, - {"InvertedStops", 1, 0, 0x9b50fac734e90657ull, 0x14650fb0739d0383ull, 0x089245abb04b9cb7ull, 0xab655e08a941e123ull, 40, 0, 4}, - {"PendingParent", 0, 1, 0x47212775031c61c2ull, 0xfd5dbde93e8d520aull, 0x47fe0d7eaf8e51e3ull, 0xe74974df53ef097aull, 40, 40, 0}, - {"PendingParent", 0, 0, 0x47212775031c61c2ull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0xe74974df53ef097aull, 40, 0, 0}, - {"PendingParent", 1, 1, 0x08f4fa130f86a0daull, 0xbd725b4c1d8be787ull, 0x47fe0d7eaf8e51e3ull, 0x27cdc3dedee10a56ull, 40, 40, 0}, - {"PendingParent", 1, 0, 0x08f4fa130f86a0daull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0x27cdc3dedee10a56ull, 40, 0, 0}, - {"GappedAdd", 0, 1, 0x8bd71987fffc51e9ull, 0x42f9dd5f352155d8ull, 0x6528238d0771c54eull, 0xaa98c069e5a019a7ull, 40, 40, 2}, - {"GappedAdd", 0, 0, 0x8bd71987fffc51e9ull, 0x14650fb0739d0383ull, 0x6528238d0771c54eull, 0xaa98c069e5a019a7ull, 40, 0, 2}, - {"GappedAdd", 1, 1, 0x7b93202bd79c1393ull, 0xa4421baf09234dd7ull, 0x6528238d0771c54eull, 0xe086e35f4f128b46ull, 40, 40, 2}, - {"GappedAdd", 1, 0, 0x7b93202bd79c1393ull, 0x14650fb0739d0383ull, 0x6528238d0771c54eull, 0xe086e35f4f128b46ull, 40, 0, 2}, - {"DeclinedReversal", 0, 1, 0xfcb9145b39ec7b5aull, 0xbbddf0514f17aef7ull, 0xc3448bef59024d51ull, 0x764450acb0596bfcull, 24, 24, 1}, - {"DeclinedReversal", 0, 0, 0xfcb9145b39ec7b5aull, 0x14650fb0739d0383ull, 0xc3448bef59024d51ull, 0x764450acb0596bfcull, 24, 0, 1}, - {"DeclinedReversal", 1, 1, 0x4a1378eba012ef40ull, 0x16ea86de895d5b40ull, 0xc3448bef59024d51ull, 0x14734f0e113371e4ull, 24, 24, 1}, - {"DeclinedReversal", 1, 0, 0x4a1378eba012ef40ull, 0x14650fb0739d0383ull, 0xc3448bef59024d51ull, 0x14734f0e113371e4ull, 24, 0, 1}, - {"CalcOnFills", 0, 1, 0x7641907936993779ull, 0x3777fe6ece242880ull, 0x87698c8f4050ce14ull, 0x2583ce2bc6a3c1d9ull, 55, 40, 8}, - {"CalcOnFills", 0, 0, 0x7641907936993779ull, 0x14650fb0739d0383ull, 0x87698c8f4050ce14ull, 0x2583ce2bc6a3c1d9ull, 55, 0, 8}, - {"CalcOnFills", 1, 1, 0xd120e8fefb0dc236ull, 0xdbb28de374d30bc3ull, 0xa16d17de4933efdaull, 0xad6e26cf69199431ull, 52, 40, 6}, - {"CalcOnFills", 1, 0, 0xd120e8fefb0dc236ull, 0x14650fb0739d0383ull, 0xa16d17de4933efdaull, 0xad6e26cf69199431ull, 52, 0, 6}, - {"CloseOrders", 0, 1, 0x78ff3352f7085436ull, 0x896bcdcf15c8a4c6ull, 0xbc944a52841481b1ull, 0x8e67de695bfb2c60ull, 48, 48, 16}, - {"CloseOrders", 0, 0, 0x78ff3352f7085436ull, 0x14650fb0739d0383ull, 0xbc944a52841481b1ull, 0x8e67de695bfb2c60ull, 48, 0, 16}, - {"CloseOrders", 1, 1, 0xe00ae8be3caf3315ull, 0x2584350c36f68309ull, 0x4cdcb678c06d1a59ull, 0xa998448999ff9b1eull, 48, 48, 16}, - {"CloseOrders", 1, 0, 0xe00ae8be3caf3315ull, 0x14650fb0739d0383ull, 0x4cdcb678c06d1a59ull, 0xa998448999ff9b1eull, 48, 0, 16}, - {"CloseOrdersSlipped", 0, 1, 0x58e9160429c03bcaull, 0xd3632e32e35ef626ull, 0x0f201395d3fe211full, 0x13f82da0279b5836ull, 36, 36, 2}, - {"CloseOrdersSlipped", 0, 0, 0x58e9160429c03bcaull, 0x14650fb0739d0383ull, 0x0f201395d3fe211full, 0x13f82da0279b5836ull, 36, 0, 2}, - {"CloseOrdersSlipped", 1, 1, 0x3507072a3e435b83ull, 0xc5d77a9b36fdec85ull, 0x0f201395d3fe211full, 0xc7b132391c547427ull, 36, 36, 2}, - {"CloseOrdersSlipped", 1, 0, 0x3507072a3e435b83ull, 0x14650fb0739d0383ull, 0x0f201395d3fe211full, 0xc7b132391c547427ull, 36, 0, 2}, - {"IntradayLoss", 0, 1, 0x644e48072a025ba8ull, 0x0421711a90fdf429ull, 0x60c9f061d60a69edull, 0xecca5625486ab2e1ull, 40, 40, 1}, - {"IntradayLoss", 0, 0, 0x644e48072a025ba8ull, 0x14650fb0739d0383ull, 0x60c9f061d60a69edull, 0xecca5625486ab2e1ull, 40, 0, 1}, - {"IntradayLoss", 1, 1, 0xce3032bd544d7840ull, 0x4e9d2d333bd5f470ull, 0x60c9f061d60a69edull, 0x4e071c66b57dfeaeull, 40, 40, 1}, - {"IntradayLoss", 1, 0, 0xce3032bd544d7840ull, 0x14650fb0739d0383ull, 0x60c9f061d60a69edull, 0x4e071c66b57dfeaeull, 40, 0, 1}, - {"IntradayCapDeferred", 0, 1, 0x93a814e5f6e8feb7ull, 0xbbbb2712dbbd9ac8ull, 0x2fa61401cccb3845ull, 0x77c142bb11eab624ull, 24, 24, 1}, - {"IntradayCapDeferred", 0, 0, 0x93a814e5f6e8feb7ull, 0x14650fb0739d0383ull, 0x2fa61401cccb3845ull, 0x77c142bb11eab624ull, 24, 0, 1}, - {"IntradayCapDeferred", 1, 1, 0xe23e69831f88d404ull, 0x63469b00b6747db8ull, 0x6517fe8ea7437b74ull, 0x72de8fef40c4c89bull, 24, 24, 1}, - {"IntradayCapDeferred", 1, 0, 0xe23e69831f88d404ull, 0x14650fb0739d0383ull, 0x6517fe8ea7437b74ull, 0x72de8fef40c4c89bull, 24, 0, 1}, - {"PreopenSlice", 0, 1, 0x9f401876dd941c1full, 0xbbce87ebf0d3f638ull, 0x96bb4615ab0ad33bull, 0xf73c6812e8dd0f92ull, 36, 36, 1}, - {"PreopenSlice", 0, 0, 0x9f401876dd941c1full, 0x14650fb0739d0383ull, 0x96bb4615ab0ad33bull, 0xf73c6812e8dd0f92ull, 36, 0, 1}, - {"PreopenSlice", 1, 1, 0x98345cf423b84f80ull, 0x36db489e43fd8218ull, 0x96bb4615ab0ad33bull, 0xa5916c1cffd7bae0ull, 36, 36, 1}, - {"PreopenSlice", 1, 0, 0x98345cf423b84f80ull, 0x14650fb0739d0383ull, 0x96bb4615ab0ad33bull, 0xa5916c1cffd7bae0ull, 36, 0, 1}, - {"SameBarBatch", 0, 1, 0xcfa0ce2dfed4b2e4ull, 0x7de7b618b505f51eull, 0xa1c83f47f2e97c0dull, 0x0bb34499121582afull, 36, 36, 8}, - {"SameBarBatch", 0, 0, 0xcfa0ce2dfed4b2e4ull, 0x14650fb0739d0383ull, 0xa1c83f47f2e97c0dull, 0x0bb34499121582afull, 36, 0, 8}, - {"SameBarBatch", 1, 1, 0x4557f9683348bf23ull, 0x79d43d1295ddc801ull, 0xa1c83f47f2e97c0dull, 0x26a5e7a6d5104dbcull, 36, 36, 8}, - {"SameBarBatch", 1, 0, 0x4557f9683348bf23ull, 0x14650fb0739d0383ull, 0xa1c83f47f2e97c0dull, 0x26a5e7a6d5104dbcull, 36, 0, 8}, - {"CommissionedShort", 0, 1, 0x18b476bd2a3db96dull, 0x54b6ba2e7a2e11c2ull, 0x35b634c9ced2d3caull, 0x48a9193b2c1c1118ull, 32, 32, 1}, - {"CommissionedShort", 0, 0, 0x18b476bd2a3db96dull, 0x14650fb0739d0383ull, 0x35b634c9ced2d3caull, 0x48a9193b2c1c1118ull, 32, 0, 1}, - {"CommissionedShort", 1, 1, 0xb24ff7e17f80c347ull, 0x20608b72a057ada4ull, 0x35b634c9ced2d3caull, 0x3ef1eaec89e6430dull, 32, 32, 1}, - {"CommissionedShort", 1, 0, 0xb24ff7e17f80c347ull, 0x14650fb0739d0383ull, 0x35b634c9ced2d3caull, 0x3ef1eaec89e6430dull, 32, 0, 1}, - {"AccountFx", 0, 1, 0xaeec7af5859fe6b3ull, 0x37039a16e8c77a7bull, 0x3b853d0c008c3078ull, 0x2a28b4e61cd5cbbcull, 48, 48, 1}, - {"AccountFx", 0, 0, 0xaeec7af5859fe6b3ull, 0x14650fb0739d0383ull, 0x3b853d0c008c3078ull, 0x2a28b4e61cd5cbbcull, 48, 0, 1}, - {"Stream", 0, 1, 0x49574a22705f52c0ull, 0x3cade1c0a6b65b75ull, 0xce38e7485b22ec04ull, 0x8c9bb935bf0c32daull, 40, 40, 5}, - {"Stream", 0, 0, 0x49574a22705f52c0ull, 0x14650fb0739d0383ull, 0xce38e7485b22ec04ull, 0x8c9bb935bf0c32daull, 40, 0, 5}, - {"Random01", 0, 1, 0xfa3864e9f5f3a619ull, 0x22da84303f34b121ull, 0xf2ae7ef9674d4425ull, 0xa142013345fb25ddull, 55, 55, 15}, - {"Random01", 0, 0, 0xfa3864e9f5f3a619ull, 0x14650fb0739d0383ull, 0xf2ae7ef9674d4425ull, 0xa142013345fb25ddull, 55, 0, 15}, - {"Random01", 1, 1, 0xfdafd368c22a3bb4ull, 0x30ace005c5ea2e1bull, 0x64f480bfff90f76aull, 0xb44a05ed46f5109dull, 55, 55, 15}, - {"Random01", 1, 0, 0xfdafd368c22a3bb4ull, 0x14650fb0739d0383ull, 0x64f480bfff90f76aull, 0xb44a05ed46f5109dull, 55, 0, 15}, - {"Random02", 0, 1, 0xf933c58464603966ull, 0x8549ba7d7e735482ull, 0x41793244d920be3dull, 0xd9870babe5d18626ull, 58, 40, 12}, - {"Random02", 0, 0, 0xf933c58464603966ull, 0x14650fb0739d0383ull, 0x41793244d920be3dull, 0xd9870babe5d18626ull, 58, 0, 12}, - {"Random02", 1, 1, 0x25ba5f1040df3b56ull, 0x1593a8e96a0c90a9ull, 0x31c2efb8bb736f56ull, 0x0fb50af38c25c54dull, 58, 40, 12}, - {"Random02", 1, 0, 0x25ba5f1040df3b56ull, 0x14650fb0739d0383ull, 0x31c2efb8bb736f56ull, 0x0fb50af38c25c54dull, 58, 0, 12}, - {"Random03", 0, 1, 0xb5a3f885ba0143fdull, 0xd91b7712cc62209bull, 0x8bd01a3594893c29ull, 0xd93ef819aff693f3ull, 44, 44, 11}, - {"Random03", 0, 0, 0xb5a3f885ba0143fdull, 0x14650fb0739d0383ull, 0x8bd01a3594893c29ull, 0xd93ef819aff693f3ull, 44, 0, 11}, - {"Random03", 1, 1, 0x36230659e7bf570eull, 0xdb83075a5690e996ull, 0xde85b8fe81a28e59ull, 0x23acbc0041308125ull, 44, 44, 11}, - {"Random03", 1, 0, 0x36230659e7bf570eull, 0x14650fb0739d0383ull, 0xde85b8fe81a28e59ull, 0x23acbc0041308125ull, 44, 0, 11}, - {"Random04", 0, 1, 0x0c41be29e4497a7cull, 0x1fdaabb2a1c312cdull, 0x38df0af477021418ull, 0x542f1b01ef32d535ull, 44, 44, 18}, - {"Random04", 0, 0, 0x0c41be29e4497a7cull, 0x14650fb0739d0383ull, 0x38df0af477021418ull, 0x542f1b01ef32d535ull, 44, 0, 18}, - {"Random04", 1, 1, 0x93ade030662e0b77ull, 0x5beecc5dc910feb8ull, 0x28a43578727c43b5ull, 0x254f4f4933be69e0ull, 44, 44, 18}, - {"Random04", 1, 0, 0x93ade030662e0b77ull, 0x14650fb0739d0383ull, 0x28a43578727c43b5ull, 0x254f4f4933be69e0ull, 44, 0, 18}, - {"Random05", 0, 1, 0x3e8e6495fe2e6dc1ull, 0x2703dbc59d3516a6ull, 0xf91857e72781f5aeull, 0xb72557b7f6894eafull, 58, 53, 3}, - {"Random05", 0, 0, 0x3e8e6495fe2e6dc1ull, 0x14650fb0739d0383ull, 0xf91857e72781f5aeull, 0xb72557b7f6894eafull, 58, 0, 3}, - {"Random05", 1, 1, 0x4be132773d14c0e0ull, 0xa6803dead66d3343ull, 0xc83a1288cfd7ee4dull, 0xb3561169b361d8bfull, 66, 53, 10}, - {"Random05", 1, 0, 0x4be132773d14c0e0ull, 0x14650fb0739d0383ull, 0xc83a1288cfd7ee4dull, 0xb3561169b361d8bfull, 66, 0, 10}, - {"Random06", 0, 1, 0xecb44da2cbdb5c42ull, 0x5485171eedfc86cfull, 0x75cd705050ee6ca5ull, 0xff2efe1925e45a9dull, 46, 46, 13}, - {"Random06", 0, 0, 0xecb44da2cbdb5c42ull, 0x14650fb0739d0383ull, 0x75cd705050ee6ca5ull, 0xff2efe1925e45a9dull, 46, 0, 13}, - {"Random06", 1, 1, 0x65cc03efd59ec6b5ull, 0x2dce2153546130dbull, 0x5f84022dee0bd36full, 0x1130940f86f108d2ull, 46, 46, 13}, - {"Random06", 1, 0, 0x65cc03efd59ec6b5ull, 0x14650fb0739d0383ull, 0x5f84022dee0bd36full, 0x1130940f86f108d2ull, 46, 0, 13}, - {"Random07", 0, 1, 0x7bf47df91573ab9cull, 0x3c1feb47a4d6d81eull, 0x67ce7c56c16f12a7ull, 0x438a828933e58c5bull, 46, 46, 12}, - {"Random07", 0, 0, 0x7bf47df91573ab9cull, 0x14650fb0739d0383ull, 0x67ce7c56c16f12a7ull, 0x438a828933e58c5bull, 46, 0, 12}, - {"Random07", 1, 1, 0x29fcc776be13bdf5ull, 0x9a5ab1b2b00efff1ull, 0x9cbc1f07c0648d08ull, 0x353c5f0e5bc1c22bull, 46, 46, 12}, - {"Random07", 1, 0, 0x29fcc776be13bdf5ull, 0x14650fb0739d0383ull, 0x9cbc1f07c0648d08ull, 0x353c5f0e5bc1c22bull, 46, 0, 12}, - {"Random08", 0, 1, 0xc2f66d9e5f4943c7ull, 0xf4a38750150093e6ull, 0x68ebab74d498abedull, 0x9ce7eb6ee2b122baull, 93, 59, 24}, - {"Random08", 0, 0, 0xc2f66d9e5f4943c7ull, 0x14650fb0739d0383ull, 0x68ebab74d498abedull, 0x9ce7eb6ee2b122baull, 93, 0, 24}, - {"Random08", 1, 1, 0xbc7e1d017519ee12ull, 0xc18ef29ec0f4fa8eull, 0x84ec2499efb30a9eull, 0xbd83191789f76873ull, 93, 59, 24}, - {"Random08", 1, 0, 0xbc7e1d017519ee12ull, 0x14650fb0739d0383ull, 0x84ec2499efb30a9eull, 0xbd83191789f76873ull, 93, 0, 24}, - {"Random09", 0, 1, 0x400c46d70e6e84bcull, 0xc58cc1d4b5bd3580ull, 0xcda6a598aa82b5f0ull, 0x1f6cdd2e111e016aull, 58, 58, 2}, - {"Random09", 0, 0, 0x400c46d70e6e84bcull, 0x14650fb0739d0383ull, 0xcda6a598aa82b5f0ull, 0x1f6cdd2e111e016aull, 58, 0, 2}, - {"Random09", 1, 1, 0x32d4b35f029bad56ull, 0x447742874dde46e0ull, 0x86b2bbc1465372d7ull, 0x3d99e7abdd92a773ull, 58, 58, 2}, - {"Random09", 1, 0, 0x32d4b35f029bad56ull, 0x14650fb0739d0383ull, 0x86b2bbc1465372d7ull, 0x3d99e7abdd92a773ull, 58, 0, 2}, - {"Random10", 0, 1, 0x998ee74ea73334c6ull, 0x5a3104007433abc0ull, 0xc9ae66b6356d2649ull, 0x0a7888fb9b890fb4ull, 69, 51, 23}, - {"Random10", 0, 0, 0x998ee74ea73334c6ull, 0x14650fb0739d0383ull, 0xc9ae66b6356d2649ull, 0x0a7888fb9b890fb4ull, 69, 0, 23}, - {"Random10", 1, 1, 0xde7dbd52ac33a7ffull, 0xaf8f0ca763b06949ull, 0x724c585ad13c935dull, 0xfbe70c357ac296cfull, 67, 51, 23}, - {"Random10", 1, 0, 0xde7dbd52ac33a7ffull, 0x14650fb0739d0383ull, 0x724c585ad13c935dull, 0xfbe70c357ac296cfull, 67, 0, 23}, - {"Random11", 0, 1, 0x805038d1cb0a3848ull, 0xebc3fde6db4a37f3ull, 0x2f92b1af8d2cfafcull, 0x27bc15c5c142c728ull, 60, 60, 3}, - {"Random11", 0, 0, 0x805038d1cb0a3848ull, 0x14650fb0739d0383ull, 0x2f92b1af8d2cfafcull, 0x27bc15c5c142c728ull, 60, 0, 3}, - {"Random11", 1, 1, 0x4a739854a58a3db9ull, 0xc91f1e4519586a06ull, 0xad6d6d2577cca5c9ull, 0x8af717fd3fa04097ull, 60, 60, 8}, - {"Random11", 1, 0, 0x4a739854a58a3db9ull, 0x14650fb0739d0383ull, 0xad6d6d2577cca5c9ull, 0x8af717fd3fa04097ull, 60, 0, 8}, - {"Random12", 0, 1, 0xb9a9bb3fafab7349ull, 0xe93fb7b821f6a7feull, 0xf60b6b66546787a7ull, 0x29c94b5df165789aull, 92, 62, 22}, - {"Random12", 0, 0, 0xb9a9bb3fafab7349ull, 0x14650fb0739d0383ull, 0xf60b6b66546787a7ull, 0x29c94b5df165789aull, 92, 0, 22}, - {"Random12", 1, 1, 0x298ab491e3caa61full, 0xf8e78400f1c148acull, 0xfc32c809c37c8290ull, 0x927c4394206b42b2ull, 93, 62, 22}, - {"Random12", 1, 0, 0x298ab491e3caa61full, 0x14650fb0739d0383ull, 0xfc32c809c37c8290ull, 0x927c4394206b42b2ull, 93, 0, 22}, - {"Random13", 0, 1, 0x39caae56088c1db8ull, 0x4028b545fb2c3ad9ull, 0x9b711a1ad7f0af2bull, 0xc19e5f5bbefe0e7full, 60, 60, 16}, - {"Random13", 0, 0, 0x39caae56088c1db8ull, 0x14650fb0739d0383ull, 0x9b711a1ad7f0af2bull, 0xc19e5f5bbefe0e7full, 60, 0, 16}, - {"Random13", 1, 1, 0x07b714c492b5ae85ull, 0xa03b3f8bed95186dull, 0x78cbd8f6435e4796ull, 0xa9f1d1a463925b52ull, 60, 60, 16}, - {"Random13", 1, 0, 0x07b714c492b5ae85ull, 0x14650fb0739d0383ull, 0x78cbd8f6435e4796ull, 0xa9f1d1a463925b52ull, 60, 0, 16}, - {"Random14", 0, 1, 0x21085d70db4c336bull, 0x394ebfbc2c15522aull, 0x4e8d14373274d558ull, 0xad1f71b1afdd37f8ull, 49, 49, 16}, - {"Random14", 0, 0, 0x21085d70db4c336bull, 0x14650fb0739d0383ull, 0x4e8d14373274d558ull, 0xad1f71b1afdd37f8ull, 49, 0, 16}, - {"Random14", 1, 1, 0x5a8dbff598244e8cull, 0x4c48857b5cd25816ull, 0xbec287230f684e1dull, 0x7f45221fb1111aa3ull, 49, 49, 16}, - {"Random14", 1, 0, 0x5a8dbff598244e8cull, 0x14650fb0739d0383ull, 0xbec287230f684e1dull, 0x7f45221fb1111aa3ull, 49, 0, 16}, - {"Random15", 0, 1, 0x1fe125a3fb2912dfull, 0x9ef69d4200bfb809ull, 0xc7b14196e7b743c6ull, 0xf8da94854e79d89eull, 42, 42, 12}, - {"Random15", 0, 0, 0x1fe125a3fb2912dfull, 0x14650fb0739d0383ull, 0xc7b14196e7b743c6ull, 0xf8da94854e79d89eull, 42, 0, 12}, - {"Random15", 1, 1, 0x7b17bf5a43044023ull, 0xdeec1e0290d1370dull, 0x9e6d92b9d40ff818ull, 0x01758143c50ac69dull, 42, 42, 12}, - {"Random15", 1, 0, 0x7b17bf5a43044023ull, 0x14650fb0739d0383ull, 0x9e6d92b9d40ff818ull, 0x01758143c50ac69dull, 42, 0, 12}, - {"Random16", 0, 1, 0xde7f3e61caa50135ull, 0x9593cb63a9a06f14ull, 0x6889115fd9a0fe41ull, 0xc99a0f083d0b2507ull, 54, 54, 15}, - {"Random16", 0, 0, 0xde7f3e61caa50135ull, 0x14650fb0739d0383ull, 0x6889115fd9a0fe41ull, 0xc99a0f083d0b2507ull, 54, 0, 15}, - {"Random16", 1, 1, 0xffbc34c7947c611bull, 0xa2b9ee52a1338391ull, 0x552b70400304fd25ull, 0x1f80513beefa0b1aull, 54, 54, 15}, - {"Random16", 1, 0, 0xffbc34c7947c611bull, 0x14650fb0739d0383ull, 0x552b70400304fd25ull, 0x1f80513beefa0b1aull, 54, 0, 15}, - {"Random17", 0, 1, 0x15bab2d913fb1d81ull, 0x0a3d587bbe912dcfull, 0xf3e62779123d7565ull, 0x7185bfaebb406df5ull, 49, 47, 1}, - {"Random17", 0, 0, 0x15bab2d913fb1d81ull, 0x14650fb0739d0383ull, 0xf3e62779123d7565ull, 0x7185bfaebb406df5ull, 49, 0, 1}, - {"Random17", 1, 1, 0xa978788efbcc1631ull, 0xbcbe24594a8acfcaull, 0x5814b347681cd974ull, 0x285eaf39ec559587ull, 50, 47, 2}, - {"Random17", 1, 0, 0xa978788efbcc1631ull, 0x14650fb0739d0383ull, 0x5814b347681cd974ull, 0x285eaf39ec559587ull, 50, 0, 2}, - {"Random18", 0, 1, 0x90ed08a2949b1fb6ull, 0x665cda1c725bc1b2ull, 0x5f03c99e6854a46aull, 0xe16bbfbfa58dbeb1ull, 66, 62, 3}, - {"Random18", 0, 0, 0x90ed08a2949b1fb6ull, 0x14650fb0739d0383ull, 0x5f03c99e6854a46aull, 0xe16bbfbfa58dbeb1ull, 66, 0, 3}, - {"Random18", 1, 1, 0xbf864c04890cf915ull, 0x807a82ee057fbb5dull, 0x5f03c99e6854a46aull, 0x5c359872c6618dd3ull, 66, 62, 3}, - {"Random18", 1, 0, 0xbf864c04890cf915ull, 0x14650fb0739d0383ull, 0x5f03c99e6854a46aull, 0x5c359872c6618dd3ull, 66, 0, 3}, - {"Random19", 0, 1, 0x3c5cc975f928b775ull, 0xea691a951590ece8ull, 0x95d7b888983ad8e6ull, 0x17d53da80c3a895full, 40, 40, 10}, - {"Random19", 0, 0, 0x3c5cc975f928b775ull, 0x14650fb0739d0383ull, 0x95d7b888983ad8e6ull, 0x17d53da80c3a895full, 40, 0, 10}, - {"Random19", 1, 1, 0x92536ad17b9fbf26ull, 0xabc10759f5e1ffa1ull, 0x011bec7774b82ec9ull, 0x168791086f51c368ull, 40, 40, 11}, - {"Random19", 1, 0, 0x92536ad17b9fbf26ull, 0x14650fb0739d0383ull, 0x011bec7774b82ec9ull, 0x168791086f51c368ull, 40, 0, 11}, - {"Random20", 0, 1, 0x90b20d166e1357e0ull, 0x808745030887aa13ull, 0x936a146fbbfb7d4bull, 0x3968c24c22199036ull, 70, 57, 21}, - {"Random20", 0, 0, 0x90b20d166e1357e0ull, 0x14650fb0739d0383ull, 0x936a146fbbfb7d4bull, 0x3968c24c22199036ull, 70, 0, 21}, - {"Random20", 1, 1, 0xa82d4e53d2192a5full, 0x8e1411c055e37af3ull, 0x57a697ba56dd611eull, 0x5c29f15f02417fcaull, 72, 57, 21}, - {"Random20", 1, 0, 0xa82d4e53d2192a5full, 0x14650fb0739d0383ull, 0x57a697ba56dd611eull, 0x5c29f15f02417fcaull, 72, 0, 21}, - {"Random21", 0, 1, 0x18e96beb584671fcull, 0x6c5b4e5a67209f94ull, 0xbc89178a735678f6ull, 0xca719be0f46f42acull, 54, 54, 18}, - {"Random21", 0, 0, 0x18e96beb584671fcull, 0x14650fb0739d0383ull, 0xbc89178a735678f6ull, 0xca719be0f46f42acull, 54, 0, 18}, - {"Random21", 1, 1, 0xd32a038cc5b6ea6eull, 0x9074957bf890f8e6ull, 0x0cb28a314aa970aeull, 0xb6387745e72b0075ull, 54, 54, 18}, - {"Random21", 1, 0, 0xd32a038cc5b6ea6eull, 0x14650fb0739d0383ull, 0x0cb28a314aa970aeull, 0xb6387745e72b0075ull, 54, 0, 18}, - {"Random22", 0, 1, 0xef05a2f01ca0c05cull, 0xb151ebfe9a44ebebull, 0xedb01aeb6057c25bull, 0x1b1b4c42bccc2191ull, 68, 49, 13}, - {"Random22", 0, 0, 0xef05a2f01ca0c05cull, 0x14650fb0739d0383ull, 0xedb01aeb6057c25bull, 0x1b1b4c42bccc2191ull, 68, 0, 13}, - {"Random22", 1, 1, 0x1624d3651edc9089ull, 0x2a2f5457686fdb7full, 0x70ac53cf16690a86ull, 0xb361721c9b7f57e7ull, 68, 49, 13}, - {"Random22", 1, 0, 0x1624d3651edc9089ull, 0x14650fb0739d0383ull, 0x70ac53cf16690a86ull, 0xb361721c9b7f57e7ull, 68, 0, 13}, - {"Random23", 0, 1, 0xc50cf90af093d6e9ull, 0x629f1e11fd65d1cdull, 0xf0e62f804df9e022ull, 0x9f14134203a5c6e9ull, 56, 54, 1}, - {"Random23", 0, 0, 0xc50cf90af093d6e9ull, 0x14650fb0739d0383ull, 0xf0e62f804df9e022ull, 0x9f14134203a5c6e9ull, 56, 0, 1}, - {"Random23", 1, 1, 0x61be7e60a493c3efull, 0x7182d347041d20e2ull, 0xf0e62f804df9e022ull, 0xcac3f08a3f6d7067ull, 56, 54, 1}, - {"Random23", 1, 0, 0x61be7e60a493c3efull, 0x14650fb0739d0383ull, 0xf0e62f804df9e022ull, 0xcac3f08a3f6d7067ull, 56, 0, 1}, - {"Random24", 0, 1, 0xadbbd26ef9c19430ull, 0x7cea484a04d1bc6dull, 0xcdafd09a1c076093ull, 0x880c60f0ef5d5a7dull, 60, 60, 16}, - {"Random24", 0, 0, 0xadbbd26ef9c19430ull, 0x14650fb0739d0383ull, 0xcdafd09a1c076093ull, 0x880c60f0ef5d5a7dull, 60, 0, 16}, - {"Random24", 1, 1, 0x91d34f247834f4e9ull, 0xf9fa951efcc4b1caull, 0xe744bb83904b658aull, 0xc06e2e1df8db5231ull, 60, 60, 16}, - {"Random24", 1, 0, 0x91d34f247834f4e9ull, 0x14650fb0739d0383ull, 0xe744bb83904b658aull, 0xc06e2e1df8db5231ull, 60, 0, 16}, - {"Random25", 0, 1, 0x1091baf336b7e96aull, 0x5836120ce0f3d783ull, 0xf92cddf4fc024c49ull, 0x3f0d68bfc74cf35full, 44, 44, 8}, - {"Random25", 0, 0, 0x1091baf336b7e96aull, 0x14650fb0739d0383ull, 0xf92cddf4fc024c49ull, 0x3f0d68bfc74cf35full, 44, 0, 8}, - {"Random25", 1, 1, 0x4c59eab8929878abull, 0x90bf03c0ed26cd99ull, 0xf92cddf4fc024c49ull, 0xeb9d9c36364972dfull, 44, 44, 8}, - {"Random25", 1, 0, 0x4c59eab8929878abull, 0x14650fb0739d0383ull, 0xf92cddf4fc024c49ull, 0xeb9d9c36364972dfull, 44, 0, 8}, - {"Random26", 0, 1, 0x406a1ebe98ed8ee5ull, 0xc4ceaa2f46b3b1c7ull, 0xd9522717c794d53bull, 0x6d9f1512cb3c63efull, 42, 42, 12}, - {"Random26", 0, 0, 0x406a1ebe98ed8ee5ull, 0x14650fb0739d0383ull, 0xd9522717c794d53bull, 0x6d9f1512cb3c63efull, 42, 0, 12}, - {"Random26", 1, 1, 0x28ce140c5a29687dull, 0x383f34833e879b1bull, 0xf7b8acd382701dd1ull, 0x500ef2279c277ba0ull, 42, 42, 12}, - {"Random26", 1, 0, 0x28ce140c5a29687dull, 0x14650fb0739d0383ull, 0xf7b8acd382701dd1ull, 0x500ef2279c277ba0ull, 42, 0, 12}, - {"Random27", 0, 1, 0xeafcaf72c5794d88ull, 0xfbb77c6f11f0944full, 0xbfe2f70bda5f466dull, 0xf2d844781c8e9819ull, 50, 50, 22}, - {"Random27", 0, 0, 0xeafcaf72c5794d88ull, 0x14650fb0739d0383ull, 0xbfe2f70bda5f466dull, 0xf2d844781c8e9819ull, 50, 0, 22}, - {"Random27", 1, 1, 0x433fe5d0340138d8ull, 0x80e409be19d6131dull, 0x0bb88d26b3dc8cc1ull, 0x2a459b69d95400b1ull, 50, 50, 21}, - {"Random27", 1, 0, 0x433fe5d0340138d8ull, 0x14650fb0739d0383ull, 0x0bb88d26b3dc8cc1ull, 0x2a459b69d95400b1ull, 50, 0, 21}, - {"Random28", 0, 1, 0xa0bb8c1925814fe3ull, 0x8e8763454e7d2dcaull, 0x7ee366b7feceed4eull, 0xe51c2406011cd87dull, 54, 54, 19}, - {"Random28", 0, 0, 0xa0bb8c1925814fe3ull, 0x14650fb0739d0383ull, 0x7ee366b7feceed4eull, 0xe51c2406011cd87dull, 54, 0, 19}, - {"Random28", 1, 1, 0xdb2802eb6d8819c7ull, 0x910e834a102ac932ull, 0xee17522a5f3c15dfull, 0x7406a539e21e57ceull, 54, 54, 19}, - {"Random28", 1, 0, 0xdb2802eb6d8819c7ull, 0x14650fb0739d0383ull, 0xee17522a5f3c15dfull, 0x7406a539e21e57ceull, 54, 0, 19}, - {"Random29", 0, 1, 0x2bbdb428b4a78935ull, 0x6c7b12c7d5aa45e4ull, 0xe6aec4ce1ae7ccd1ull, 0xfd3d2fb6be6132d7ull, 57, 57, 1}, - {"Random29", 0, 0, 0x2bbdb428b4a78935ull, 0x14650fb0739d0383ull, 0xe6aec4ce1ae7ccd1ull, 0xfd3d2fb6be6132d7ull, 57, 0, 1}, - {"Random29", 1, 1, 0x2bc2551fd2128e20ull, 0xb9728dfab890bfd1ull, 0xe6aec4ce1ae7ccd1ull, 0x082924a3760602daull, 57, 57, 1}, - {"Random29", 1, 0, 0x2bc2551fd2128e20ull, 0x14650fb0739d0383ull, 0xe6aec4ce1ae7ccd1ull, 0x082924a3760602daull, 57, 0, 1}, - {"Random30", 0, 1, 0x585a1a9f34476a29ull, 0x5d09652b7dc7603full, 0x260809f2d3657490ull, 0x8e3b3ec00a1454a2ull, 61, 61, 1}, - {"Random30", 0, 0, 0x585a1a9f34476a29ull, 0x14650fb0739d0383ull, 0x260809f2d3657490ull, 0x8e3b3ec00a1454a2ull, 61, 0, 1}, - {"Random30", 1, 1, 0x8de0587cbe97fc0aull, 0x2fc1ebe82b1783eeull, 0x260809f2d3657490ull, 0x7ffde30dc93fc9d7ull, 61, 61, 1}, - {"Random30", 1, 0, 0x8de0587cbe97fc0aull, 0x14650fb0739d0383ull, 0x260809f2d3657490ull, 0x7ffde30dc93fc9d7ull, 61, 0, 1}, - {"Random31", 0, 1, 0x09b545a0067e8c26ull, 0x3c6ae0150bb63629ull, 0xcad16841aad879ddull, 0xfaf6a143c8b0d038ull, 49, 49, 9}, - {"Random31", 0, 0, 0x09b545a0067e8c26ull, 0x14650fb0739d0383ull, 0xcad16841aad879ddull, 0xfaf6a143c8b0d038ull, 49, 0, 9}, - {"Random31", 1, 1, 0xb42bd9cfbe060a73ull, 0x928f7cfde5bdb105ull, 0xc42eba994cb3bc7full, 0xf602f077ecb2b7cfull, 49, 49, 9}, - {"Random31", 1, 0, 0xb42bd9cfbe060a73ull, 0x14650fb0739d0383ull, 0xc42eba994cb3bc7full, 0xf602f077ecb2b7cfull, 49, 0, 9}, - {"Random32", 0, 1, 0x220cd02053dbc40aull, 0x9afe2ada13350695ull, 0xb5c0211fafc19569ull, 0xe577534dd3033b52ull, 62, 62, 18}, - {"Random32", 0, 0, 0x220cd02053dbc40aull, 0x14650fb0739d0383ull, 0xb5c0211fafc19569ull, 0xe577534dd3033b52ull, 62, 0, 18}, - {"Random32", 1, 1, 0xc0ca43094334f008ull, 0xe85eb4136b8c435dull, 0xb1a7a66b620c4fd5ull, 0xba233c9fc46444a4ull, 62, 62, 19}, - {"Random32", 1, 0, 0xc0ca43094334f008ull, 0x14650fb0739d0383ull, 0xb1a7a66b620c4fd5ull, 0xba233c9fc46444a4ull, 62, 0, 19}, - {"Random33", 0, 1, 0xbfcfebe96632bd39ull, 0x116bc2f57887ba4bull, 0xb69ec34fc873583aull, 0xf8a0e855a842d39cull, 51, 51, 13}, - {"Random33", 0, 0, 0xbfcfebe96632bd39ull, 0x14650fb0739d0383ull, 0xb69ec34fc873583aull, 0xf8a0e855a842d39cull, 51, 0, 13}, - {"Random33", 1, 1, 0xe17f0353f37c41f1ull, 0x0ba98d74e8b769b7ull, 0xa51cad1078866f00ull, 0xb76d3ca699ea4ebdull, 51, 51, 13}, - {"Random33", 1, 0, 0xe17f0353f37c41f1ull, 0x14650fb0739d0383ull, 0xa51cad1078866f00ull, 0xb76d3ca699ea4ebdull, 51, 0, 13}, - {"Random34", 0, 1, 0xea1b68237d95805full, 0xb29d9ea005d9c282ull, 0x81535eb933e496e7ull, 0xb03d41aea9c00c63ull, 51, 51, 1}, - {"Random34", 0, 0, 0xea1b68237d95805full, 0x14650fb0739d0383ull, 0x81535eb933e496e7ull, 0xb03d41aea9c00c63ull, 51, 0, 1}, - {"Random34", 1, 1, 0x4ecacf8ab2d85c47ull, 0x294fe821fef3729dull, 0xa788aa75c6b931e3ull, 0xe4e38b6ada294505ull, 51, 51, 2}, - {"Random34", 1, 0, 0x4ecacf8ab2d85c47ull, 0x14650fb0739d0383ull, 0xa788aa75c6b931e3ull, 0xe4e38b6ada294505ull, 51, 0, 2}, - {"Random35", 0, 1, 0xeb16fb1a6bc80c17ull, 0x7265444996669f20ull, 0x834764512474d77cull, 0xb971c6bc6d7ac182ull, 63, 63, 24}, - {"Random35", 0, 0, 0xeb16fb1a6bc80c17ull, 0x14650fb0739d0383ull, 0x834764512474d77cull, 0xb971c6bc6d7ac182ull, 63, 0, 24}, - {"Random35", 1, 1, 0x193ec2cb9a163900ull, 0x9233c78214742aa3ull, 0xce2ab725ccda6ebdull, 0xa9bdcf22e28567b2ull, 63, 63, 24}, - {"Random35", 1, 0, 0x193ec2cb9a163900ull, 0x14650fb0739d0383ull, 0xce2ab725ccda6ebdull, 0xa9bdcf22e28567b2ull, 63, 0, 24}, - {"Random36", 0, 1, 0xd4b5588948d4b755ull, 0x6af2dc7dd208ef5dull, 0x71b837070cbf3386ull, 0x1980e25b047dea78ull, 87, 60, 21}, - {"Random36", 0, 0, 0xd4b5588948d4b755ull, 0x14650fb0739d0383ull, 0x71b837070cbf3386ull, 0x1980e25b047dea78ull, 87, 0, 21}, - {"Random36", 1, 1, 0xd1532dd9d52ae5f4ull, 0x0e1d68d8d6c4c5e2ull, 0xc91ddda9c3e46569ull, 0xe93d9e6c724efef6ull, 87, 60, 21}, - {"Random36", 1, 0, 0xd1532dd9d52ae5f4ull, 0x14650fb0739d0383ull, 0xc91ddda9c3e46569ull, 0xe93d9e6c724efef6ull, 87, 0, 21}, - {"Random37", 0, 1, 0x3a89ff92c403a3e7ull, 0xddda8d77310d918full, 0x52bf39ca2f2765c6ull, 0xff9c74898bf3a72aull, 71, 45, 18}, - {"Random37", 0, 0, 0x3a89ff92c403a3e7ull, 0x14650fb0739d0383ull, 0x52bf39ca2f2765c6ull, 0xff9c74898bf3a72aull, 71, 0, 18}, - {"Random37", 1, 1, 0x8a36a09ee42999ecull, 0xf12dd4800e96f492ull, 0x41de938f8a060981ull, 0x9f14bee9808ee8baull, 70, 45, 17}, - {"Random37", 1, 0, 0x8a36a09ee42999ecull, 0x14650fb0739d0383ull, 0x41de938f8a060981ull, 0x9f14bee9808ee8baull, 70, 0, 17}, - {"Random38", 0, 1, 0x36ed3eee0da888c2ull, 0x2bf58dbe1c5bd9acull, 0xac12ee6aa53f424cull, 0x5b82e26291364fc0ull, 54, 54, 25}, - {"Random38", 0, 0, 0x36ed3eee0da888c2ull, 0x14650fb0739d0383ull, 0xac12ee6aa53f424cull, 0x5b82e26291364fc0ull, 54, 0, 25}, - {"Random38", 1, 1, 0xa3225feaf19b98feull, 0x59c34402fcf68e2aull, 0xc6a0970cf0716d7cull, 0x135197c1ef34cbbdull, 54, 54, 25}, - {"Random38", 1, 0, 0xa3225feaf19b98feull, 0x14650fb0739d0383ull, 0xc6a0970cf0716d7cull, 0x135197c1ef34cbbdull, 54, 0, 25}, - {"Random39", 0, 1, 0x9bcdaebb98702055ull, 0xf4194fbb3f8e63f4ull, 0xc193386eb35a8e65ull, 0x3026f01eb73dfcdaull, 44, 44, 19}, - {"Random39", 0, 0, 0x9bcdaebb98702055ull, 0x14650fb0739d0383ull, 0xc193386eb35a8e65ull, 0x3026f01eb73dfcdaull, 44, 0, 19}, - {"Random39", 1, 1, 0x5fa98522adc27aafull, 0x50b22f98ba965993ull, 0xeea051f0749b0428ull, 0x123f0c5b09aeae91ull, 44, 44, 19}, - {"Random39", 1, 0, 0x5fa98522adc27aafull, 0x14650fb0739d0383ull, 0xeea051f0749b0428ull, 0x123f0c5b09aeae91ull, 44, 0, 19}, - {"Random40", 0, 1, 0x869959795f89f7afull, 0xe9919376f3542ea7ull, 0x78c15a2f88a24807ull, 0xd91533e6639e22f9ull, 77, 53, 18}, - {"Random40", 0, 0, 0x869959795f89f7afull, 0x14650fb0739d0383ull, 0x78c15a2f88a24807ull, 0xd91533e6639e22f9ull, 77, 0, 18}, - {"Random40", 1, 1, 0xf47407130500a9bdull, 0xda9b4928c023ca07ull, 0x41975079a7cc855bull, 0x5a8f1ec5a18f3731ull, 78, 53, 19}, - {"Random40", 1, 0, 0xf47407130500a9bdull, 0x14650fb0739d0383ull, 0x41975079a7cc855bull, 0x5a8f1ec5a18f3731ull, 78, 0, 19}, - {"Random41", 0, 1, 0x6509ace183e7d2cdull, 0x957c5244d07600b9ull, 0xf6e95f1e1a0264bcull, 0xcc1203a69591b134ull, 51, 40, 6}, - {"Random41", 0, 0, 0x6509ace183e7d2cdull, 0x14650fb0739d0383ull, 0xf6e95f1e1a0264bcull, 0xcc1203a69591b134ull, 51, 0, 6}, - {"Random41", 1, 1, 0xd2d43ab5fdd2ef0bull, 0xb907bbe1bb2da083ull, 0xf6e95f1e1a0264bcull, 0xc55d2d003a5bec69ull, 51, 40, 6}, - {"Random41", 1, 0, 0xd2d43ab5fdd2ef0bull, 0x14650fb0739d0383ull, 0xf6e95f1e1a0264bcull, 0xc55d2d003a5bec69ull, 51, 0, 6}, - {"Random42", 0, 1, 0xa0ee848263988a3dull, 0x1f2ad0a937e24f39ull, 0xd4d5dd88f1bc7129ull, 0xb124b9f52e8cf9f2ull, 60, 60, 12}, - {"Random42", 0, 0, 0xa0ee848263988a3dull, 0x14650fb0739d0383ull, 0xd4d5dd88f1bc7129ull, 0xb124b9f52e8cf9f2ull, 60, 0, 12}, - {"Random42", 1, 1, 0x7f6282e75cc72bb0ull, 0x23bb6075acaedb2eull, 0x292b9553961fa163ull, 0xdb2120d5edab4d9full, 60, 60, 12}, - {"Random42", 1, 0, 0x7f6282e75cc72bb0ull, 0x14650fb0739d0383ull, 0x292b9553961fa163ull, 0xdb2120d5edab4d9full, 60, 0, 12}, - {"Random43", 0, 1, 0x7fb8967d6d6d24d2ull, 0x98cae92178fe91ebull, 0xbd8ba613082e0da5ull, 0xdc29a310e9c7d06bull, 63, 63, 14}, - {"Random43", 0, 0, 0x7fb8967d6d6d24d2ull, 0x14650fb0739d0383ull, 0xbd8ba613082e0da5ull, 0xdc29a310e9c7d06bull, 63, 0, 14}, - {"Random43", 1, 1, 0xffd1c55ce888ef72ull, 0x8ac3c2c1053a4819ull, 0x572d3f9f9521e7caull, 0x45393018e93b32beull, 63, 63, 14}, - {"Random43", 1, 0, 0xffd1c55ce888ef72ull, 0x14650fb0739d0383ull, 0x572d3f9f9521e7caull, 0x45393018e93b32beull, 63, 0, 14}, - {"Random44", 0, 1, 0xdae57d80c4d2a634ull, 0xf7669a1535370da9ull, 0x316199af1a335dbfull, 0xc555f0e88b5eb378ull, 69, 47, 15}, - {"Random44", 0, 0, 0xdae57d80c4d2a634ull, 0x14650fb0739d0383ull, 0x316199af1a335dbfull, 0xc555f0e88b5eb378ull, 69, 0, 15}, - {"Random44", 1, 1, 0x360b704190f157adull, 0x5b2a75cdabd7235cull, 0x9f0e22f8fb2632f6ull, 0xde36f7f290af13a5ull, 69, 47, 15}, - {"Random44", 1, 0, 0x360b704190f157adull, 0x14650fb0739d0383ull, 0x9f0e22f8fb2632f6ull, 0xde36f7f290af13a5ull, 69, 0, 15}, - {"Random45", 0, 1, 0xbf351c50f4a44f8full, 0xfee900af4573bbc1ull, 0xf7b0f07cda17c110ull, 0x7f2867c87c78723eull, 99, 63, 25}, - {"Random45", 0, 0, 0xbf351c50f4a44f8full, 0x14650fb0739d0383ull, 0xf7b0f07cda17c110ull, 0x7f2867c87c78723eull, 99, 0, 25}, - {"Random45", 1, 1, 0x32a1d85e85b93f0aull, 0xebcfcdf78cd848a5ull, 0x797aff1ec9239dffull, 0x56b0e295081820c0ull, 99, 63, 24}, - {"Random45", 1, 0, 0x32a1d85e85b93f0aull, 0x14650fb0739d0383ull, 0x797aff1ec9239dffull, 0x56b0e295081820c0ull, 99, 0, 24}, - {"Random46", 0, 1, 0x3ad713968f2546d9ull, 0x84dd0930114cd829ull, 0xfaba8060a07d20fdull, 0x6bb382cfb976e148ull, 52, 52, 1}, - {"Random46", 0, 0, 0x3ad713968f2546d9ull, 0x14650fb0739d0383ull, 0xfaba8060a07d20fdull, 0x6bb382cfb976e148ull, 52, 0, 1}, - {"Random46", 1, 1, 0x997f2e0ee9786d91ull, 0x1ce86cd2c755bea3ull, 0xfaba8060a07d20fdull, 0x556134f8585397c9ull, 52, 52, 1}, - {"Random46", 1, 0, 0x997f2e0ee9786d91ull, 0x14650fb0739d0383ull, 0xfaba8060a07d20fdull, 0x556134f8585397c9ull, 52, 0, 1}, - {"Random47", 0, 1, 0x8ce025f147bbdb2dull, 0x74895c0d69d6b4a8ull, 0x072cedccf12d6a31ull, 0x31ec37588708d5f1ull, 57, 57, 13}, - {"Random47", 0, 0, 0x8ce025f147bbdb2dull, 0x14650fb0739d0383ull, 0x072cedccf12d6a31ull, 0x31ec37588708d5f1ull, 57, 0, 13}, - {"Random47", 1, 1, 0xebf0544eb982483aull, 0xca410d7365dca790ull, 0x33811b67f07874f3ull, 0xa8aff582382caf48ull, 57, 57, 13}, - {"Random47", 1, 0, 0xebf0544eb982483aull, 0x14650fb0739d0383ull, 0x33811b67f07874f3ull, 0xa8aff582382caf48ull, 57, 0, 13}, - {"Random48", 0, 1, 0xaa6b18e4254e2f68ull, 0x2f80186c69547a10ull, 0xda74ceb1a9e7d904ull, 0xaff43b38e6bc02acull, 48, 48, 8}, - {"Random48", 0, 0, 0xaa6b18e4254e2f68ull, 0x14650fb0739d0383ull, 0xda74ceb1a9e7d904ull, 0xaff43b38e6bc02acull, 48, 0, 8}, - {"Random48", 1, 1, 0x655a5637ae415d60ull, 0x13866e45be14eaeeull, 0x0876fbb01d747e2dull, 0xa0e205aa768a0bc9ull, 48, 48, 8}, - {"Random48", 1, 0, 0x655a5637ae415d60ull, 0x14650fb0739d0383ull, 0x0876fbb01d747e2dull, 0xa0e205aa768a0bc9ull, 48, 0, 8}, + {"Quiet", 0, 1, 0xcd8d3ecb4d2c740aull, 0xe6da4342a82c460cull, 0x47fe0d7eaf8e51e3ull, 0x458f1ecf00ef1d5cull, 40, 40, 0}, + {"Quiet", 0, 0, 0xcd8d3ecb4d2c740aull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0x458f1ecf00ef1d5cull, 40, 0, 0}, + {"Quiet", 1, 1, 0x2a102759d268d84dull, 0x923a578d640d7fdbull, 0x47fe0d7eaf8e51e3ull, 0x6ca064a309be6bfaull, 40, 40, 0}, + {"Quiet", 1, 0, 0x2a102759d268d84dull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0x6ca064a309be6bfaull, 40, 0, 0}, + {"HoldLongFull", 0, 1, 0x2be6111eb08aff44ull, 0x37135fdf24860586ull, 0xa32454de68248651ull, 0x6caa6506b43c5228ull, 40, 40, 1}, + {"HoldLongFull", 0, 0, 0x2be6111eb08aff44ull, 0x14650fb0739d0383ull, 0xa32454de68248651ull, 0x6caa6506b43c5228ull, 40, 0, 1}, + {"HoldLongFull", 1, 1, 0x6da42e25a0445e57ull, 0xa90a9669249de5f2ull, 0xa32454de68248651ull, 0x1fdb374b13f0dd9cull, 40, 40, 1}, + {"HoldLongFull", 1, 0, 0x6da42e25a0445e57ull, 0x14650fb0739d0383ull, 0xa32454de68248651ull, 0x1fdb374b13f0dd9cull, 40, 0, 1}, + {"MarginCalls", 0, 1, 0x9ffada7afff423a5ull, 0xf1451bf046aea2aeull, 0x259f52c0c76aafe9ull, 0xd58658d95c54acbeull, 36, 36, 3}, + {"MarginCalls", 0, 0, 0x9ffada7afff423a5ull, 0x14650fb0739d0383ull, 0x259f52c0c76aafe9ull, 0xd58658d95c54acbeull, 36, 0, 3}, + {"MarginCalls", 1, 1, 0x548dd443b38f74b0ull, 0xfdd852b37b07d408ull, 0x92cc9a49e414ed25ull, 0x76f99b20ecd7e68cull, 36, 36, 2}, + {"MarginCalls", 1, 0, 0x548dd443b38f74b0ull, 0x14650fb0739d0383ull, 0x92cc9a49e414ed25ull, 0x76f99b20ecd7e68cull, 36, 0, 2}, + {"MarginCallCycles", 0, 1, 0x53630c222ae9b1a8ull, 0x26e80eef46f3b822ull, 0x657a702082e43f34ull, 0x9dea3ea1d3cbc012ull, 64, 64, 1}, + {"MarginCallCycles", 0, 0, 0x53630c222ae9b1a8ull, 0x14650fb0739d0383ull, 0x657a702082e43f34ull, 0x9dea3ea1d3cbc012ull, 64, 0, 1}, + {"MarginCallCycles", 1, 1, 0xd66af1875ed2e747ull, 0xffa2470471097d1dull, 0x9b23f7fef31dec0cull, 0x22ec1fab2742fa75ull, 64, 64, 3}, + {"MarginCallCycles", 1, 0, 0xd66af1875ed2e747ull, 0x14650fb0739d0383ull, 0x9b23f7fef31dec0cull, 0x22ec1fab2742fa75ull, 64, 0, 3}, + {"Bracket", 0, 1, 0xaeddc0d29a30f3f7ull, 0xd324958e0a8a53deull, 0xc17c8a18143849dcull, 0xef163ce5ad81ccceull, 48, 48, 1}, + {"Bracket", 0, 0, 0xaeddc0d29a30f3f7ull, 0x14650fb0739d0383ull, 0xc17c8a18143849dcull, 0xef163ce5ad81ccceull, 48, 0, 1}, + {"Bracket", 1, 1, 0xdb519033b2a6887bull, 0xb2b3be51c614016dull, 0xd474aeacc0bce022ull, 0x488b102fb534087cull, 48, 48, 1}, + {"Bracket", 1, 0, 0xdb519033b2a6887bull, 0x14650fb0739d0383ull, 0xd474aeacc0bce022ull, 0x488b102fb534087cull, 48, 0, 1}, + {"Trail", 0, 1, 0xa901a158196e8e90ull, 0x3bf9ee1972d44776ull, 0xd244c7dc0fdc94a1ull, 0xf1d8827b0b6f8fcfull, 48, 48, 1}, + {"Trail", 0, 0, 0xa901a158196e8e90ull, 0x14650fb0739d0383ull, 0xd244c7dc0fdc94a1ull, 0xf1d8827b0b6f8fcfull, 48, 0, 1}, + {"Trail", 1, 1, 0xb23147b5951428e2ull, 0x4f409e92b7a0d98aull, 0xd244c7dc0fdc94a1ull, 0x2716f35c358b7145ull, 48, 48, 1}, + {"Trail", 1, 0, 0xb23147b5951428e2ull, 0x14650fb0739d0383ull, 0xd244c7dc0fdc94a1ull, 0x2716f35c358b7145ull, 48, 0, 1}, + {"DualStops", 0, 1, 0x29701bba6d7182b5ull, 0xf109235082c1860eull, 0xb19c9aea35851016ull, 0x34452d46a85f96c6ull, 40, 40, 1}, + {"DualStops", 0, 0, 0x29701bba6d7182b5ull, 0x14650fb0739d0383ull, 0xb19c9aea35851016ull, 0x34452d46a85f96c6ull, 40, 0, 1}, + {"DualStops", 1, 1, 0x86f9654dc6f8ed22ull, 0x4393ede3dede0436ull, 0xb19c9aea35851016ull, 0x1bfce4a56d0314f2ull, 40, 40, 1}, + {"DualStops", 1, 0, 0x86f9654dc6f8ed22ull, 0x14650fb0739d0383ull, 0xb19c9aea35851016ull, 0x1bfce4a56d0314f2ull, 40, 0, 1}, + {"InvertedStops", 0, 1, 0xeb8c3dfbd9d273a2ull, 0xb7ecf902b2ff0befull, 0x089245abb04b9cb7ull, 0x1993f824c35e154dull, 40, 40, 4}, + {"InvertedStops", 0, 0, 0xeb8c3dfbd9d273a2ull, 0x14650fb0739d0383ull, 0x089245abb04b9cb7ull, 0x1993f824c35e154dull, 40, 0, 4}, + {"InvertedStops", 1, 1, 0x976cbd72b16fcb6full, 0xe49f3d6c9fe39f5bull, 0x089245abb04b9cb7ull, 0x69e468caa11329ccull, 40, 40, 4}, + {"InvertedStops", 1, 0, 0x976cbd72b16fcb6full, 0x14650fb0739d0383ull, 0x089245abb04b9cb7ull, 0x69e468caa11329ccull, 40, 0, 4}, + {"PendingParent", 0, 1, 0xc2b9247123313aceull, 0xa8f42cb39e1aeff3ull, 0x47fe0d7eaf8e51e3ull, 0xfbf54246d7baccdaull, 40, 40, 0}, + {"PendingParent", 0, 0, 0xc2b9247123313aceull, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0xfbf54246d7baccdaull, 40, 0, 0}, + {"PendingParent", 1, 1, 0xcb795828e625815full, 0x808c94697e2231bdull, 0x47fe0d7eaf8e51e3ull, 0x0f9d2bb1d6492d5aull, 40, 40, 0}, + {"PendingParent", 1, 0, 0xcb795828e625815full, 0x14650fb0739d0383ull, 0x47fe0d7eaf8e51e3ull, 0x0f9d2bb1d6492d5aull, 40, 0, 0}, + {"GappedAdd", 0, 1, 0x2aa05060cf893d51ull, 0xca15382fd8d71504ull, 0x6528238d0771c54eull, 0x53a5ed59712f1502ull, 40, 40, 2}, + {"GappedAdd", 0, 0, 0x2aa05060cf893d51ull, 0x14650fb0739d0383ull, 0x6528238d0771c54eull, 0x53a5ed59712f1502ull, 40, 0, 2}, + {"GappedAdd", 1, 1, 0x1de2b53c2e69ae04ull, 0xb77b870b1ef756a3ull, 0x6528238d0771c54eull, 0xc471e2ce0ce5b5b7ull, 40, 40, 2}, + {"GappedAdd", 1, 0, 0x1de2b53c2e69ae04ull, 0x14650fb0739d0383ull, 0x6528238d0771c54eull, 0xc471e2ce0ce5b5b7ull, 40, 0, 2}, + {"DeclinedReversal", 0, 1, 0x5dc519d25cd8e456ull, 0xcd770d65bdeb5e28ull, 0xc3448bef59024d51ull, 0xacfb010f8ef2c1dbull, 24, 24, 1}, + {"DeclinedReversal", 0, 0, 0x5dc519d25cd8e456ull, 0x14650fb0739d0383ull, 0xc3448bef59024d51ull, 0xacfb010f8ef2c1dbull, 24, 0, 1}, + {"DeclinedReversal", 1, 1, 0x506c2023d62a4e6full, 0xe0eb2dae405533f4ull, 0xc3448bef59024d51ull, 0x980a839868ba5b97ull, 24, 24, 1}, + {"DeclinedReversal", 1, 0, 0x506c2023d62a4e6full, 0x14650fb0739d0383ull, 0xc3448bef59024d51ull, 0x980a839868ba5b97ull, 24, 0, 1}, + {"CalcOnFills", 0, 1, 0xbd9feba11a355c84ull, 0xbd62a54cd2c27075ull, 0x87698c8f4050ce14ull, 0x8e716ca55979f5e4ull, 55, 40, 8}, + {"CalcOnFills", 0, 0, 0xbd9feba11a355c84ull, 0x14650fb0739d0383ull, 0x87698c8f4050ce14ull, 0x8e716ca55979f5e4ull, 55, 0, 8}, + {"CalcOnFills", 1, 1, 0x0e2860d938b1d36dull, 0xfe826221acbd42c3ull, 0xa16d17de4933efdaull, 0x1ae91d50d6762fd0ull, 52, 40, 6}, + {"CalcOnFills", 1, 0, 0x0e2860d938b1d36dull, 0x14650fb0739d0383ull, 0xa16d17de4933efdaull, 0x1ae91d50d6762fd0ull, 52, 0, 6}, + {"CloseOrders", 0, 1, 0xb27e9cfcbfee41d3ull, 0x0033c4a6a644f0ddull, 0xbc944a52841481b1ull, 0xbedbc1183b7526a1ull, 48, 48, 16}, + {"CloseOrders", 0, 0, 0xb27e9cfcbfee41d3ull, 0x14650fb0739d0383ull, 0xbc944a52841481b1ull, 0xbedbc1183b7526a1ull, 48, 0, 16}, + {"CloseOrders", 1, 1, 0xff96b9591f72f309ull, 0x5aaa3b2f562a501dull, 0x4cdcb678c06d1a59ull, 0xc542c7fb8bb5559dull, 48, 48, 16}, + {"CloseOrders", 1, 0, 0xff96b9591f72f309ull, 0x14650fb0739d0383ull, 0x4cdcb678c06d1a59ull, 0xc542c7fb8bb5559dull, 48, 0, 16}, + {"CloseOrdersSlipped", 0, 1, 0xc2834ef9bd74b55full, 0xb1042b50a92de9a0ull, 0x0f201395d3fe211full, 0x6c12422a13b3f077ull, 36, 36, 2}, + {"CloseOrdersSlipped", 0, 0, 0xc2834ef9bd74b55full, 0x14650fb0739d0383ull, 0x0f201395d3fe211full, 0x6c12422a13b3f077ull, 36, 0, 2}, + {"CloseOrdersSlipped", 1, 1, 0x71906fcd647a169full, 0xa2710dbb1c8a57edull, 0x0f201395d3fe211full, 0x0330185649c756c2ull, 36, 36, 2}, + {"CloseOrdersSlipped", 1, 0, 0x71906fcd647a169full, 0x14650fb0739d0383ull, 0x0f201395d3fe211full, 0x0330185649c756c2ull, 36, 0, 2}, + {"IntradayLoss", 0, 1, 0x8a9bd5dd1429352eull, 0x616b96966292e8adull, 0x60c9f061d60a69edull, 0xc14ffe9c379fbee2ull, 40, 40, 1}, + {"IntradayLoss", 0, 0, 0x8a9bd5dd1429352eull, 0x14650fb0739d0383ull, 0x60c9f061d60a69edull, 0xc14ffe9c379fbee2ull, 40, 0, 1}, + {"IntradayLoss", 1, 1, 0x1a92d1a03b233c6cull, 0x3382648892a694c4ull, 0x60c9f061d60a69edull, 0x66f4f2fe8b455019ull, 40, 40, 1}, + {"IntradayLoss", 1, 0, 0x1a92d1a03b233c6cull, 0x14650fb0739d0383ull, 0x60c9f061d60a69edull, 0x66f4f2fe8b455019ull, 40, 0, 1}, + {"IntradayCapDeferred", 0, 1, 0xde19edf4087b930dull, 0x560de539b138ea41ull, 0x2fa61401cccb3845ull, 0x7730e7fe703663edull, 24, 24, 1}, + {"IntradayCapDeferred", 0, 0, 0xde19edf4087b930dull, 0x14650fb0739d0383ull, 0x2fa61401cccb3845ull, 0x7730e7fe703663edull, 24, 0, 1}, + {"IntradayCapDeferred", 1, 1, 0x293bdcc491ae22d7ull, 0xd165d92830916a1cull, 0x6517fe8ea7437b74ull, 0x7ba4fe8defa15d4aull, 24, 24, 1}, + {"IntradayCapDeferred", 1, 0, 0x293bdcc491ae22d7ull, 0x14650fb0739d0383ull, 0x6517fe8ea7437b74ull, 0x7ba4fe8defa15d4aull, 24, 0, 1}, + {"PreopenSlice", 0, 1, 0xcbc8fb1ba9d83481ull, 0xbe57d9275ae9e2bbull, 0x96bb4615ab0ad33bull, 0x62f97adf60eb373full, 36, 36, 1}, + {"PreopenSlice", 0, 0, 0xcbc8fb1ba9d83481ull, 0x14650fb0739d0383ull, 0x96bb4615ab0ad33bull, 0x62f97adf60eb373full, 36, 0, 1}, + {"PreopenSlice", 1, 1, 0xadcfe58d853cedfcull, 0xecc971145edced41ull, 0x96bb4615ab0ad33bull, 0x94fff9b38c7f1bd5ull, 36, 36, 1}, + {"PreopenSlice", 1, 0, 0xadcfe58d853cedfcull, 0x14650fb0739d0383ull, 0x96bb4615ab0ad33bull, 0x94fff9b38c7f1bd5ull, 36, 0, 1}, + {"SameBarBatch", 0, 1, 0x85b57cf2c67fbdd6ull, 0xdf711eb77d489011ull, 0xa1c83f47f2e97c0dull, 0x77819f662ca36048ull, 36, 36, 8}, + {"SameBarBatch", 0, 0, 0x85b57cf2c67fbdd6ull, 0x14650fb0739d0383ull, 0xa1c83f47f2e97c0dull, 0x77819f662ca36048ull, 36, 0, 8}, + {"SameBarBatch", 1, 1, 0x5d280dcf47a7af5bull, 0x987f9a54f93d934eull, 0xa1c83f47f2e97c0dull, 0x6a0ac704a36bef57ull, 36, 36, 8}, + {"SameBarBatch", 1, 0, 0x5d280dcf47a7af5bull, 0x14650fb0739d0383ull, 0xa1c83f47f2e97c0dull, 0x6a0ac704a36bef57ull, 36, 0, 8}, + {"CommissionedShort", 0, 1, 0xc73e33b30bf09c29ull, 0x26a3e6d04263ad27ull, 0x35b634c9ced2d3caull, 0xeb0d9ea9f0bdfd4bull, 32, 32, 1}, + {"CommissionedShort", 0, 0, 0xc73e33b30bf09c29ull, 0x14650fb0739d0383ull, 0x35b634c9ced2d3caull, 0xeb0d9ea9f0bdfd4bull, 32, 0, 1}, + {"CommissionedShort", 1, 1, 0x350b092698bdf8fdull, 0x3fe25a6001c6c4feull, 0x35b634c9ced2d3caull, 0xd73d8e053dbf5cd2ull, 32, 32, 1}, + {"CommissionedShort", 1, 0, 0x350b092698bdf8fdull, 0x14650fb0739d0383ull, 0x35b634c9ced2d3caull, 0xd73d8e053dbf5cd2ull, 32, 0, 1}, + {"AccountFx", 0, 1, 0xd27340484425ff52ull, 0x4e596d61c552597eull, 0x3b853d0c008c3078ull, 0xea884b09a27ea73bull, 48, 48, 1}, + {"AccountFx", 0, 0, 0xd27340484425ff52ull, 0x14650fb0739d0383ull, 0x3b853d0c008c3078ull, 0xea884b09a27ea73bull, 48, 0, 1}, + {"Stream", 0, 1, 0x4f42bff80af50865ull, 0xca31c77c5a63ae01ull, 0xce38e7485b22ec04ull, 0xeb75594695696b2full, 40, 40, 5}, + {"Stream", 0, 0, 0x4f42bff80af50865ull, 0x14650fb0739d0383ull, 0xce38e7485b22ec04ull, 0xeb75594695696b2full, 40, 0, 5}, + {"Random01", 0, 1, 0xe7fdfddc42a2ae75ull, 0x0b38785d82bc8fe6ull, 0xf2ae7ef9674d4425ull, 0x16fe00b5a88e8ff2ull, 55, 55, 15}, + {"Random01", 0, 0, 0xe7fdfddc42a2ae75ull, 0x14650fb0739d0383ull, 0xf2ae7ef9674d4425ull, 0x16fe00b5a88e8ff2ull, 55, 0, 15}, + {"Random01", 1, 1, 0x64e7087d99c04c46ull, 0x1c0e119f02f64ddeull, 0x64f480bfff90f76aull, 0xb0114372fd93a01cull, 55, 55, 15}, + {"Random01", 1, 0, 0x64e7087d99c04c46ull, 0x14650fb0739d0383ull, 0x64f480bfff90f76aull, 0xb0114372fd93a01cull, 55, 0, 15}, + {"Random02", 0, 1, 0x8a2b805556d22659ull, 0xce086fbf3b8296e5ull, 0x41793244d920be3dull, 0x5221fce916020c33ull, 58, 40, 12}, + {"Random02", 0, 0, 0x8a2b805556d22659ull, 0x14650fb0739d0383ull, 0x41793244d920be3dull, 0x5221fce916020c33ull, 58, 0, 12}, + {"Random02", 1, 1, 0x844819f3aaccd232ull, 0x74fc5a6e6a256e36ull, 0x31c2efb8bb736f56ull, 0x089086fe02b53049ull, 58, 40, 12}, + {"Random02", 1, 0, 0x844819f3aaccd232ull, 0x14650fb0739d0383ull, 0x31c2efb8bb736f56ull, 0x089086fe02b53049ull, 58, 0, 12}, + {"Random03", 0, 1, 0x2dad9d24e763c932ull, 0xa3f6fa59cf96ba52ull, 0x8bd01a3594893c29ull, 0xeb202b23d902f8b8ull, 44, 44, 11}, + {"Random03", 0, 0, 0x2dad9d24e763c932ull, 0x14650fb0739d0383ull, 0x8bd01a3594893c29ull, 0xeb202b23d902f8b8ull, 44, 0, 11}, + {"Random03", 1, 1, 0x7196371dde7bb130ull, 0xbdf7be966e72b156ull, 0xde85b8fe81a28e59ull, 0x92f95865ffb213eeull, 44, 44, 11}, + {"Random03", 1, 0, 0x7196371dde7bb130ull, 0x14650fb0739d0383ull, 0xde85b8fe81a28e59ull, 0x92f95865ffb213eeull, 44, 0, 11}, + {"Random04", 0, 1, 0x9d74676f534d8b76ull, 0xdb1c5eb0cb9f6c32ull, 0x38df0af477021418ull, 0x82a419ac37909af3ull, 44, 44, 18}, + {"Random04", 0, 0, 0x9d74676f534d8b76ull, 0x14650fb0739d0383ull, 0x38df0af477021418ull, 0x82a419ac37909af3ull, 44, 0, 18}, + {"Random04", 1, 1, 0x19cdc09e2267df5aull, 0x7eca887896ce161bull, 0x28a43578727c43b5ull, 0x350e10b2e19f0b44ull, 44, 44, 18}, + {"Random04", 1, 0, 0x19cdc09e2267df5aull, 0x14650fb0739d0383ull, 0x28a43578727c43b5ull, 0x350e10b2e19f0b44ull, 44, 0, 18}, + {"Random05", 0, 1, 0xac397a37be0d4789ull, 0x64b7bb57706476b6ull, 0xf91857e72781f5aeull, 0x85274de8bea42d05ull, 58, 53, 3}, + {"Random05", 0, 0, 0xac397a37be0d4789ull, 0x14650fb0739d0383ull, 0xf91857e72781f5aeull, 0x85274de8bea42d05ull, 58, 0, 3}, + {"Random05", 1, 1, 0xbd96a4fdd0383edbull, 0xdcb784e78fba749eull, 0xc83a1288cfd7ee4dull, 0x919116731d265d3dull, 66, 53, 10}, + {"Random05", 1, 0, 0xbd96a4fdd0383edbull, 0x14650fb0739d0383ull, 0xc83a1288cfd7ee4dull, 0x919116731d265d3dull, 66, 0, 10}, + {"Random06", 0, 1, 0x7deef79bd0217f64ull, 0xd166805cc595f38eull, 0x75cd705050ee6ca5ull, 0x4c7c821f90e38cccull, 46, 46, 13}, + {"Random06", 0, 0, 0x7deef79bd0217f64ull, 0x14650fb0739d0383ull, 0x75cd705050ee6ca5ull, 0x4c7c821f90e38cccull, 46, 0, 13}, + {"Random06", 1, 1, 0x8a1bb21a5e64fed9ull, 0x7d881d1a93512848ull, 0x5f84022dee0bd36full, 0xaa8716ffbdfd11b2ull, 46, 46, 13}, + {"Random06", 1, 0, 0x8a1bb21a5e64fed9ull, 0x14650fb0739d0383ull, 0x5f84022dee0bd36full, 0xaa8716ffbdfd11b2ull, 46, 0, 13}, + {"Random07", 0, 1, 0xe6120913fe5da50dull, 0xe90b62d77e4625b7ull, 0x67ce7c56c16f12a7ull, 0xbc6f67df5b0f0b2full, 46, 46, 12}, + {"Random07", 0, 0, 0xe6120913fe5da50dull, 0x14650fb0739d0383ull, 0x67ce7c56c16f12a7ull, 0xbc6f67df5b0f0b2full, 46, 0, 12}, + {"Random07", 1, 1, 0xe2c3492a3155bf7dull, 0x0e5f1aa6cabc3a32ull, 0x9cbc1f07c0648d08ull, 0xbad1ae57e202b2d0ull, 46, 46, 12}, + {"Random07", 1, 0, 0xe2c3492a3155bf7dull, 0x14650fb0739d0383ull, 0x9cbc1f07c0648d08ull, 0xbad1ae57e202b2d0ull, 46, 0, 12}, + {"Random08", 0, 1, 0x0a3af182d33c479aull, 0xc91b237d10b90ecbull, 0x68ebab74d498abedull, 0x1930dc38f4f605fbull, 93, 59, 24}, + {"Random08", 0, 0, 0x0a3af182d33c479aull, 0x14650fb0739d0383ull, 0x68ebab74d498abedull, 0x1930dc38f4f605fbull, 93, 0, 24}, + {"Random08", 1, 1, 0xc981400f75b65614ull, 0xb37d8f41ed349758ull, 0x84ec2499efb30a9eull, 0xe48afc5e8a67e872ull, 93, 59, 24}, + {"Random08", 1, 0, 0xc981400f75b65614ull, 0x14650fb0739d0383ull, 0x84ec2499efb30a9eull, 0xe48afc5e8a67e872ull, 93, 0, 24}, + {"Random09", 0, 1, 0xaaff8dcd840f16ceull, 0x9021538022cf9b9bull, 0xcda6a598aa82b5f0ull, 0xbbe3c07366b0cbeaull, 58, 58, 2}, + {"Random09", 0, 0, 0xaaff8dcd840f16ceull, 0x14650fb0739d0383ull, 0xcda6a598aa82b5f0ull, 0xbbe3c07366b0cbeaull, 58, 0, 2}, + {"Random09", 1, 1, 0x026e829145cad6f5ull, 0x32ca6bae685cea43ull, 0x86b2bbc1465372d7ull, 0xc55c597c6508d557ull, 58, 58, 2}, + {"Random09", 1, 0, 0x026e829145cad6f5ull, 0x14650fb0739d0383ull, 0x86b2bbc1465372d7ull, 0xc55c597c6508d557ull, 58, 0, 2}, + {"Random10", 0, 1, 0x82933908492fe6aeull, 0x839a10fa6478c692ull, 0xc9ae66b6356d2649ull, 0x6907e89e2f502bd7ull, 69, 51, 23}, + {"Random10", 0, 0, 0x82933908492fe6aeull, 0x14650fb0739d0383ull, 0xc9ae66b6356d2649ull, 0x6907e89e2f502bd7ull, 69, 0, 23}, + {"Random10", 1, 1, 0x3af283057bf553afull, 0xd948d5022e691361ull, 0x724c585ad13c935dull, 0x794b361143554e09ull, 67, 51, 23}, + {"Random10", 1, 0, 0x3af283057bf553afull, 0x14650fb0739d0383ull, 0x724c585ad13c935dull, 0x794b361143554e09ull, 67, 0, 23}, + {"Random11", 0, 1, 0x9f99f3ad7d35ba23ull, 0x29a35fd6705cc48aull, 0x2f92b1af8d2cfafcull, 0x8235cad2a6dfb3f3ull, 60, 60, 3}, + {"Random11", 0, 0, 0x9f99f3ad7d35ba23ull, 0x14650fb0739d0383ull, 0x2f92b1af8d2cfafcull, 0x8235cad2a6dfb3f3ull, 60, 0, 3}, + {"Random11", 1, 1, 0xf6fb4588ed987e13ull, 0x7dcd8b5314fb7562ull, 0xad6d6d2577cca5c9ull, 0xb44ac10707d01de6ull, 60, 60, 8}, + {"Random11", 1, 0, 0xf6fb4588ed987e13ull, 0x14650fb0739d0383ull, 0xad6d6d2577cca5c9ull, 0xb44ac10707d01de6ull, 60, 0, 8}, + {"Random12", 0, 1, 0xc02247ff66b0dc87ull, 0xf39477bfa32bb689ull, 0xf60b6b66546787a7ull, 0x5f0b7200f4ecdfe1ull, 92, 62, 22}, + {"Random12", 0, 0, 0xc02247ff66b0dc87ull, 0x14650fb0739d0383ull, 0xf60b6b66546787a7ull, 0x5f0b7200f4ecdfe1ull, 92, 0, 22}, + {"Random12", 1, 1, 0xfcd62d0327e0553aull, 0xf36949e9fad408efull, 0xfc32c809c37c8290ull, 0x84a69af72aae96d9ull, 93, 62, 22}, + {"Random12", 1, 0, 0xfcd62d0327e0553aull, 0x14650fb0739d0383ull, 0xfc32c809c37c8290ull, 0x84a69af72aae96d9ull, 93, 0, 22}, + {"Random13", 0, 1, 0x53be6a77e760fe0bull, 0xe1b3dac90adb9f4bull, 0x9b711a1ad7f0af2bull, 0xafd98319ef581f61ull, 60, 60, 16}, + {"Random13", 0, 0, 0x53be6a77e760fe0bull, 0x14650fb0739d0383ull, 0x9b711a1ad7f0af2bull, 0xafd98319ef581f61ull, 60, 0, 16}, + {"Random13", 1, 1, 0xd238668589210027ull, 0xc4b7dac2b95e01fbull, 0x78cbd8f6435e4796ull, 0xb4b9d6f51a69342full, 60, 60, 16}, + {"Random13", 1, 0, 0xd238668589210027ull, 0x14650fb0739d0383ull, 0x78cbd8f6435e4796ull, 0xb4b9d6f51a69342full, 60, 0, 16}, + {"Random14", 0, 1, 0xd37abe66ab158e44ull, 0x38cb950f5ceb9d9cull, 0x4e8d14373274d558ull, 0x9d488869e9756d89ull, 49, 49, 16}, + {"Random14", 0, 0, 0xd37abe66ab158e44ull, 0x14650fb0739d0383ull, 0x4e8d14373274d558ull, 0x9d488869e9756d89ull, 49, 0, 16}, + {"Random14", 1, 1, 0xb8392469a2a0abfeull, 0xfed8161637d6c938ull, 0xbec287230f684e1dull, 0x04b4096b966c585cull, 49, 49, 16}, + {"Random14", 1, 0, 0xb8392469a2a0abfeull, 0x14650fb0739d0383ull, 0xbec287230f684e1dull, 0x04b4096b966c585cull, 49, 0, 16}, + {"Random15", 0, 1, 0x1fc67d9462b82276ull, 0x167381a97aa1c870ull, 0xc7b14196e7b743c6ull, 0x69f92f0d300a126eull, 42, 42, 12}, + {"Random15", 0, 0, 0x1fc67d9462b82276ull, 0x14650fb0739d0383ull, 0xc7b14196e7b743c6ull, 0x69f92f0d300a126eull, 42, 0, 12}, + {"Random15", 1, 1, 0xbbbd08da5449bc19ull, 0x2a9a6d807f0676baull, 0x9e6d92b9d40ff818ull, 0xe8dd6d8df584c6d6ull, 42, 42, 12}, + {"Random15", 1, 0, 0xbbbd08da5449bc19ull, 0x14650fb0739d0383ull, 0x9e6d92b9d40ff818ull, 0xe8dd6d8df584c6d6ull, 42, 0, 12}, + {"Random16", 0, 1, 0xe8851a2e0e645003ull, 0xcfab7fb0e8c24b17ull, 0x6889115fd9a0fe41ull, 0x5639dcc11e103f25ull, 54, 54, 15}, + {"Random16", 0, 0, 0xe8851a2e0e645003ull, 0x14650fb0739d0383ull, 0x6889115fd9a0fe41ull, 0x5639dcc11e103f25ull, 54, 0, 15}, + {"Random16", 1, 1, 0x431440275998e000ull, 0x90d12ea4ed48dd05ull, 0x552b70400304fd25ull, 0xf7e82ef73f56a2eeull, 54, 54, 15}, + {"Random16", 1, 0, 0x431440275998e000ull, 0x14650fb0739d0383ull, 0x552b70400304fd25ull, 0xf7e82ef73f56a2eeull, 54, 0, 15}, + {"Random17", 0, 1, 0xf4b303a1ef4696f7ull, 0x6417861b10d32340ull, 0xf3e62779123d7565ull, 0xa61332be53034674ull, 49, 47, 1}, + {"Random17", 0, 0, 0xf4b303a1ef4696f7ull, 0x14650fb0739d0383ull, 0xf3e62779123d7565ull, 0xa61332be53034674ull, 49, 0, 1}, + {"Random17", 1, 1, 0x4fbe2d70efbe24eaull, 0xaf770da711891f5aull, 0x5814b347681cd974ull, 0x6665febf6fb7f354ull, 50, 47, 2}, + {"Random17", 1, 0, 0x4fbe2d70efbe24eaull, 0x14650fb0739d0383ull, 0x5814b347681cd974ull, 0x6665febf6fb7f354ull, 50, 0, 2}, + {"Random18", 0, 1, 0xf434360cb53f77baull, 0xff5d4060a3fafb03ull, 0x5f03c99e6854a46aull, 0xe0f82ba48054cc9cull, 66, 62, 3}, + {"Random18", 0, 0, 0xf434360cb53f77baull, 0x14650fb0739d0383ull, 0x5f03c99e6854a46aull, 0xe0f82ba48054cc9cull, 66, 0, 3}, + {"Random18", 1, 1, 0x7b6ad000002b1959ull, 0x70472dcb4abe021full, 0x5f03c99e6854a46aull, 0x73a5afc061a888aaull, 66, 62, 3}, + {"Random18", 1, 0, 0x7b6ad000002b1959ull, 0x14650fb0739d0383ull, 0x5f03c99e6854a46aull, 0x73a5afc061a888aaull, 66, 0, 3}, + {"Random19", 0, 1, 0xa21f7b0b62d72170ull, 0xda14d42130450ee8ull, 0x95d7b888983ad8e6ull, 0x0bcf8b1fde6775f8ull, 40, 40, 10}, + {"Random19", 0, 0, 0xa21f7b0b62d72170ull, 0x14650fb0739d0383ull, 0x95d7b888983ad8e6ull, 0x0bcf8b1fde6775f8ull, 40, 0, 10}, + {"Random19", 1, 1, 0xceae18544190aae2ull, 0x7d8104e455ec01f8ull, 0x011bec7774b82ec9ull, 0x426c72d5bce82609ull, 40, 40, 11}, + {"Random19", 1, 0, 0xceae18544190aae2ull, 0x14650fb0739d0383ull, 0x011bec7774b82ec9ull, 0x426c72d5bce82609ull, 40, 0, 11}, + {"Random20", 0, 1, 0xad83f25bfdab1b50ull, 0x103b25042e1313deull, 0x936a146fbbfb7d4bull, 0xb27d00f43fad75a1ull, 70, 57, 21}, + {"Random20", 0, 0, 0xad83f25bfdab1b50ull, 0x14650fb0739d0383ull, 0x936a146fbbfb7d4bull, 0xb27d00f43fad75a1ull, 70, 0, 21}, + {"Random20", 1, 1, 0x4b8bcf5ef1f015c5ull, 0x6e2d790aa8a7762bull, 0x57a697ba56dd611eull, 0xd3e60d6fda04adbeull, 72, 57, 21}, + {"Random20", 1, 0, 0x4b8bcf5ef1f015c5ull, 0x14650fb0739d0383ull, 0x57a697ba56dd611eull, 0xd3e60d6fda04adbeull, 72, 0, 21}, + {"Random21", 0, 1, 0xd7b52779988b5cfaull, 0xcfddcd52fb629169ull, 0xbc89178a735678f6ull, 0xfd9b7e4dad3197b5ull, 54, 54, 18}, + {"Random21", 0, 0, 0xd7b52779988b5cfaull, 0x14650fb0739d0383ull, 0xbc89178a735678f6ull, 0xfd9b7e4dad3197b5ull, 54, 0, 18}, + {"Random21", 1, 1, 0x1c515b9e6cd109a3ull, 0xaa94d0f54845ee02ull, 0x0cb28a314aa970aeull, 0x29fa4e20de356d85ull, 54, 54, 18}, + {"Random21", 1, 0, 0x1c515b9e6cd109a3ull, 0x14650fb0739d0383ull, 0x0cb28a314aa970aeull, 0x29fa4e20de356d85ull, 54, 0, 18}, + {"Random22", 0, 1, 0x79adc16250cf8292ull, 0x2512a0afd913906aull, 0xedb01aeb6057c25bull, 0x1d49ae94182c6dc3ull, 68, 49, 13}, + {"Random22", 0, 0, 0x79adc16250cf8292ull, 0x14650fb0739d0383ull, 0xedb01aeb6057c25bull, 0x1d49ae94182c6dc3ull, 68, 0, 13}, + {"Random22", 1, 1, 0x301434ca9762fba0ull, 0x9371e473c135020full, 0x70ac53cf16690a86ull, 0xaadd4e3cb0a37251ull, 68, 49, 13}, + {"Random22", 1, 0, 0x301434ca9762fba0ull, 0x14650fb0739d0383ull, 0x70ac53cf16690a86ull, 0xaadd4e3cb0a37251ull, 68, 0, 13}, + {"Random23", 0, 1, 0xbf93319ca8e8ed61ull, 0x178099cac4dd9abfull, 0xf0e62f804df9e022ull, 0x160121fb8a6ad628ull, 56, 54, 1}, + {"Random23", 0, 0, 0xbf93319ca8e8ed61ull, 0x14650fb0739d0383ull, 0xf0e62f804df9e022ull, 0x160121fb8a6ad628ull, 56, 0, 1}, + {"Random23", 1, 1, 0x03336650fccfc186ull, 0x84fb2988132b6bdbull, 0xf0e62f804df9e022ull, 0x54a3c7db6bc08316ull, 56, 54, 1}, + {"Random23", 1, 0, 0x03336650fccfc186ull, 0x14650fb0739d0383ull, 0xf0e62f804df9e022ull, 0x54a3c7db6bc08316ull, 56, 0, 1}, + {"Random24", 0, 1, 0x21714f0414285779ull, 0x8e07ee97e3c18592ull, 0xcdafd09a1c076093ull, 0x9b4982cc0c9d5836ull, 60, 60, 16}, + {"Random24", 0, 0, 0x21714f0414285779ull, 0x14650fb0739d0383ull, 0xcdafd09a1c076093ull, 0x9b4982cc0c9d5836ull, 60, 0, 16}, + {"Random24", 1, 1, 0x104927385caf1bafull, 0xb58d2d29b9934e0cull, 0xe744bb83904b658aull, 0x892eef5960b6186eull, 60, 60, 16}, + {"Random24", 1, 0, 0x104927385caf1bafull, 0x14650fb0739d0383ull, 0xe744bb83904b658aull, 0x892eef5960b6186eull, 60, 0, 16}, + {"Random25", 0, 1, 0x7fe75851d29b5f6aull, 0xbd64625a7bcfb73cull, 0xf92cddf4fc024c49ull, 0xdbdc12e31962344aull, 44, 44, 8}, + {"Random25", 0, 0, 0x7fe75851d29b5f6aull, 0x14650fb0739d0383ull, 0xf92cddf4fc024c49ull, 0xdbdc12e31962344aull, 44, 0, 8}, + {"Random25", 1, 1, 0x49f7049727341b3cull, 0x4023f4c7efa8cdacull, 0xf92cddf4fc024c49ull, 0x1e3a08e1516ae4beull, 44, 44, 8}, + {"Random25", 1, 0, 0x49f7049727341b3cull, 0x14650fb0739d0383ull, 0xf92cddf4fc024c49ull, 0x1e3a08e1516ae4beull, 44, 0, 8}, + {"Random26", 0, 1, 0xca8049a8b96e9724ull, 0xc7e39193f4b07aa7ull, 0xd9522717c794d53bull, 0xf4e31e8175400963ull, 42, 42, 12}, + {"Random26", 0, 0, 0xca8049a8b96e9724ull, 0x14650fb0739d0383ull, 0xd9522717c794d53bull, 0xf4e31e8175400963ull, 42, 0, 12}, + {"Random26", 1, 1, 0x9c9ac48945130531ull, 0xeb4d985eb31f93e0ull, 0xf7b8acd382701dd1ull, 0xecd913dc1c2bb6edull, 42, 42, 12}, + {"Random26", 1, 0, 0x9c9ac48945130531ull, 0x14650fb0739d0383ull, 0xf7b8acd382701dd1ull, 0xecd913dc1c2bb6edull, 42, 0, 12}, + {"Random27", 0, 1, 0xa5ad84fe83acddfcull, 0x4da695a0a63ae358ull, 0xbfe2f70bda5f466dull, 0x70f7b127c522e392ull, 50, 50, 22}, + {"Random27", 0, 0, 0xa5ad84fe83acddfcull, 0x14650fb0739d0383ull, 0xbfe2f70bda5f466dull, 0x70f7b127c522e392ull, 50, 0, 22}, + {"Random27", 1, 1, 0x67f5b41cf936747aull, 0x06665f5393126836ull, 0x0bb88d26b3dc8cc1ull, 0x94de1904cefde26dull, 50, 50, 21}, + {"Random27", 1, 0, 0x67f5b41cf936747aull, 0x14650fb0739d0383ull, 0x0bb88d26b3dc8cc1ull, 0x94de1904cefde26dull, 50, 0, 21}, + {"Random28", 0, 1, 0x8b9cc3d4febe4c62ull, 0x69471bc0f4097ac1ull, 0x7ee366b7feceed4eull, 0x373d1983261f87caull, 54, 54, 19}, + {"Random28", 0, 0, 0x8b9cc3d4febe4c62ull, 0x14650fb0739d0383ull, 0x7ee366b7feceed4eull, 0x373d1983261f87caull, 54, 0, 19}, + {"Random28", 1, 1, 0x57f3120e39e241ddull, 0x0a99bf96869493a4ull, 0xee17522a5f3c15dfull, 0xe2efc2ce4ee88ff9ull, 54, 54, 19}, + {"Random28", 1, 0, 0x57f3120e39e241ddull, 0x14650fb0739d0383ull, 0xee17522a5f3c15dfull, 0xe2efc2ce4ee88ff9ull, 54, 0, 19}, + {"Random29", 0, 1, 0x89b631d5f5f01691ull, 0x167525b5e459f6a9ull, 0xe6aec4ce1ae7ccd1ull, 0xb42cb126538c6f0aull, 57, 57, 1}, + {"Random29", 0, 0, 0x89b631d5f5f01691ull, 0x14650fb0739d0383ull, 0xe6aec4ce1ae7ccd1ull, 0xb42cb126538c6f0aull, 57, 0, 1}, + {"Random29", 1, 1, 0x41cca15aeaf4b30dull, 0x5c0fe05b50ac7989ull, 0xe6aec4ce1ae7ccd1ull, 0xdaa6c53d3410e42full, 57, 57, 1}, + {"Random29", 1, 0, 0x41cca15aeaf4b30dull, 0x14650fb0739d0383ull, 0xe6aec4ce1ae7ccd1ull, 0xdaa6c53d3410e42full, 57, 0, 1}, + {"Random30", 0, 1, 0xe74142e41965c7f8ull, 0x7d6bfad3ec602d1eull, 0x260809f2d3657490ull, 0x2ff813254130dde5ull, 61, 61, 1}, + {"Random30", 0, 0, 0xe74142e41965c7f8ull, 0x14650fb0739d0383ull, 0x260809f2d3657490ull, 0x2ff813254130dde5ull, 61, 0, 1}, + {"Random30", 1, 1, 0xffbc3c35359e842dull, 0xa978037a0a737ed4ull, 0x260809f2d3657490ull, 0xb713984b2b953de4ull, 61, 61, 1}, + {"Random30", 1, 0, 0xffbc3c35359e842dull, 0x14650fb0739d0383ull, 0x260809f2d3657490ull, 0xb713984b2b953de4ull, 61, 0, 1}, + {"Random31", 0, 1, 0xc1d0985632aa85e5ull, 0xb1ff4f1ff8ce7ff6ull, 0xcad16841aad879ddull, 0x0138efa3affc4452ull, 49, 49, 9}, + {"Random31", 0, 0, 0xc1d0985632aa85e5ull, 0x14650fb0739d0383ull, 0xcad16841aad879ddull, 0x0138efa3affc4452ull, 49, 0, 9}, + {"Random31", 1, 1, 0x29c04e4a3f62fc66ull, 0xd37bee2bcbd5b472ull, 0xc42eba994cb3bc7full, 0x3e283f49a532b649ull, 49, 49, 9}, + {"Random31", 1, 0, 0x29c04e4a3f62fc66ull, 0x14650fb0739d0383ull, 0xc42eba994cb3bc7full, 0x3e283f49a532b649ull, 49, 0, 9}, + {"Random32", 0, 1, 0x69fb37996a1a6d8aull, 0x4eda1ad97257444eull, 0xb5c0211fafc19569ull, 0xf8f4a46581375bc8ull, 62, 62, 18}, + {"Random32", 0, 0, 0x69fb37996a1a6d8aull, 0x14650fb0739d0383ull, 0xb5c0211fafc19569ull, 0xf8f4a46581375bc8ull, 62, 0, 18}, + {"Random32", 1, 1, 0x6aa8d3b227f15c4cull, 0xefb2e786712329f2ull, 0xb1a7a66b620c4fd5ull, 0xe3b7a40e1641948dull, 62, 62, 19}, + {"Random32", 1, 0, 0x6aa8d3b227f15c4cull, 0x14650fb0739d0383ull, 0xb1a7a66b620c4fd5ull, 0xe3b7a40e1641948dull, 62, 0, 19}, + {"Random33", 0, 1, 0x9651f8f876e48213ull, 0x70cd8d3b24d08640ull, 0xb69ec34fc873583aull, 0x495394a49a7d3245ull, 51, 51, 13}, + {"Random33", 0, 0, 0x9651f8f876e48213ull, 0x14650fb0739d0383ull, 0xb69ec34fc873583aull, 0x495394a49a7d3245ull, 51, 0, 13}, + {"Random33", 1, 1, 0x23686cd9a3fe0637ull, 0x17ecf363cd313d8aull, 0xa51cad1078866f00ull, 0xcefd187486fda5beull, 51, 51, 13}, + {"Random33", 1, 0, 0x23686cd9a3fe0637ull, 0x14650fb0739d0383ull, 0xa51cad1078866f00ull, 0xcefd187486fda5beull, 51, 0, 13}, + {"Random34", 0, 1, 0xb0d26d6ec62ee0b2ull, 0xe24b790bed6d8209ull, 0x81535eb933e496e7ull, 0xa8799fc8644a8be8ull, 51, 51, 1}, + {"Random34", 0, 0, 0xb0d26d6ec62ee0b2ull, 0x14650fb0739d0383ull, 0x81535eb933e496e7ull, 0xa8799fc8644a8be8ull, 51, 0, 1}, + {"Random34", 1, 1, 0x40ad548ed9e2d639ull, 0x2d022bb216743a64ull, 0xa788aa75c6b931e3ull, 0x0a4527b569ac8a10ull, 51, 51, 2}, + {"Random34", 1, 0, 0x40ad548ed9e2d639ull, 0x14650fb0739d0383ull, 0xa788aa75c6b931e3ull, 0x0a4527b569ac8a10ull, 51, 0, 2}, + {"Random35", 0, 1, 0xbdcab3fd94434657ull, 0x8f07aebde3f01772ull, 0x834764512474d77cull, 0x4d118037ee1626adull, 63, 63, 24}, + {"Random35", 0, 0, 0xbdcab3fd94434657ull, 0x14650fb0739d0383ull, 0x834764512474d77cull, 0x4d118037ee1626adull, 63, 0, 24}, + {"Random35", 1, 1, 0x9f158f1f03765f97ull, 0x7ea6860a76f2a755ull, 0xce2ab725ccda6ebdull, 0x07820fe3390f4450ull, 63, 63, 24}, + {"Random35", 1, 0, 0x9f158f1f03765f97ull, 0x14650fb0739d0383ull, 0xce2ab725ccda6ebdull, 0x07820fe3390f4450ull, 63, 0, 24}, + {"Random36", 0, 1, 0xc06367cbde7ecc9aull, 0x9c13280fd6c4a76full, 0x71b837070cbf3386ull, 0x4a73a58e1bf4c419ull, 87, 60, 21}, + {"Random36", 0, 0, 0xc06367cbde7ecc9aull, 0x14650fb0739d0383ull, 0x71b837070cbf3386ull, 0x4a73a58e1bf4c419ull, 87, 0, 21}, + {"Random36", 1, 1, 0xf78394d86184426aull, 0xd52d837b120d0443ull, 0xc91ddda9c3e46569ull, 0x1e90f705be3da39full, 87, 60, 21}, + {"Random36", 1, 0, 0xf78394d86184426aull, 0x14650fb0739d0383ull, 0xc91ddda9c3e46569ull, 0x1e90f705be3da39full, 87, 0, 21}, + {"Random37", 0, 1, 0x8af4f686e8afd249ull, 0xb43a05dbbc636b7aull, 0x52bf39ca2f2765c6ull, 0x8692adfc1cddb1c4ull, 71, 45, 18}, + {"Random37", 0, 0, 0x8af4f686e8afd249ull, 0x14650fb0739d0383ull, 0x52bf39ca2f2765c6ull, 0x8692adfc1cddb1c4ull, 71, 0, 18}, + {"Random37", 1, 1, 0xe82a3b0bd1861474ull, 0x5ea014024a78fb97ull, 0x41de938f8a060981ull, 0x1ace37e580b56141ull, 70, 45, 17}, + {"Random37", 1, 0, 0xe82a3b0bd1861474ull, 0x14650fb0739d0383ull, 0x41de938f8a060981ull, 0x1ace37e580b56141ull, 70, 0, 17}, + {"Random38", 0, 1, 0xeaf05ca1aedc03d5ull, 0x80971876a1156ee8ull, 0xac12ee6aa53f424cull, 0x2d7719753c10f4bbull, 54, 54, 25}, + {"Random38", 0, 0, 0xeaf05ca1aedc03d5ull, 0x14650fb0739d0383ull, 0xac12ee6aa53f424cull, 0x2d7719753c10f4bbull, 54, 0, 25}, + {"Random38", 1, 1, 0x8285a035265d771cull, 0x0d3b927724077789ull, 0xc6a0970cf0716d7cull, 0x09336a0556a2fbb7ull, 54, 54, 25}, + {"Random38", 1, 0, 0x8285a035265d771cull, 0x14650fb0739d0383ull, 0xc6a0970cf0716d7cull, 0x09336a0556a2fbb7ull, 54, 0, 25}, + {"Random39", 0, 1, 0xc989defa2d8cff00ull, 0x31db41d49a36aaa6ull, 0xc193386eb35a8e65ull, 0xb8c88da9531702d9ull, 44, 44, 19}, + {"Random39", 0, 0, 0xc989defa2d8cff00ull, 0x14650fb0739d0383ull, 0xc193386eb35a8e65ull, 0xb8c88da9531702d9ull, 44, 0, 19}, + {"Random39", 1, 1, 0x588cc94ab848f7e6ull, 0x3b1478373127afc0ull, 0xeea051f0749b0428ull, 0xbdab759ad2c5865cull, 44, 44, 19}, + {"Random39", 1, 0, 0x588cc94ab848f7e6ull, 0x14650fb0739d0383ull, 0xeea051f0749b0428ull, 0xbdab759ad2c5865cull, 44, 0, 19}, + {"Random40", 0, 1, 0xc4f976129a95a285ull, 0x389162a99c89a49cull, 0x78c15a2f88a24807ull, 0xcafbb488b92f9edaull, 77, 53, 18}, + {"Random40", 0, 0, 0xc4f976129a95a285ull, 0x14650fb0739d0383ull, 0x78c15a2f88a24807ull, 0xcafbb488b92f9edaull, 77, 0, 18}, + {"Random40", 1, 1, 0x1cc6041a6a6fcdcdull, 0x33a043a5f0e45941ull, 0x41975079a7cc855bull, 0x798fe8d7596aaa9cull, 78, 53, 19}, + {"Random40", 1, 0, 0x1cc6041a6a6fcdcdull, 0x14650fb0739d0383ull, 0x41975079a7cc855bull, 0x798fe8d7596aaa9cull, 78, 0, 19}, + {"Random41", 0, 1, 0xb4b89e111611f4ceull, 0xea3d6d0c77bc7e76ull, 0xf6e95f1e1a0264bcull, 0x980f8a18b3d80a23ull, 51, 40, 6}, + {"Random41", 0, 0, 0xb4b89e111611f4ceull, 0x14650fb0739d0383ull, 0xf6e95f1e1a0264bcull, 0x980f8a18b3d80a23ull, 51, 0, 6}, + {"Random41", 1, 1, 0xf15d27dad1575f3bull, 0x9245b7b247af4cc1ull, 0xf6e95f1e1a0264bcull, 0xc730d99532f512d8ull, 51, 40, 6}, + {"Random41", 1, 0, 0xf15d27dad1575f3bull, 0x14650fb0739d0383ull, 0xf6e95f1e1a0264bcull, 0xc730d99532f512d8ull, 51, 0, 6}, + {"Random42", 0, 1, 0x0da9b26d3847ab1full, 0x9c9329bd9adc3676ull, 0xd4d5dd88f1bc7129ull, 0x49a0634035589083ull, 60, 60, 12}, + {"Random42", 0, 0, 0x0da9b26d3847ab1full, 0x14650fb0739d0383ull, 0xd4d5dd88f1bc7129ull, 0x49a0634035589083ull, 60, 0, 12}, + {"Random42", 1, 1, 0xbd61639c42be9cb3ull, 0x5d2fe8dd7d35b222ull, 0x292b9553961fa163ull, 0xa80c78dbeddc4468ull, 60, 60, 12}, + {"Random42", 1, 0, 0xbd61639c42be9cb3ull, 0x14650fb0739d0383ull, 0x292b9553961fa163ull, 0xa80c78dbeddc4468ull, 60, 0, 12}, + {"Random43", 0, 1, 0xc521512ba92a54c9ull, 0x5993bd8ceee91044ull, 0xbd8ba613082e0da5ull, 0x5fbe995d73a8bff0ull, 63, 63, 14}, + {"Random43", 0, 0, 0xc521512ba92a54c9ull, 0x14650fb0739d0383ull, 0xbd8ba613082e0da5ull, 0x5fbe995d73a8bff0ull, 63, 0, 14}, + {"Random43", 1, 1, 0xa9607e56c7e4b193ull, 0x941becccb8fe75ceull, 0x572d3f9f9521e7caull, 0xfdc00d0d1ff0cbfbull, 63, 63, 14}, + {"Random43", 1, 0, 0xa9607e56c7e4b193ull, 0x14650fb0739d0383ull, 0x572d3f9f9521e7caull, 0xfdc00d0d1ff0cbfbull, 63, 0, 14}, + {"Random44", 0, 1, 0x971e7c4a38e301ddull, 0x55249f0ae51722feull, 0x316199af1a335dbfull, 0x5a13570337a638b5ull, 69, 47, 15}, + {"Random44", 0, 0, 0x971e7c4a38e301ddull, 0x14650fb0739d0383ull, 0x316199af1a335dbfull, 0x5a13570337a638b5ull, 69, 0, 15}, + {"Random44", 1, 1, 0x3ee064c4f943e93aull, 0xbc62822019ded427ull, 0x9f0e22f8fb2632f6ull, 0xc18096028ba7be64ull, 69, 47, 15}, + {"Random44", 1, 0, 0x3ee064c4f943e93aull, 0x14650fb0739d0383ull, 0x9f0e22f8fb2632f6ull, 0xc18096028ba7be64ull, 69, 0, 15}, + {"Random45", 0, 1, 0xa155b80e37d41cf2ull, 0xbacd32f5d8b94030ull, 0xf7b0f07cda17c110ull, 0x598c179a2d5c735cull, 99, 63, 25}, + {"Random45", 0, 0, 0xa155b80e37d41cf2ull, 0x14650fb0739d0383ull, 0xf7b0f07cda17c110ull, 0x598c179a2d5c735cull, 99, 0, 25}, + {"Random45", 1, 1, 0x9644e4dd1f925cdfull, 0x2cc38b54d184cf0aull, 0x797aff1ec9239dffull, 0x290739687f68f174ull, 99, 63, 24}, + {"Random45", 1, 0, 0x9644e4dd1f925cdfull, 0x14650fb0739d0383ull, 0x797aff1ec9239dffull, 0x290739687f68f174ull, 99, 0, 24}, + {"Random46", 0, 1, 0xa74f65268d41a1bbull, 0xfc4e6864d98d62c0ull, 0xfaba8060a07d20fdull, 0x7097cd184da8a4dcull, 52, 52, 1}, + {"Random46", 0, 0, 0xa74f65268d41a1bbull, 0x14650fb0739d0383ull, 0xfaba8060a07d20fdull, 0x7097cd184da8a4dcull, 52, 0, 1}, + {"Random46", 1, 1, 0xb1406e1f21710782ull, 0xe0b910f752cbc3adull, 0xfaba8060a07d20fdull, 0x2539296694529005ull, 52, 52, 1}, + {"Random46", 1, 0, 0xb1406e1f21710782ull, 0x14650fb0739d0383ull, 0xfaba8060a07d20fdull, 0x2539296694529005ull, 52, 0, 1}, + {"Random47", 0, 1, 0x214dc1308457a01aull, 0xe293c7196f5c9f95ull, 0x072cedccf12d6a31ull, 0x8176d644a621a19eull, 57, 57, 13}, + {"Random47", 0, 0, 0x214dc1308457a01aull, 0x14650fb0739d0383ull, 0x072cedccf12d6a31ull, 0x8176d644a621a19eull, 57, 0, 13}, + {"Random47", 1, 1, 0xfe949f5ba003397dull, 0xe112a9940c547399ull, 0x33811b67f07874f3ull, 0x9ee620e1bf252639ull, 57, 57, 13}, + {"Random47", 1, 0, 0xfe949f5ba003397dull, 0x14650fb0739d0383ull, 0x33811b67f07874f3ull, 0x9ee620e1bf252639ull, 57, 0, 13}, + {"Random48", 0, 1, 0x21c0bf9ae910d3acull, 0xe2d98f0ffb5c1b96ull, 0xda74ceb1a9e7d904ull, 0x1e560afd06471537ull, 48, 48, 8}, + {"Random48", 0, 0, 0x21c0bf9ae910d3acull, 0x14650fb0739d0383ull, 0xda74ceb1a9e7d904ull, 0x1e560afd06471537ull, 48, 0, 8}, + {"Random48", 1, 1, 0x4fd90c8ca1025428ull, 0x7706d5e9ebfd9226ull, 0x0876fbb01d747e2dull, 0xee01efa6d7b40515ull, 48, 48, 8}, + {"Random48", 1, 0, 0x4fd90c8ca1025428ull, 0x14650fb0739d0383ull, 0x0876fbb01d747e2dull, 0xee01efa6d7b40515ull, 48, 0, 8}, }; diff --git a/tests/test_adapter_receipts_in_place.cpp b/tests/test_adapter_receipts_in_place.cpp index a93bacda..08fe8388 100644 --- a/tests/test_adapter_receipts_in_place.cpp +++ b/tests/test_adapter_receipts_in_place.cpp @@ -410,10 +410,21 @@ void emit(Scenario scenario, bool magnifier, const Observed& got) { // 15813806343021779355ull -> 10876124900683281275ull // 3478712054363587307ull -> 10020927749345911019ull // 8064893742935713930ull -> 8691527508212156618ull +// expectation corrected (INT26 v19 hash re-pin, kDeclined_rows, 10 of 10 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 16367413721785097631ull -> 10078075251929413260ull [H-THIN] +// 8673411908040166317ull -> 9021013497480718974ull [H-THIN] +// 711509299982175852ull -> 5211498519342463915ull [H-THIN] +// 7197939734931235205ull -> 8693678377712780894ull [H-THIN] +// 7029670217763137463ull -> 3263497715304832018ull [H-THIN] +// 3635687300847380421ull -> 4887532512140189270ull [H-THIN] +// 16534761561291369739ull -> 3474228148466484630ull [H-THIN] +// 10876124900683281275ull -> 8939974732258322358ull [H-THIN] +// 10020927749345911019ull -> 2229197510643109192ull [H-THIN] +// 8691527508212156618ull -> 5373392566564228697ull [H-THIN] constexpr std::uint64_t kDeclined_rows[] = { - 16367413721785097631ull, 8673411908040166317ull, 711509299982175852ull, 7197939734931235205ull, - 7029670217763137463ull, 3635687300847380421ull, 16534761561291369739ull, 10876124900683281275ull, - 10020927749345911019ull, 8691527508212156618ull, + 10078075251929413260ull, 9021013497480718974ull, 5211498519342463915ull, 8693678377712780894ull, + 3263497715304832018ull, 4887532512140189270ull, 3474228148466484630ull, 8939974732258322358ull, + 2229197510643109192ull, 5373392566564228697ull, }; constexpr std::uint64_t kDeclined_at_bar[] = { 4ull, 0ull, 12ull, 9ull, @@ -439,7 +450,8 @@ constexpr std::uint64_t kDeclined_folded[] = { // expectation corrected (v19-E): kDeclined_final 1945621519056434088ull -> 9684931586165891051ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (v19-D): kDeclined_final 9684931586165891051ull -> 8064893742935713930ull, because v19-D keeps fewer retired rows and folds a bracket family's erased members behind a retained one as runs (pineforge-source-adapter/v4: K1 releases a current-cycle leg the revival's superseded test answers for; BracketRoster parks); the receipt bars, folded counts and trades did not move; harvested with the TU's own switch on this tree (r5/v19-d). // expectation corrected (V19-FIX): kDeclined_final 8064893742935713930ull -> 8691527508212156618ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kDeclined_final = 8691527508212156618ull; +// expectation corrected (INT26 v19 hash re-pin): kDeclined_final 8691527508212156618ull -> 5373392566564228697ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kDeclined_final = 5373392566564228697ull; constexpr Trade kDeclined_trades[] = { {1736121660000LL, 1736121960000LL, 100, 95, 100, 0}, }; @@ -482,10 +494,21 @@ constexpr Trade kDeclined_trades[] = { // 11561237993722196187ull -> 1773947867498079483ull // 8752407783783115299ull -> 15294623478765439011ull // 13196624524101047406ull -> 483092226467101870ull +// expectation corrected (INT26 v19 hash re-pin, kDeclinedMag_rows, 10 of 10 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 17043975752116642733ull -> 16782433437025871990ull [H-THIN] +// 10606185829452660874ull -> 1222497984017113931ull [H-THIN] +// 15448675564357825832ull -> 13459087267571170739ull [H-THIN] +// 15175044830743125885ull -> 9516889388168741832ull [H-THIN] +// 6450443484594143151ull -> 7518654345809068380ull [H-THIN] +// 2398923910778006973ull -> 11893247087917015032ull [H-THIN] +// 8459031541244945398ull -> 12055886405220663097ull [H-THIN] +// 1773947867498079483ull -> 10055241910018728642ull [H-THIN] +// 15294623478765439011ull -> 12275394792888174860ull [H-THIN] +// 483092226467101870ull -> 9368999989622318001ull [H-THIN] constexpr std::uint64_t kDeclinedMag_rows[] = { - 17043975752116642733ull, 10606185829452660874ull, 15448675564357825832ull, 15175044830743125885ull, - 6450443484594143151ull, 2398923910778006973ull, 8459031541244945398ull, 1773947867498079483ull, - 15294623478765439011ull, 483092226467101870ull, + 16782433437025871990ull, 1222497984017113931ull, 13459087267571170739ull, 9516889388168741832ull, + 7518654345809068380ull, 11893247087917015032ull, 12055886405220663097ull, 10055241910018728642ull, + 12275394792888174860ull, 9368999989622318001ull, }; constexpr std::uint64_t kDeclinedMag_at_bar[] = { 4ull, 0ull, 12ull, 0ull, @@ -511,7 +534,8 @@ constexpr std::uint64_t kDeclinedMag_folded[] = { // expectation corrected (v19-E): kDeclinedMag_final 6442860896266831741ull -> 13182614947071830947ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (v19-D): kDeclinedMag_final 13182614947071830947ull -> 13196624524101047406ull, because v19-D keeps fewer retired rows and folds a bracket family's erased members behind a retained one as runs (pineforge-source-adapter/v4: K1 releases a current-cycle leg the revival's superseded test answers for; BracketRoster parks); the receipt bars, folded counts and trades did not move; harvested with the TU's own switch on this tree (r5/v19-d). // expectation corrected (V19-FIX): kDeclinedMag_final 13196624524101047406ull -> 483092226467101870ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kDeclinedMag_final = 483092226467101870ull; +// expectation corrected (INT26 v19 hash re-pin): kDeclinedMag_final 483092226467101870ull -> 9368999989622318001ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kDeclinedMag_final = 9368999989622318001ull; constexpr Trade kDeclinedMag_trades[] = { {1736121660000LL, 1736121960000LL, 100, 95, 100, 0}, }; @@ -540,9 +564,17 @@ constexpr Trade kDeclinedMag_trades[] = { // 8695591448261818412ull -> 8604888200074309324ull // 15937329709211664256ull -> 10980118139031631968ull // 10634995016395674704ull -> 17030525150602589392ull +// expectation corrected (INT26 v19 hash re-pin, kRevive_rows, 7 of 7 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 13340112391979823686ull -> 13853830474015623389ull [H-THIN] +// 3080039388426713206ull -> 4728749920725358539ull [H-THIN] +// 7902869859755540775ull -> 4849688479110990770ull [H-THIN] +// 18430037521897822255ull -> 16929648573929730497ull [H-THIN] +// 8604888200074309324ull -> 1041650372773372953ull [H-THIN] +// 10980118139031631968ull -> 5462196043591698097ull [H-THIN] +// 17030525150602589392ull -> 2747991239711285631ull [H-THIN] constexpr std::uint64_t kRevive_rows[] = { - 13340112391979823686ull, 3080039388426713206ull, 7902869859755540775ull, 18430037521897822255ull, - 8604888200074309324ull, 10980118139031631968ull, 17030525150602589392ull, + 13853830474015623389ull, 4728749920725358539ull, 4849688479110990770ull, 16929648573929730497ull, + 1041650372773372953ull, 5462196043591698097ull, 2747991239711285631ull, }; constexpr std::uint64_t kRevive_at_bar[] = { 4ull, 0ull, 15ull, 10ull, @@ -563,7 +595,8 @@ constexpr std::uint64_t kRevive_folded[] = { // expectation corrected: kRevive_final 3480095526769689953ull -> 18153826259251315829ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); receipt cursors and trades did not move. // expectation corrected (v19-E): kRevive_final 18153826259251315829ull -> 10634995016395674704ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kRevive_final 10634995016395674704ull -> 17030525150602589392ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kRevive_final = 17030525150602589392ull; +// expectation corrected (INT26 v19 hash re-pin): kRevive_final 17030525150602589392ull -> 2747991239711285631ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kRevive_final = 2747991239711285631ull; constexpr Trade kRevive_trades[] = { {1736121660000LL, 1736121780000LL, 100, 170, 47.058823529411768, 0}, {1736121660000LL, 1736121840000LL, 100, 180, 52.941176470588232, 0}, @@ -593,9 +626,17 @@ constexpr Trade kRevive_trades[] = { // 9719574618747977040ull -> 6288134072033682736ull // 16705937982676897010ull -> 5799450267550970898ull // 403749858405626294ull -> 16015312550992593014ull +// expectation corrected (INT26 v19 hash re-pin, kReviveMag_rows, 7 of 7 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 9323387230543354105ull -> 832728159842300632ull [H-THIN] +// 17230544106411439877ull -> 10054388176425177056ull [H-THIN] +// 8558481171000670347ull -> 18222497838575167482ull [H-THIN] +// 11778096795536835427ull -> 7009173660391284590ull [H-THIN] +// 6288134072033682736ull -> 13277765918702967861ull [H-THIN] +// 5799450267550970898ull -> 6034130187223021955ull [H-THIN] +// 16015312550992593014ull -> 9402276759429607197ull [H-THIN] constexpr std::uint64_t kReviveMag_rows[] = { - 9323387230543354105ull, 17230544106411439877ull, 8558481171000670347ull, 11778096795536835427ull, - 6288134072033682736ull, 5799450267550970898ull, 16015312550992593014ull, + 832728159842300632ull, 10054388176425177056ull, 18222497838575167482ull, 7009173660391284590ull, + 13277765918702967861ull, 6034130187223021955ull, 9402276759429607197ull, }; constexpr std::uint64_t kReviveMag_at_bar[] = { 4ull, 0ull, 15ull, 0ull, @@ -616,7 +657,8 @@ constexpr std::uint64_t kReviveMag_folded[] = { // expectation corrected: kReviveMag_final 11170248427254383026ull -> 11245820422146263909ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); receipt cursors and trades did not move. // expectation corrected (v19-E): kReviveMag_final 11245820422146263909ull -> 403749858405626294ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kReviveMag_final 403749858405626294ull -> 16015312550992593014ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kReviveMag_final = 16015312550992593014ull; +// expectation corrected (INT26 v19 hash re-pin): kReviveMag_final 16015312550992593014ull -> 9402276759429607197ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kReviveMag_final = 9402276759429607197ull; constexpr Trade kReviveMag_trades[] = { {1736121660000LL, 1736121840000LL, 100, 175, 100, 0}, }; @@ -645,9 +687,17 @@ constexpr Trade kReviveMag_trades[] = { // 10109285908224112591ull -> 1236032771344307983ull // 10060812652362367792ull -> 17545089905645669552ull // 12854015805238324476ull -> 11022588141404321180ull +// expectation corrected (INT26 v19 hash re-pin, kCascade_rows, 7 of 7 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 551096499902803590ull -> 11665685740161685773ull [H-THIN] +// 6515737401085179265ull -> 6762973723098471536ull [H-THIN] +// 13117702113570278612ull -> 13194180830256499995ull [H-THIN] +// 1257083709566184300ull -> 5915515851064168701ull [H-THIN] +// 1236032771344307983ull -> 14468652264495611008ull [H-THIN] +// 17545089905645669552ull -> 4582123328933630207ull [H-THIN] +// 11022588141404321180ull -> 16894420136553841491ull [H-THIN] constexpr std::uint64_t kCascade_rows[] = { - 551096499902803590ull, 6515737401085179265ull, 13117702113570278612ull, 1257083709566184300ull, - 1236032771344307983ull, 17545089905645669552ull, 11022588141404321180ull, + 11665685740161685773ull, 6762973723098471536ull, 13194180830256499995ull, 5915515851064168701ull, + 14468652264495611008ull, 4582123328933630207ull, 16894420136553841491ull, }; constexpr std::uint64_t kCascade_at_bar[] = { 4ull, 0ull, 15ull, 10ull, @@ -668,7 +718,8 @@ constexpr std::uint64_t kCascade_folded[] = { // expectation corrected: kCascade_final 17244746764465919736ull -> 5566037634063544638ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); receipt cursors and trades did not move. // expectation corrected (v19-E): kCascade_final 5566037634063544638ull -> 12854015805238324476ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kCascade_final 12854015805238324476ull -> 11022588141404321180ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kCascade_final = 11022588141404321180ull; +// expectation corrected (INT26 v19 hash re-pin): kCascade_final 11022588141404321180ull -> 16894420136553841491ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kCascade_final = 16894420136553841491ull; constexpr Trade kCascade_trades[] = { {1736121660000LL, 1736121780000LL, 100, 170, 47.058823529411768, 0}, {1736121660000LL, 1736121780000LL, 100, 170, 52.941176470588232, 0}, @@ -698,9 +749,17 @@ constexpr Trade kCascade_trades[] = { // 2820795946605591416ull -> 16758361987609330968ull // 8320281576492738244ull -> 4624738216223074340ull // 8969517760469763972ull -> 2074679355477699332ull +// expectation corrected (INT26 v19 hash re-pin, kCascadeMag_rows, 7 of 7 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 15338041165786710953ull -> 10293226467739870528ull [H-THIN] +// 9326370104822563476ull -> 15440279261765505431ull [H-THIN] +// 2546778736934085057ull -> 3033096808895778346ull [H-THIN] +// 15492970885942622778ull -> 8603410363956848503ull [H-THIN] +// 16758361987609330968ull -> 3716382050228218553ull [H-THIN] +// 4624738216223074340ull -> 15049237438778114557ull [H-THIN] +// 2074679355477699332ull -> 3591493413416270605ull [H-THIN] constexpr std::uint64_t kCascadeMag_rows[] = { - 15338041165786710953ull, 9326370104822563476ull, 2546778736934085057ull, 15492970885942622778ull, - 16758361987609330968ull, 4624738216223074340ull, 2074679355477699332ull, + 10293226467739870528ull, 15440279261765505431ull, 3033096808895778346ull, 8603410363956848503ull, + 3716382050228218553ull, 15049237438778114557ull, 3591493413416270605ull, }; constexpr std::uint64_t kCascadeMag_at_bar[] = { 4ull, 0ull, 15ull, 0ull, @@ -721,7 +780,8 @@ constexpr std::uint64_t kCascadeMag_folded[] = { // expectation corrected: kCascadeMag_final 12442414818770313178ull -> 8438336155355340534ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); receipt cursors and trades did not move. // expectation corrected (v19-E): kCascadeMag_final 8438336155355340534ull -> 8969517760469763972ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kCascadeMag_final 8969517760469763972ull -> 2074679355477699332ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kCascadeMag_final = 2074679355477699332ull; +// expectation corrected (INT26 v19 hash re-pin): kCascadeMag_final 2074679355477699332ull -> 3591493413416270605ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kCascadeMag_final = 3591493413416270605ull; constexpr Trade kCascadeMag_trades[] = { {1736121660000LL, 1736121840000LL, 100, 168, 19.047619047619047, 0}, {1736121660000LL, 1736121840000LL, 100, 168, 80.952380952380949, 0}, @@ -879,17 +939,58 @@ constexpr Trade kCascadeMag_trades[] = { // 6753420119756311418ull -> 3654409746117594394ull // 1593360149240348528ull -> 13081935113382965168ull // 13709538166202381782ull -> 10127071882762265462ull +// expectation corrected (INT26 v19 hash re-pin, kBrackets_rows, 40 of 40 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 16217277159719941353ull -> 7500883169712558290ull [H-THIN] +// 8338318974496761540ull -> 11727648583129089083ull [H-THIN] +// 4387288109901666172ull -> 6413647876750793739ull [H-THIN] +// 17194843007024741926ull -> 16308656194518151033ull [H-THIN] +// 13764914223976981334ull -> 16727135511009534069ull [H-THIN] +// 5165427215729956474ull -> 6044943424043481425ull [H-THIN] +// 4634967915219578727ull -> 14841136291072554584ull [H-THIN] +// 16692219291368299752ull -> 1773096913540911661ull [H-THIN] +// 14748223541663733754ull -> 17033478288745263781ull [H-THIN] +// 3456524395963272453ull -> 17415237666140008114ull [H-THIN] +// 12434621022045497961ull -> 14903371499312557794ull [H-THIN] +// 16335369385158554819ull -> 1559388622250313888ull [H-THIN] +// 12991044494042544886ull -> 11339295434477982165ull [H-THIN] +// 4973527342518025567ull -> 5188816558658073040ull [H-THIN] +// 17883762509965744260ull -> 10562336006974642565ull [H-THIN] +// 2061839802394366722ull -> 9944319438386404069ull [H-THIN] +// 9666386366910181655ull -> 18087204908683637838ull [H-THIN] +// 4436881865564527894ull -> 6643491299232430087ull [H-THIN] +// 16715420632714991716ull -> 6148826652091198175ull [H-THIN] +// 1529353304450412038ull -> 4758323598983849211ull [H-THIN] +// 1035717575522872886ull -> 2642662310786859897ull [H-THIN] +// 16225013858806894718ull -> 595312524362424357ull [H-THIN] +// 8409169559076599494ull -> 14890548070825444249ull [H-THIN] +// 9707763992756481156ull -> 3533069104010016939ull [H-THIN] +// 2831826005880704344ull -> 1617991145468681997ull [H-THIN] +// 17431835404196442687ull -> 10622401576246976900ull [H-THIN] +// 9788230484951507538ull -> 8817393790173822955ull [H-THIN] +// 13167836072962096695ull -> 16364625047298717046ull [H-THIN] +// 9063096009681232646ull -> 14563367105470732551ull [H-THIN] +// 5410108115990088215ull -> 4971637099003501106ull [H-THIN] +// 13040426354353828713ull -> 7489452283312605238ull [H-THIN] +// 14801076939233187415ull -> 12683946654012290214ull [H-THIN] +// 16815914518686289691ull -> 17971398269388980488ull [H-THIN] +// 2747558237080166725ull -> 16748341084474510752ull [H-THIN] +// 8660899489027983548ull -> 824168580839598771ull [H-THIN] +// 17102303971583026849ull -> 17966172105988324718ull [H-THIN] +// 15533897202822525630ull -> 12921910347520578453ull [H-THIN] +// 3654409746117594394ull -> 10997125561879301907ull [H-THIN] +// 13081935113382965168ull -> 6586432521644155739ull [H-THIN] +// 10127071882762265462ull -> 5868434502871598073ull [H-THIN] constexpr std::uint64_t kBrackets_rows[] = { - 16217277159719941353ull, 8338318974496761540ull, 4387288109901666172ull, 17194843007024741926ull, - 13764914223976981334ull, 5165427215729956474ull, 4634967915219578727ull, 16692219291368299752ull, - 14748223541663733754ull, 3456524395963272453ull, 12434621022045497961ull, 16335369385158554819ull, - 12991044494042544886ull, 4973527342518025567ull, 17883762509965744260ull, 2061839802394366722ull, - 9666386366910181655ull, 4436881865564527894ull, 16715420632714991716ull, 1529353304450412038ull, - 1035717575522872886ull, 16225013858806894718ull, 8409169559076599494ull, 9707763992756481156ull, - 2831826005880704344ull, 17431835404196442687ull, 9788230484951507538ull, 13167836072962096695ull, - 9063096009681232646ull, 5410108115990088215ull, 13040426354353828713ull, 14801076939233187415ull, - 16815914518686289691ull, 2747558237080166725ull, 8660899489027983548ull, 17102303971583026849ull, - 15533897202822525630ull, 3654409746117594394ull, 13081935113382965168ull, 10127071882762265462ull, + 7500883169712558290ull, 11727648583129089083ull, 6413647876750793739ull, 16308656194518151033ull, + 16727135511009534069ull, 6044943424043481425ull, 14841136291072554584ull, 1773096913540911661ull, + 17033478288745263781ull, 17415237666140008114ull, 14903371499312557794ull, 1559388622250313888ull, + 11339295434477982165ull, 5188816558658073040ull, 10562336006974642565ull, 9944319438386404069ull, + 18087204908683637838ull, 6643491299232430087ull, 6148826652091198175ull, 4758323598983849211ull, + 2642662310786859897ull, 595312524362424357ull, 14890548070825444249ull, 3533069104010016939ull, + 1617991145468681997ull, 10622401576246976900ull, 8817393790173822955ull, 16364625047298717046ull, + 14563367105470732551ull, 4971637099003501106ull, 7489452283312605238ull, 12683946654012290214ull, + 17971398269388980488ull, 16748341084474510752ull, 824168580839598771ull, 17966172105988324718ull, + 12921910347520578453ull, 10997125561879301907ull, 6586432521644155739ull, 5868434502871598073ull, }; constexpr std::uint64_t kBrackets_at_bar[] = { 4ull, 0ull, 25ull, 20ull, @@ -960,7 +1061,8 @@ constexpr std::uint64_t kBrackets_folded[] = { // expectation corrected (v19-E): kBrackets_final 15408496303561531855ull -> 7215366721055711217ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (v19-D): kBrackets_final 7215366721055711217ull -> 13709538166202381782ull, because v19-D keeps fewer retired rows and folds a bracket family's erased members behind a retained one as runs (pineforge-source-adapter/v4: K1 releases a current-cycle leg the revival's superseded test answers for; BracketRoster parks); the receipt bars, folded counts and trades did not move; harvested with the TU's own switch on this tree (r5/v19-d). // expectation corrected (V19-FIX): kBrackets_final 13709538166202381782ull -> 10127071882762265462ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kBrackets_final = 10127071882762265462ull; +// expectation corrected (INT26 v19 hash re-pin): kBrackets_final 10127071882762265462ull -> 5868434502871598073ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kBrackets_final = 5868434502871598073ull; constexpr Trade kBrackets_trades[] = { {1736121660000LL, 1736121660000LL, 100.75, 101, 2, 0}, {1736122020000LL, 1736122260000LL, 100.75, 102, 2, 0}, @@ -1121,17 +1223,58 @@ constexpr Trade kBrackets_trades[] = { // 17217996485244538122ull -> 6026087689782884746ull // 16847584222090599594ull -> 7992738227861920074ull // 17233175003301036440ull -> 2507370523932097816ull +// expectation corrected (INT26 v19 hash re-pin, kBracketsMag_rows, 40 of 40 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 16815971932027180960ull -> 7509436481287183131ull [H-THIN] +// 8203166635201027950ull -> 9787443206195299241ull [H-THIN] +// 17138741981140426534ull -> 9468051353845057081ull [H-THIN] +// 1925655849137931056ull -> 11617996857175938359ull [H-THIN] +// 14052428529967425108ull -> 17297388508275178983ull [H-THIN] +// 9832333236513193860ull -> 8305808312144255951ull [H-THIN] +// 16191684320998506441ull -> 6113367866531691422ull [H-THIN] +// 8371860663058915477ull -> 9106991683344179872ull [H-THIN] +// 8552593071486288536ull -> 14803910782973314363ull [H-THIN] +// 1454522877014641667ull -> 8372038073428945080ull [H-THIN] +// 2707471427108666620ull -> 7616171107040094915ull [H-THIN] +// 16372300294283464170ull -> 10072793551676710573ull [H-THIN] +// 13159511447411425168ull -> 202129580709274867ull [H-THIN] +// 7564232112284716309ull -> 4132398015409106530ull [H-THIN] +// 1543613811599180298ull -> 4510478381903057991ull [H-THIN] +// 15537720168749660922ull -> 8742602620032897417ull [H-THIN] +// 1622379678095332862ull -> 9123990550757961143ull [H-THIN] +// 12005994129115025487ull -> 11712877018079407950ull [H-THIN] +// 5070356265405608919ull -> 2366334284611481108ull [H-THIN] +// 13665825986379254999ull -> 13240125918553875754ull [H-THIN] +// 5146373771364769642ull -> 9789183794260015895ull [H-THIN] +// 6239145605357339126ull -> 14154778537780493947ull [H-THIN] +// 3824110391792374830ull -> 17674589416348533163ull [H-THIN] +// 6558902854431182828ull -> 12580566125972638241ull [H-THIN] +// 8689860431737549825ull -> 12408515542794035006ull [H-THIN] +// 7395806610126361265ull -> 12236372317329550998ull [H-THIN] +// 4003508704365535789ull -> 18175585318787098632ull [H-THIN] +// 15338849660478731120ull -> 10989358971982145541ull [H-THIN] +// 12122897657346658873ull -> 12151142627304375048ull [H-THIN] +// 14082864916763771424ull -> 7079152462663640793ull [H-THIN] +// 4695176159045771576ull -> 9430854713541920393ull [H-THIN] +// 11640860124898684186ull -> 10120897727742141847ull [H-THIN] +// 7934415857533705714ull -> 6015137363156052521ull [H-THIN] +// 2180712887517991172ull -> 9143335679527440329ull [H-THIN] +// 13696242844024611290ull -> 1965209665081157387ull [H-THIN] +// 4388532892304967463ull -> 16559717930979642786ull [H-THIN] +// 886913764131836035ull -> 6332179897502099272ull [H-THIN] +// 6026087689782884746ull -> 1448475308516474107ull [H-THIN] +// 7992738227861920074ull -> 7560963980189861213ull [H-THIN] +// 2507370523932097816ull -> 16163234093112718807ull [H-THIN] constexpr std::uint64_t kBracketsMag_rows[] = { - 16815971932027180960ull, 8203166635201027950ull, 17138741981140426534ull, 1925655849137931056ull, - 14052428529967425108ull, 9832333236513193860ull, 16191684320998506441ull, 8371860663058915477ull, - 8552593071486288536ull, 1454522877014641667ull, 2707471427108666620ull, 16372300294283464170ull, - 13159511447411425168ull, 7564232112284716309ull, 1543613811599180298ull, 15537720168749660922ull, - 1622379678095332862ull, 12005994129115025487ull, 5070356265405608919ull, 13665825986379254999ull, - 5146373771364769642ull, 6239145605357339126ull, 3824110391792374830ull, 6558902854431182828ull, - 8689860431737549825ull, 7395806610126361265ull, 4003508704365535789ull, 15338849660478731120ull, - 12122897657346658873ull, 14082864916763771424ull, 4695176159045771576ull, 11640860124898684186ull, - 7934415857533705714ull, 2180712887517991172ull, 13696242844024611290ull, 4388532892304967463ull, - 886913764131836035ull, 6026087689782884746ull, 7992738227861920074ull, 2507370523932097816ull, + 7509436481287183131ull, 9787443206195299241ull, 9468051353845057081ull, 11617996857175938359ull, + 17297388508275178983ull, 8305808312144255951ull, 6113367866531691422ull, 9106991683344179872ull, + 14803910782973314363ull, 8372038073428945080ull, 7616171107040094915ull, 10072793551676710573ull, + 202129580709274867ull, 4132398015409106530ull, 4510478381903057991ull, 8742602620032897417ull, + 9123990550757961143ull, 11712877018079407950ull, 2366334284611481108ull, 13240125918553875754ull, + 9789183794260015895ull, 14154778537780493947ull, 17674589416348533163ull, 12580566125972638241ull, + 12408515542794035006ull, 12236372317329550998ull, 18175585318787098632ull, 10989358971982145541ull, + 12151142627304375048ull, 7079152462663640793ull, 9430854713541920393ull, 10120897727742141847ull, + 6015137363156052521ull, 9143335679527440329ull, 1965209665081157387ull, 16559717930979642786ull, + 6332179897502099272ull, 1448475308516474107ull, 7560963980189861213ull, 16163234093112718807ull, }; constexpr std::uint64_t kBracketsMag_at_bar[] = { 4ull, 0ull, 25ull, 0ull, @@ -1202,7 +1345,8 @@ constexpr std::uint64_t kBracketsMag_folded[] = { // expectation corrected (v19-E): kBracketsMag_final 16366316158039625809ull -> 10498718013143678943ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (v19-D): kBracketsMag_final 10498718013143678943ull -> 17233175003301036440ull, because v19-D keeps fewer retired rows and folds a bracket family's erased members behind a retained one as runs (pineforge-source-adapter/v4: K1 releases a current-cycle leg the revival's superseded test answers for; BracketRoster parks); the receipt bars, folded counts and trades did not move; harvested with the TU's own switch on this tree (r5/v19-d). // expectation corrected (V19-FIX): kBracketsMag_final 17233175003301036440ull -> 2507370523932097816ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kBracketsMag_final = 2507370523932097816ull; +// expectation corrected (INT26 v19 hash re-pin): kBracketsMag_final 2507370523932097816ull -> 16163234093112718807ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kBracketsMag_final = 16163234093112718807ull; constexpr Trade kBracketsMag_trades[] = { {1736121660000LL, 1736121660000LL, 100.75, 101.25, 2, 0}, {1736122020000LL, 1736122260000LL, 100.75, 102, 2, 0}, @@ -1334,17 +1478,58 @@ constexpr Trade kBracketsMag_trades[] = { // 7242134762483671893ull -> 8464039108905730677ull // 1865370244366172271ull -> 16757005045255032303ull // 10203291295434148760ull -> 14459029476982242904ull +// expectation corrected (INT26 v19 hash re-pin, kGroups_rows, 40 of 40 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 428451890117060597ull -> 16343339797501156050ull [H-THIN] +// 16045090594768170214ull -> 4412441122235541001ull [H-THIN] +// 18166866907386903819ull -> 8796315700613324416ull [H-THIN] +// 6958941312543333314ull -> 7395320384889894535ull [H-THIN] +// 12357763379855329333ull -> 14779897915981054858ull [H-THIN] +// 10319108494843725228ull -> 12902321761157770999ull [H-THIN] +// 8057235825342986589ull -> 13233007585699491660ull [H-THIN] +// 775027953877311050ull -> 4759091960862828143ull [H-THIN] +// 14610483346660466821ull -> 9823824249826547972ull [H-THIN] +// 1198104509264338521ull -> 10222432195471458288ull [H-THIN] +// 15842771393076564009ull -> 12268080935222816256ull [H-THIN] +// 5342656873780671151ull -> 1820590039511046510ull [H-THIN] +// 2165268433984104ull -> 13378031530146436277ull [H-THIN] +// 16270682949794220379ull -> 12953190010614641206ull [H-THIN] +// 14994318600896901474ull -> 1053166407871468103ull [H-THIN] +// 3023615581009694835ull -> 9272374160641534696ull [H-THIN] +// 13496533941019930808ull -> 10364329263136600283ull [H-THIN] +// 6174715221277441970ull -> 4225956051004008949ull [H-THIN] +// 5113739070112146000ull -> 7108588782921604133ull [H-THIN] +// 1772495940269705534ull -> 5489213286228074399ull [H-THIN] +// 10686035606077590379ull -> 10616002336161510798ull [H-THIN] +// 17379645375024748460ull -> 9622546459129830553ull [H-THIN] +// 3810215059026298411ull -> 944039958041978562ull [H-THIN] +// 6980015280280300871ull -> 15116276931277952764ull [H-THIN] +// 17653590613423189804ull -> 10604949906509802561ull [H-THIN] +// 5097055414090822762ull -> 2073356378818895203ull [H-THIN] +// 16040617079938685278ull -> 2582840377395668879ull [H-THIN] +// 7474835689400851509ull -> 2562757827364035110ull [H-THIN] +// 3588654748202396640ull -> 12699965404948142476ull [H-THIN] +// 482256096822295700ull -> 1195332705294901458ull [H-THIN] +// 491618572355040023ull -> 16972648823711764497ull [H-THIN] +// 15447520409861182807ull -> 4481092489104066192ull [H-THIN] +// 13546661642780035054ull -> 7005130885374206459ull [H-THIN] +// 586417508450537570ull -> 6610411880574253007ull [H-THIN] +// 16805284807511709227ull -> 3204572366595249610ull [H-THIN] +// 14582173341406959084ull -> 2120665229545761995ull [H-THIN] +// 4670485499468050406ull -> 6282585683572106081ull [H-THIN] +// 8464039108905730677ull -> 12064630322548111510ull [H-THIN] +// 16757005045255032303ull -> 1478230385172911488ull [H-THIN] +// 14459029476982242904ull -> 11603812731166869657ull [H-THIN] constexpr std::uint64_t kGroups_rows[] = { - 428451890117060597ull, 16045090594768170214ull, 18166866907386903819ull, 6958941312543333314ull, - 12357763379855329333ull, 10319108494843725228ull, 8057235825342986589ull, 775027953877311050ull, - 14610483346660466821ull, 1198104509264338521ull, 15842771393076564009ull, 5342656873780671151ull, - 2165268433984104ull, 16270682949794220379ull, 14994318600896901474ull, 3023615581009694835ull, - 13496533941019930808ull, 6174715221277441970ull, 5113739070112146000ull, 1772495940269705534ull, - 10686035606077590379ull, 17379645375024748460ull, 3810215059026298411ull, 6980015280280300871ull, - 17653590613423189804ull, 5097055414090822762ull, 16040617079938685278ull, 7474835689400851509ull, - 3588654748202396640ull, 482256096822295700ull, 491618572355040023ull, 15447520409861182807ull, - 13546661642780035054ull, 586417508450537570ull, 16805284807511709227ull, 14582173341406959084ull, - 4670485499468050406ull, 8464039108905730677ull, 16757005045255032303ull, 14459029476982242904ull, + 16343339797501156050ull, 4412441122235541001ull, 8796315700613324416ull, 7395320384889894535ull, + 14779897915981054858ull, 12902321761157770999ull, 13233007585699491660ull, 4759091960862828143ull, + 9823824249826547972ull, 10222432195471458288ull, 12268080935222816256ull, 1820590039511046510ull, + 13378031530146436277ull, 12953190010614641206ull, 1053166407871468103ull, 9272374160641534696ull, + 10364329263136600283ull, 4225956051004008949ull, 7108588782921604133ull, 5489213286228074399ull, + 10616002336161510798ull, 9622546459129830553ull, 944039958041978562ull, 15116276931277952764ull, + 10604949906509802561ull, 2073356378818895203ull, 2582840377395668879ull, 2562757827364035110ull, + 12699965404948142476ull, 1195332705294901458ull, 16972648823711764497ull, 4481092489104066192ull, + 7005130885374206459ull, 6610411880574253007ull, 3204572366595249610ull, 2120665229545761995ull, + 6282585683572106081ull, 12064630322548111510ull, 1478230385172911488ull, 11603812731166869657ull, }; constexpr std::uint64_t kGroups_at_bar[] = { 4ull, 0ull, 9ull, 0ull, @@ -1414,7 +1599,8 @@ constexpr std::uint64_t kGroups_folded[] = { // expectation corrected: kGroups_final 11093578271132015941ull -> 2748401044783380800ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); receipt cursors and trades did not move. // expectation corrected (v19-E): kGroups_final 2748401044783380800ull -> 10203291295434148760ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kGroups_final 10203291295434148760ull -> 14459029476982242904ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kGroups_final = 14459029476982242904ull; +// expectation corrected (INT26 v19 hash re-pin): kGroups_final 14459029476982242904ull -> 11603812731166869657ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kGroups_final = 11603812731166869657ull; constexpr Trade kGroups_trades[] = { {1736121840000LL, 1736122020000LL, 102.25, 100.75, 1, 0}, {1736122020000LL, 1736122500000LL, 100.25, 100.5, 2, 0}, @@ -1550,17 +1736,58 @@ constexpr Trade kGroups_trades[] = { // 6230002762718235250ull -> 5741952581690045810ull // 14578437808706486766ull -> 1055864288829102030ull // 12557658352255323163ull -> 16431710829567235323ull +// expectation corrected (INT26 v19 hash re-pin, kGroupsMag_rows, 40 of 40 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 18395077400277767983ull -> 2680312739779497464ull [H-THIN] +// 12064304672483746293ull -> 822058034361535600ull [H-THIN] +// 14523426078186844972ull -> 3962469670734696157ull [H-THIN] +// 8862832225214863179ull -> 17411473840245726690ull [H-THIN] +// 14588621505662414032ull -> 5739216726257722739ull [H-THIN] +// 4447156622707225162ull -> 4823834691276900811ull [H-THIN] +// 4361493804042466119ull -> 14557402943266793758ull [H-THIN] +// 1804450653994911768ull -> 17153420186503897123ull [H-THIN] +// 14783451729418173724ull -> 11456037253658883689ull [H-THIN] +// 8082972508033422197ull -> 2071497720185716146ull [H-THIN] +// 8979544420957768909ull -> 10228359593541992710ull [H-THIN] +// 7493375864971444891ull -> 8164374031973170202ull [H-THIN] +// 2222992351194986144ull -> 14741153401126253421ull [H-THIN] +// 11771896598749089907ull -> 5321723665543660526ull [H-THIN] +// 12560649180545194470ull -> 13847739486469904387ull [H-THIN] +// 14421558832844187772ull -> 5160912244748048563ull [H-THIN] +// 4296761660805133425ull -> 9669498150986227540ull [H-THIN] +// 742061714794648722ull -> 8947635553433824973ull [H-THIN] +// 14109205439874640145ull -> 12178890551572236268ull [H-THIN] +// 6057192239224243194ull -> 5673400076925890495ull [H-THIN] +// 18247263908222071868ull -> 15315129035357191511ull [H-THIN] +// 17174776223039553865ull -> 13281596230891049428ull [H-THIN] +// 13727420337195600730ull -> 4615956555268131813ull [H-THIN] +// 2818312375125287588ull -> 5864244316711809037ull [H-THIN] +// 9915087812300743522ull -> 1805681795225876151ull [H-THIN] +// 7161399393808283031ull -> 3885225909948070556ull [H-THIN] +// 2862680592861432951ull -> 16301392942591338516ull [H-THIN] +// 1307007383947855281ull -> 16771933549340211026ull [H-THIN] +// 18356049985413183826ull -> 1873451560715463970ull [H-THIN] +// 15935937615335005856ull -> 8712225159834374858ull [H-THIN] +// 16415296467281494110ull -> 3729496159990057708ull [H-THIN] +// 16803171265864625638ull -> 1612160720622215895ull [H-THIN] +// 10957401283361829601ull -> 5717348211442227590ull [H-THIN] +// 11252967136817419064ull -> 2472676727577223345ull [H-THIN] +// 4760661672048575327ull -> 8470082562618443630ull [H-THIN] +// 14915144339907298167ull -> 1840587632070777498ull [H-THIN] +// 575503570718308907ull -> 15629226293335228938ull [H-THIN] +// 5741952581690045810ull -> 2022458278016162571ull [H-THIN] +// 1055864288829102030ull -> 13820365368293950247ull [H-THIN] +// 16431710829567235323ull -> 7247005295497143736ull [H-THIN] constexpr std::uint64_t kGroupsMag_rows[] = { - 18395077400277767983ull, 12064304672483746293ull, 14523426078186844972ull, 8862832225214863179ull, - 14588621505662414032ull, 4447156622707225162ull, 4361493804042466119ull, 1804450653994911768ull, - 14783451729418173724ull, 8082972508033422197ull, 8979544420957768909ull, 7493375864971444891ull, - 2222992351194986144ull, 11771896598749089907ull, 12560649180545194470ull, 14421558832844187772ull, - 4296761660805133425ull, 742061714794648722ull, 14109205439874640145ull, 6057192239224243194ull, - 18247263908222071868ull, 17174776223039553865ull, 13727420337195600730ull, 2818312375125287588ull, - 9915087812300743522ull, 7161399393808283031ull, 2862680592861432951ull, 1307007383947855281ull, - 18356049985413183826ull, 15935937615335005856ull, 16415296467281494110ull, 16803171265864625638ull, - 10957401283361829601ull, 11252967136817419064ull, 4760661672048575327ull, 14915144339907298167ull, - 575503570718308907ull, 5741952581690045810ull, 1055864288829102030ull, 16431710829567235323ull, + 2680312739779497464ull, 822058034361535600ull, 3962469670734696157ull, 17411473840245726690ull, + 5739216726257722739ull, 4823834691276900811ull, 14557402943266793758ull, 17153420186503897123ull, + 11456037253658883689ull, 2071497720185716146ull, 10228359593541992710ull, 8164374031973170202ull, + 14741153401126253421ull, 5321723665543660526ull, 13847739486469904387ull, 5160912244748048563ull, + 9669498150986227540ull, 8947635553433824973ull, 12178890551572236268ull, 5673400076925890495ull, + 15315129035357191511ull, 13281596230891049428ull, 4615956555268131813ull, 5864244316711809037ull, + 1805681795225876151ull, 3885225909948070556ull, 16301392942591338516ull, 16771933549340211026ull, + 1873451560715463970ull, 8712225159834374858ull, 3729496159990057708ull, 1612160720622215895ull, + 5717348211442227590ull, 2472676727577223345ull, 8470082562618443630ull, 1840587632070777498ull, + 15629226293335228938ull, 2022458278016162571ull, 13820365368293950247ull, 7247005295497143736ull, }; constexpr std::uint64_t kGroupsMag_at_bar[] = { 4ull, 0ull, 9ull, 0ull, @@ -1630,7 +1857,8 @@ constexpr std::uint64_t kGroupsMag_folded[] = { // expectation corrected: kGroupsMag_final 8471333527175518852ull -> 17786987883651578348ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); receipt cursors and trades did not move. // expectation corrected (v19-E): kGroupsMag_final 17786987883651578348ull -> 12557658352255323163ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kGroupsMag_final 12557658352255323163ull -> 16431710829567235323ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kGroupsMag_final = 16431710829567235323ull; +// expectation corrected (INT26 v19 hash re-pin): kGroupsMag_final 16431710829567235323ull -> 7247005295497143736ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kGroupsMag_final = 7247005295497143736ull; constexpr Trade kGroupsMag_trades[] = { {1736121840000LL, 1736122020000LL, 102.25, 100.75, 1, 0}, {1736122020000LL, 1736122500000LL, 100.25, 100.5, 2, 0}, @@ -1766,17 +1994,58 @@ constexpr Trade kGroupsMag_trades[] = { // 1600250331183838308ull -> 2477382922949126500ull // 11097052730880109561ull -> 9214756763675162553ull // 13378853722070711842ull -> 6336494971372918338ull +// expectation corrected (INT26 v19 hash re-pin, kStopLimit_rows, 40 of 40 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 4402211772532359251ull -> 10950004051556324382ull [H-THIN] +// 5449557300220864793ull -> 14490347146816688448ull [H-THIN] +// 2575787073227669374ull -> 17137431077388642227ull [H-THIN] +// 9938418473191636202ull -> 2067378368474728109ull [H-THIN] +// 10058127381824367565ull -> 11488424314019716432ull [H-THIN] +// 1315470856055524560ull -> 8954327035592863039ull [H-THIN] +// 15263698702667167777ull -> 7512665423720105326ull [H-THIN] +// 13220310601440668918ull -> 4187947268963235259ull [H-THIN] +// 15409637845860903908ull -> 2913839783495075163ull [H-THIN] +// 6446798858981198964ull -> 17362053743420220567ull [H-THIN] +// 10451839227886399841ull -> 18277069102810285906ull [H-THIN] +// 11816001997725235374ull -> 6966128010082440393ull [H-THIN] +// 1932657111780796664ull -> 343766021385529931ull [H-THIN] +// 5608877402063171472ull -> 14896797560397420035ull [H-THIN] +// 4816751693073396948ull -> 17434159485109243471ull [H-THIN] +// 406036428804793947ull -> 6133926189978579814ull [H-THIN] +// 8411172447822998997ull -> 13535889090349139748ull [H-THIN] +// 9566812430047964551ull -> 7154291922301271050ull [H-THIN] +// 7047212701932505101ull -> 12262784895722576372ull [H-THIN] +// 12944086715960255318ull -> 12762159079513717323ull [H-THIN] +// 2932986234293574878ull -> 6211630995163031141ull [H-THIN] +// 3851484164654454670ull -> 15698793793400314809ull [H-THIN] +// 5793783382051423327ull -> 9224825516139358046ull [H-THIN] +// 17080321546688673331ull -> 3897555601472887486ull [H-THIN] +// 15357710425425554761ull -> 7742563012562507868ull [H-THIN] +// 17224699499285056096ull -> 6030704080576753327ull [H-THIN] +// 10136441707663244279ull -> 1172792760249453492ull [H-THIN] +// 17829354381425508170ull -> 215530794059409427ull [H-THIN] +// 12155373643870828842ull -> 1515537412314014355ull [H-THIN] +// 10715131222481220760ull -> 8129234433613561553ull [H-THIN] +// 4867968313932752304ull -> 7238089528421731329ull [H-THIN] +// 4064453165671217168ull -> 9745351952936386365ull [H-THIN] +// 1769313611189948830ull -> 14384312445426600067ull [H-THIN] +// 4504345708664910252ull -> 6198847811909310381ull [H-THIN] +// 782093603809331885ull -> 11872127846069280170ull [H-THIN] +// 18267060294147680041ull -> 6744981219021824364ull [H-THIN] +// 2944439646726608949ull -> 6655913456858325688ull [H-THIN] +// 2477382922949126500ull -> 17554715975906318531ull [H-THIN] +// 9214756763675162553ull -> 542461356767618432ull [H-THIN] +// 6336494971372918338ull -> 13852676117274540777ull [H-THIN] constexpr std::uint64_t kStopLimit_rows[] = { - 4402211772532359251ull, 5449557300220864793ull, 2575787073227669374ull, 9938418473191636202ull, - 10058127381824367565ull, 1315470856055524560ull, 15263698702667167777ull, 13220310601440668918ull, - 15409637845860903908ull, 6446798858981198964ull, 10451839227886399841ull, 11816001997725235374ull, - 1932657111780796664ull, 5608877402063171472ull, 4816751693073396948ull, 406036428804793947ull, - 8411172447822998997ull, 9566812430047964551ull, 7047212701932505101ull, 12944086715960255318ull, - 2932986234293574878ull, 3851484164654454670ull, 5793783382051423327ull, 17080321546688673331ull, - 15357710425425554761ull, 17224699499285056096ull, 10136441707663244279ull, 17829354381425508170ull, - 12155373643870828842ull, 10715131222481220760ull, 4867968313932752304ull, 4064453165671217168ull, - 1769313611189948830ull, 4504345708664910252ull, 782093603809331885ull, 18267060294147680041ull, - 2944439646726608949ull, 2477382922949126500ull, 9214756763675162553ull, 6336494971372918338ull, + 10950004051556324382ull, 14490347146816688448ull, 17137431077388642227ull, 2067378368474728109ull, + 11488424314019716432ull, 8954327035592863039ull, 7512665423720105326ull, 4187947268963235259ull, + 2913839783495075163ull, 17362053743420220567ull, 18277069102810285906ull, 6966128010082440393ull, + 343766021385529931ull, 14896797560397420035ull, 17434159485109243471ull, 6133926189978579814ull, + 13535889090349139748ull, 7154291922301271050ull, 12262784895722576372ull, 12762159079513717323ull, + 6211630995163031141ull, 15698793793400314809ull, 9224825516139358046ull, 3897555601472887486ull, + 7742563012562507868ull, 6030704080576753327ull, 1172792760249453492ull, 215530794059409427ull, + 1515537412314014355ull, 8129234433613561553ull, 7238089528421731329ull, 9745351952936386365ull, + 14384312445426600067ull, 6198847811909310381ull, 11872127846069280170ull, 6744981219021824364ull, + 6655913456858325688ull, 17554715975906318531ull, 542461356767618432ull, 13852676117274540777ull, }; constexpr std::uint64_t kStopLimit_at_bar[] = { 4ull, 0ull, 13ull, 11ull, @@ -1846,7 +2115,8 @@ constexpr std::uint64_t kStopLimit_folded[] = { // expectation corrected: kStopLimit_final 2398349164106856788ull -> 3665904849405917965ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); receipt cursors and trades did not move. // expectation corrected (v19-E): kStopLimit_final 3665904849405917965ull -> 13378853722070711842ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kStopLimit_final 13378853722070711842ull -> 6336494971372918338ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kStopLimit_final = 6336494971372918338ull; +// expectation corrected (INT26 v19 hash re-pin): kStopLimit_final 6336494971372918338ull -> 13852676117274540777ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kStopLimit_final = 13852676117274540777ull; constexpr Trade kStopLimit_trades[] = { {1736121660000LL, 1736121780000LL, 100.5, 101.5, 1, 0}, {1736121660000LL, 1736121900000LL, 100.5, 102, 1, 0}, @@ -1985,17 +2255,58 @@ constexpr Trade kStopLimit_trades[] = { // 10084593582520942716ull -> 17028258441606210396ull // 7804430760616484748ull -> 8328930773731943340ull // 12421926773651986280ull -> 9254963348917194152ull +// expectation corrected (INT26 v19 hash re-pin, kStopLimitMag_rows, 40 of 40 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 11157594904456853627ull -> 12049254652408164286ull [H-THIN] +// 8401675850175966532ull -> 7170364698800146161ull [H-THIN] +// 14768083129611579792ull -> 16063150345282688529ull [H-THIN] +// 2813084369439848034ull -> 2375247605653847437ull [H-THIN] +// 680081896452532403ull -> 5989703757491448050ull [H-THIN] +// 3114044039354922134ull -> 7325059507104947563ull [H-THIN] +// 18101581505709266307ull -> 4110843077158619028ull [H-THIN] +// 3765585083570211695ull -> 8512043085829177902ull [H-THIN] +// 5126585860831024376ull -> 11976516818017786813ull [H-THIN] +// 15762785015022297388ull -> 8000889795845112059ull [H-THIN] +// 14002286754420941147ull -> 3812031270016354768ull [H-THIN] +// 9222864100890729823ull -> 13171068499659342452ull [H-THIN] +// 6646548798123240355ull -> 2834570042813630280ull [H-THIN] +// 6473759346034722294ull -> 612293012859052373ull [H-THIN] +// 12710876809005991003ull -> 941008970127083544ull [H-THIN] +// 3124661257347549740ull -> 12868627571096102339ull [H-THIN] +// 2631460917473729378ull -> 13282802570212513971ull [H-THIN] +// 6218399147747637008ull -> 3990088271426726433ull [H-THIN] +// 7885746142563048312ull -> 15805224658207253271ull [H-THIN] +// 7465774284363189296ull -> 4861907005886599783ull [H-THIN] +// 15336532554253228688ull -> 5606595334476947895ull [H-THIN] +// 3052942228447511665ull -> 16715759077213701098ull [H-THIN] +// 11131484997342707171ull -> 17047719425359875908ull [H-THIN] +// 12129492814419792298ull -> 8426786106325009307ull [H-THIN] +// 16582911731330051275ull -> 15538761259524269350ull [H-THIN] +// 14696325906442206119ull -> 14720303962652891230ull [H-THIN] +// 13365743854012499388ull -> 6644795815053845919ull [H-THIN] +// 271739893287090075ull -> 13567799877554087198ull [H-THIN] +// 1688196853701772460ull -> 303066552358707847ull [H-THIN] +// 1042572030152166013ull -> 1144887452290110514ull [H-THIN] +// 18233864280626554198ull -> 17215798505737563325ull [H-THIN] +// 14647413247529149725ull -> 13389121371815587728ull [H-THIN] +// 4738394210991551364ull -> 10590670751038194751ull [H-THIN] +// 10052202710057429660ull -> 16401520297421927605ull [H-THIN] +// 3980637583370132169ull -> 1764458890420715860ull [H-THIN] +// 11984610609265407890ull -> 15454338180826410585ull [H-THIN] +// 17391162522930315245ull -> 10945801551956678372ull [H-THIN] +// 17028258441606210396ull -> 13245700179032171137ull [H-THIN] +// 8328930773731943340ull -> 14239530769682782007ull [H-THIN] +// 9254963348917194152ull -> 4769056648897025171ull [H-THIN] constexpr std::uint64_t kStopLimitMag_rows[] = { - 11157594904456853627ull, 8401675850175966532ull, 14768083129611579792ull, 2813084369439848034ull, - 680081896452532403ull, 3114044039354922134ull, 18101581505709266307ull, 3765585083570211695ull, - 5126585860831024376ull, 15762785015022297388ull, 14002286754420941147ull, 9222864100890729823ull, - 6646548798123240355ull, 6473759346034722294ull, 12710876809005991003ull, 3124661257347549740ull, - 2631460917473729378ull, 6218399147747637008ull, 7885746142563048312ull, 7465774284363189296ull, - 15336532554253228688ull, 3052942228447511665ull, 11131484997342707171ull, 12129492814419792298ull, - 16582911731330051275ull, 14696325906442206119ull, 13365743854012499388ull, 271739893287090075ull, - 1688196853701772460ull, 1042572030152166013ull, 18233864280626554198ull, 14647413247529149725ull, - 4738394210991551364ull, 10052202710057429660ull, 3980637583370132169ull, 11984610609265407890ull, - 17391162522930315245ull, 17028258441606210396ull, 8328930773731943340ull, 9254963348917194152ull, + 12049254652408164286ull, 7170364698800146161ull, 16063150345282688529ull, 2375247605653847437ull, + 5989703757491448050ull, 7325059507104947563ull, 4110843077158619028ull, 8512043085829177902ull, + 11976516818017786813ull, 8000889795845112059ull, 3812031270016354768ull, 13171068499659342452ull, + 2834570042813630280ull, 612293012859052373ull, 941008970127083544ull, 12868627571096102339ull, + 13282802570212513971ull, 3990088271426726433ull, 15805224658207253271ull, 4861907005886599783ull, + 5606595334476947895ull, 16715759077213701098ull, 17047719425359875908ull, 8426786106325009307ull, + 15538761259524269350ull, 14720303962652891230ull, 6644795815053845919ull, 13567799877554087198ull, + 303066552358707847ull, 1144887452290110514ull, 17215798505737563325ull, 13389121371815587728ull, + 10590670751038194751ull, 16401520297421927605ull, 1764458890420715860ull, 15454338180826410585ull, + 10945801551956678372ull, 13245700179032171137ull, 14239530769682782007ull, 4769056648897025171ull, }; constexpr std::uint64_t kStopLimitMag_at_bar[] = { 4ull, 0ull, 13ull, 0ull, @@ -2065,7 +2376,8 @@ constexpr std::uint64_t kStopLimitMag_folded[] = { // expectation corrected: kStopLimitMag_final 3006349506749355420ull -> 15015254700001776595ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); receipt cursors and trades did not move. // expectation corrected (v19-E): kStopLimitMag_final 15015254700001776595ull -> 12421926773651986280ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kStopLimitMag_final 12421926773651986280ull -> 9254963348917194152ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kStopLimitMag_final = 9254963348917194152ull; +// expectation corrected (INT26 v19 hash re-pin): kStopLimitMag_final 9254963348917194152ull -> 4769056648897025171ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kStopLimitMag_final = 4769056648897025171ull; constexpr Trade kStopLimitMag_trades[] = { {1736121660000LL, 1736121780000LL, 100.25, 101.5, 1, 0}, {1736121660000LL, 1736121900000LL, 100.25, 102, 1, 0}, @@ -2132,11 +2444,28 @@ constexpr Trade kStopLimitMag_trades[] = { // 117536285131979531ull -> 14672633943864425963ull // 15011555328205275466ull -> 5161029633467225738ull // 10880804929896122760ull -> 7726211904455540872ull +// expectation corrected (INT26 v19 hash re-pin, kQuiet_rows, 16 of 16 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 428451890117060597ull -> 16343339797501156050ull [H-THIN] +// 1939867743749048694ull -> 16937855132455432219ull [H-THIN] +// 11195157439215676551ull -> 4413074683041463858ull [H-THIN] +// 13964997473137927097ull -> 1254109408918791568ull [H-THIN] +// 9212479816748968188ull -> 13031573772438562141ull [H-THIN] +// 16000164592560197492ull -> 9886236508543006595ull [H-THIN] +// 8130410293855426317ull -> 7133223670036907210ull [H-THIN] +// 10300840897873716753ull -> 3481249560441549862ull [H-THIN] +// 16568174616242417603ull -> 4184701415579817428ull [H-THIN] +// 17876633525817055002ull -> 17420350975284167145ull [H-THIN] +// 10659236710582653713ull -> 14713852734592639794ull [H-THIN] +// 17983194377250571525ull -> 17934243807563394006ull [H-THIN] +// 2047181318263126988ull -> 4115550806570448239ull [H-THIN] +// 14672633943864425963ull -> 14750302925653734844ull [H-THIN] +// 5161029633467225738ull -> 15882033274424875345ull [H-THIN] +// 7726211904455540872ull -> 10123935771407189591ull [H-THIN] constexpr std::uint64_t kQuiet_rows[] = { - 428451890117060597ull, 1939867743749048694ull, 11195157439215676551ull, 13964997473137927097ull, - 9212479816748968188ull, 16000164592560197492ull, 8130410293855426317ull, 10300840897873716753ull, - 16568174616242417603ull, 17876633525817055002ull, 10659236710582653713ull, 17983194377250571525ull, - 2047181318263126988ull, 14672633943864425963ull, 5161029633467225738ull, 7726211904455540872ull, + 16343339797501156050ull, 16937855132455432219ull, 4413074683041463858ull, 1254109408918791568ull, + 13031573772438562141ull, 9886236508543006595ull, 7133223670036907210ull, 3481249560441549862ull, + 4184701415579817428ull, 17420350975284167145ull, 14713852734592639794ull, 17934243807563394006ull, + 4115550806570448239ull, 14750302925653734844ull, 15882033274424875345ull, 10123935771407189591ull, }; constexpr std::uint64_t kQuiet_at_bar[] = { 4ull, 0ull, 9ull, 0ull, @@ -2170,7 +2499,8 @@ constexpr std::uint64_t kQuiet_folded[] = { // expectation corrected: kQuiet_final 6139548852501668824ull -> 15339934536417004376ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); receipt cursors and trades did not move. // expectation corrected (v19-E): kQuiet_final 15339934536417004376ull -> 10880804929896122760ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kQuiet_final 10880804929896122760ull -> 7726211904455540872ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kQuiet_final = 7726211904455540872ull; +// expectation corrected (INT26 v19 hash re-pin): kQuiet_final 7726211904455540872ull -> 10123935771407189591ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kQuiet_final = 10123935771407189591ull; constexpr Trade kQuiet_trades[] = { {1736121720000LL, 1736121840000LL, 101.5, 102.25, 2, 0}, }; @@ -2226,11 +2556,28 @@ constexpr Trade kQuiet_trades[] = { // 10276732342521370229ull -> 14848461357115496341ull // 7149263525418787648ull -> 8190184809671648256ull // 18272199307858808366ull -> 15117606282418226478ull +// expectation corrected (INT26 v19 hash re-pin, kQuietMag_rows, 16 of 16 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move: +// 18395077400277767983ull -> 2680312739779497464ull [H-THIN] +// 8974918290184464318ull -> 14986120281306231523ull [H-THIN] +// 9820360325097478805ull -> 9036345257640757520ull [H-THIN] +// 9357979130704319683ull -> 2157695045143891892ull [H-THIN] +// 1380600006529331319ull -> 12933046682291176380ull [H-THIN] +// 3158417560156675530ull -> 1720445300800871165ull [H-THIN] +// 3834533453170743615ull -> 13872138906269598328ull [H-THIN] +// 4903111063815600551ull -> 7090820869966325000ull [H-THIN] +// 8341995541182971309ull -> 8891708591767162058ull [H-THIN] +// 1420418759607814080ull -> 16869781438282071171ull [H-THIN] +// 6268521046740087103ull -> 16039329020103217780ull [H-THIN] +// 8200284679939302671ull -> 646593827978091276ull [H-THIN] +// 4063870167578037858ull -> 33504042054535113ull [H-THIN] +// 14848461357115496341ull -> 18256975930975412058ull [H-THIN] +// 8190184809671648256ull -> 12491220415833066003ull [H-THIN] +// 15117606282418226478ull -> 6982614520627168209ull [H-THIN] constexpr std::uint64_t kQuietMag_rows[] = { - 18395077400277767983ull, 8974918290184464318ull, 9820360325097478805ull, 9357979130704319683ull, - 1380600006529331319ull, 3158417560156675530ull, 3834533453170743615ull, 4903111063815600551ull, - 8341995541182971309ull, 1420418759607814080ull, 6268521046740087103ull, 8200284679939302671ull, - 4063870167578037858ull, 14848461357115496341ull, 8190184809671648256ull, 15117606282418226478ull, + 2680312739779497464ull, 14986120281306231523ull, 9036345257640757520ull, 2157695045143891892ull, + 12933046682291176380ull, 1720445300800871165ull, 13872138906269598328ull, 7090820869966325000ull, + 8891708591767162058ull, 16869781438282071171ull, 16039329020103217780ull, 646593827978091276ull, + 33504042054535113ull, 18256975930975412058ull, 12491220415833066003ull, 6982614520627168209ull, }; constexpr std::uint64_t kQuietMag_at_bar[] = { 4ull, 0ull, 9ull, 0ull, @@ -2264,7 +2611,8 @@ constexpr std::uint64_t kQuietMag_folded[] = { // expectation corrected: kQuietMag_final 12364294267467016655ull -> 15979450874488009551ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); receipt cursors and trades did not move. // expectation corrected (v19-E): kQuietMag_final 15979450874488009551ull -> 18272199307858808366ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the receipt bars, folded counts and trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kQuietMag_final 18272199307858808366ull -> 15117606282418226478ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the receipt bars, folded counts and trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kQuietMag_final = 15117606282418226478ull; +// expectation corrected (INT26 v19 hash re-pin): kQuietMag_final 15117606282418226478ull -> 6982614520627168209ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the receipt bars, folded counts and trades did not move. +constexpr std::uint64_t kQuietMag_final = 6982614520627168209ull; constexpr Trade kQuietMag_trades[] = { {1736121720000LL, 1736121840000LL, 101.5, 102.25, 2, 0}, }; diff --git a/tests/test_adapter_recording_hash_witness.cpp b/tests/test_adapter_recording_hash_witness.cpp index 2df1349f..0969239b 100644 --- a/tests/test_adapter_recording_hash_witness.cpp +++ b/tests/test_adapter_recording_hash_witness.cpp @@ -375,39 +375,72 @@ void emit(Scenario scenario, const Observed& got) { // 11423048270061715692ull -> 7789401651194802796ull // 13118814681321380879ull -> 16644199069697802031ull // 8426887344131973464ull -> 13033963886965614456ull +// expectation corrected (INT26 v19 hash re-pin, kSubmit_rows, 32 of 32 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 8933562200260968840ull -> 8019365971493599225ull [H-THIN] +// 16496088491414191334ull -> 12084928604454138979ull [H-THIN] +// 17745030456223078154ull -> 7685642069387429171ull [H-THIN] +// 15976454321452418827ull -> 3048458165749351592ull [H-THIN] +// 13867778238676275167ull -> 1016691552216785494ull [H-THIN] +// 10620212436062486555ull -> 13664491885780078814ull [H-THIN] +// 16387014991078439144ull -> 9587023827432894395ull [H-THIN] +// 5372310558802353946ull -> 14524900109868328621ull [H-THIN] +// 8117928017652874474ull -> 9306730010360799799ull [H-THIN] +// 12227480580368604280ull -> 10975560481515534245ull [H-THIN] +// 11671431865371234655ull -> 6175700847996002068ull [H-THIN] +// 15852753888607827401ull -> 17944617882018938484ull [H-THIN] +// 4564599765874863441ull -> 89066156438191170ull [H-THIN] +// 12753515022268993432ull -> 7853223159133263199ull [H-THIN] +// 16693007526387479527ull -> 16829337964629633742ull [H-THIN] +// 9553609781489681132ull -> 14048972284943154657ull [H-THIN] +// 9723295982254899748ull -> 10772403295793355383ull [H-THIN] +// 9596448656160987273ull -> 12849842554292919416ull [H-THIN] +// 14172117111020274407ull -> 11523841829240280476ull [H-THIN] +// 1811404642983946404ull -> 14633701048963800807ull [H-THIN] +// 6600438489637577460ull -> 13106625599621486697ull [H-THIN] +// 1823325663201613856ull -> 11578248645351712284ull [H-THIN] +// 671600216917334430ull -> 12923518206466935124ull [H-THIN] +// 14450998569007950837ull -> 11664001542269768811ull [H-THIN] +// 18123601071981623076ull -> 4353270054101958588ull [H-THIN] +// 5788029478896071429ull -> 12509431446261158271ull [H-THIN] +// 5791781074908951980ull -> 6827814975693211880ull [H-THIN] +// 8180458203668078357ull -> 13768158546548443068ull [H-THIN] +// 10842259428116090863ull -> 6634357882333049268ull [H-THIN] +// 7789401651194802796ull -> 8521102792149394239ull [H-THIN] +// 16644199069697802031ull -> 15674750399051881772ull [H-THIN] +// 13033963886965614456ull -> 7695240294002266835ull [H-THIN] constexpr std::uint64_t kSubmit_rows[] = { - 8933562200260968840ull, - 16496088491414191334ull, - 17745030456223078154ull, - 15976454321452418827ull, - 13867778238676275167ull, - 10620212436062486555ull, - 16387014991078439144ull, - 5372310558802353946ull, - 8117928017652874474ull, - 12227480580368604280ull, - 11671431865371234655ull, - 15852753888607827401ull, - 4564599765874863441ull, - 12753515022268993432ull, - 16693007526387479527ull, - 9553609781489681132ull, - 9723295982254899748ull, - 9596448656160987273ull, - 14172117111020274407ull, - 1811404642983946404ull, - 6600438489637577460ull, - 1823325663201613856ull, - 671600216917334430ull, - 14450998569007950837ull, - 18123601071981623076ull, - 5788029478896071429ull, - 5791781074908951980ull, - 8180458203668078357ull, - 10842259428116090863ull, - 7789401651194802796ull, - 16644199069697802031ull, - 13033963886965614456ull, + 8019365971493599225ull, + 12084928604454138979ull, + 7685642069387429171ull, + 3048458165749351592ull, + 1016691552216785494ull, + 13664491885780078814ull, + 9587023827432894395ull, + 14524900109868328621ull, + 9306730010360799799ull, + 10975560481515534245ull, + 6175700847996002068ull, + 17944617882018938484ull, + 89066156438191170ull, + 7853223159133263199ull, + 16829337964629633742ull, + 14048972284943154657ull, + 10772403295793355383ull, + 12849842554292919416ull, + 11523841829240280476ull, + 14633701048963800807ull, + 13106625599621486697ull, + 11578248645351712284ull, + 12923518206466935124ull, + 11664001542269768811ull, + 4353270054101958588ull, + 12509431446261158271ull, + 6827814975693211880ull, + 13768158546548443068ull, + 6634357882333049268ull, + 8521102792149394239ull, + 15674750399051881772ull, + 7695240294002266835ull, }; // expectation corrected (kSubmit_reads, 6 of 6 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); the trades did not move: // 10242542257977841923ull -> 17535387619041092918ull @@ -430,18 +463,26 @@ constexpr std::uint64_t kSubmit_rows[] = { // 3006894002872600101ull -> 18016456059351763749ull // 14061760729711229018ull -> 6161691521401770042ull // 11455159794740779404ull -> 11652988695302303084ull +// expectation corrected (INT26 v19 hash re-pin, kSubmit_reads, 6 of 6 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 5783964658104710927ull -> 18234150623076098218ull [H-THIN] +// 9790324176931117308ull -> 7528636846310271579ull [H-THIN] +// 8640216615515382983ull -> 6832312803948695488ull [H-THIN] +// 18016456059351763749ull -> 11582188679677093386ull [H-THIN] +// 6161691521401770042ull -> 13475769493769981375ull [H-THIN] +// 11652988695302303084ull -> 10681991124354674742ull [H-THIN] constexpr std::uint64_t kSubmit_reads[] = { - 5783964658104710927ull, - 9790324176931117308ull, - 8640216615515382983ull, - 18016456059351763749ull, - 6161691521401770042ull, - 11652988695302303084ull, + 18234150623076098218ull, + 7528636846310271579ull, + 6832312803948695488ull, + 11582188679677093386ull, + 13475769493769981375ull, + 10681991124354674742ull, }; // expectation corrected: kSubmit_final 4550614867809308423ull -> 6186330499779649827ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); the trades did not move. // expectation corrected (v19-E): kSubmit_final 6186330499779649827ull -> 8426887344131973464ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kSubmit_final 8426887344131973464ull -> 13033963886965614456ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kSubmit_final = 13033963886965614456ull; +// expectation corrected (INT26 v19 hash re-pin): kSubmit_final 13033963886965614456ull -> 7695240294002266835ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move. +constexpr std::uint64_t kSubmit_final = 7695240294002266835ull; constexpr Trade kSubmit_trades[] = { {1736121720000LL, 1736122260000LL, 101.5, 102, 2, 0}, {1736122080000LL, 1736122620000LL, 100, 101, 2, 0}, @@ -547,39 +588,72 @@ constexpr Trade kSubmit_trades[] = { // 1078279461778315641ull -> 4090055281926467129ull // 13355807928615532752ull -> 10973342611545588080ull // 836327192465797433ull -> 17124982032285163865ull +// expectation corrected (INT26 v19 hash re-pin, kReplace_rows, 32 of 32 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 8933562200260968840ull -> 8019365971493599225ull [H-THIN] +// 3907924323473264241ull -> 10913970432357026228ull [H-THIN] +// 14461884941578386867ull -> 16718022224308212524ull [H-THIN] +// 16511157315480471622ull -> 11959962526167016269ull [H-THIN] +// 3959552939271361991ull -> 13682103540524835918ull [H-THIN] +// 2555551956738490898ull -> 14243367393985337845ull [H-THIN] +// 10502649610878884111ull -> 8014600618664019672ull [H-THIN] +// 18306494604560422814ull -> 12889133748772252011ull [H-THIN] +// 17185448195161859406ull -> 360570764097142565ull [H-THIN] +// 15450494133515205524ull -> 11876071708761659347ull [H-THIN] +// 6827766899990563012ull -> 3851347914344196379ull [H-THIN] +// 181096906100466027ull -> 269582453071901974ull [H-THIN] +// 3679474327996900584ull -> 7374511927462938135ull [H-THIN] +// 5841986886798320038ull -> 14141590806976750289ull [H-THIN] +// 1819868140276584460ull -> 10009811972588039873ull [H-THIN] +// 4420902119403666462ull -> 6680845581363570511ull [H-THIN] +// 13480284355483224649ull -> 14019640910801526228ull [H-THIN] +// 7175317954498263753ull -> 8435723009555847478ull [H-THIN] +// 17073623445577360118ull -> 11806787700855741945ull [H-THIN] +// 17009991270755579471ull -> 4186867479979939080ull [H-THIN] +// 12090343301005594287ull -> 4211449273197151184ull [H-THIN] +// 13041638478193314581ull -> 2665513011878476638ull [H-THIN] +// 1923525511758336863ull -> 6197194006704225848ull [H-THIN] +// 1774789224381390235ull -> 9112509013863788708ull [H-THIN] +// 15509873019280549637ull -> 8040469185102305882ull [H-THIN] +// 18049355505317406500ull -> 2771278037748002351ull [H-THIN] +// 9113574443316558064ull -> 11922167282207411627ull [H-THIN] +// 52835018303832477ull -> 11390247996551829374ull [H-THIN] +// 17600220618483225632ull -> 5206095883568772047ull [H-THIN] +// 4090055281926467129ull -> 2965585984607700150ull [H-THIN] +// 10973342611545588080ull -> 14433906025888897687ull [H-THIN] +// 17124982032285163865ull -> 8339014417427264918ull [H-THIN] constexpr std::uint64_t kReplace_rows[] = { - 8933562200260968840ull, - 3907924323473264241ull, - 14461884941578386867ull, - 16511157315480471622ull, - 3959552939271361991ull, - 2555551956738490898ull, - 10502649610878884111ull, - 18306494604560422814ull, - 17185448195161859406ull, - 15450494133515205524ull, - 6827766899990563012ull, - 181096906100466027ull, - 3679474327996900584ull, - 5841986886798320038ull, - 1819868140276584460ull, - 4420902119403666462ull, - 13480284355483224649ull, - 7175317954498263753ull, - 17073623445577360118ull, - 17009991270755579471ull, - 12090343301005594287ull, - 13041638478193314581ull, - 1923525511758336863ull, - 1774789224381390235ull, - 15509873019280549637ull, - 18049355505317406500ull, - 9113574443316558064ull, - 52835018303832477ull, - 17600220618483225632ull, - 4090055281926467129ull, - 10973342611545588080ull, - 17124982032285163865ull, + 8019365971493599225ull, + 10913970432357026228ull, + 16718022224308212524ull, + 11959962526167016269ull, + 13682103540524835918ull, + 14243367393985337845ull, + 8014600618664019672ull, + 12889133748772252011ull, + 360570764097142565ull, + 11876071708761659347ull, + 3851347914344196379ull, + 269582453071901974ull, + 7374511927462938135ull, + 14141590806976750289ull, + 10009811972588039873ull, + 6680845581363570511ull, + 14019640910801526228ull, + 8435723009555847478ull, + 11806787700855741945ull, + 4186867479979939080ull, + 4211449273197151184ull, + 2665513011878476638ull, + 6197194006704225848ull, + 9112509013863788708ull, + 8040469185102305882ull, + 2771278037748002351ull, + 11922167282207411627ull, + 11390247996551829374ull, + 5206095883568772047ull, + 2965585984607700150ull, + 14433906025888897687ull, + 8339014417427264918ull, }; // expectation corrected (kReplace_reads, 8 of 8 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); the trades did not move: // 8242046592519750921ull -> 5144918420709740188ull @@ -608,20 +682,30 @@ constexpr std::uint64_t kReplace_rows[] = { // 1583090386514829653ull -> 4857350833193818325ull // 10955971990176778399ull -> 4317633836685601759ull // 5763828109735666890ull -> 1066029825997878730ull +// expectation corrected (INT26 v19 hash re-pin, kReplace_reads, 8 of 8 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 2326526227949002076ull -> 18240752413274260441ull [H-THIN] +// 13390056939270062271ull -> 2691016625363928396ull [H-THIN] +// 10021657182331917789ull -> 6810061420294520706ull [H-THIN] +// 14242637685443801194ull -> 9524522377897439163ull [H-THIN] +// 711594472462182014ull -> 7296154269922229361ull [H-THIN] +// 4857350833193818325ull -> 17144192060924016500ull [H-THIN] +// 4317633836685601759ull -> 15841551483731080184ull [H-THIN] +// 1066029825997878730ull -> 3093248295129869043ull [H-THIN] constexpr std::uint64_t kReplace_reads[] = { - 2326526227949002076ull, - 13390056939270062271ull, - 10021657182331917789ull, - 14242637685443801194ull, - 711594472462182014ull, - 4857350833193818325ull, - 4317633836685601759ull, - 1066029825997878730ull, + 18240752413274260441ull, + 2691016625363928396ull, + 6810061420294520706ull, + 9524522377897439163ull, + 7296154269922229361ull, + 17144192060924016500ull, + 15841551483731080184ull, + 3093248295129869043ull, }; // expectation corrected: kReplace_final 17912935963855979970ull -> 2578691877718696634ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); the trades did not move. // expectation corrected (v19-E): kReplace_final 2578691877718696634ull -> 836327192465797433ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kReplace_final 836327192465797433ull -> 17124982032285163865ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kReplace_final = 17124982032285163865ull; +// expectation corrected (INT26 v19 hash re-pin): kReplace_final 17124982032285163865ull -> 8339014417427264918ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move. +constexpr std::uint64_t kReplace_final = 8339014417427264918ull; constexpr Trade kReplace_trades[] = { {1736122080000LL, 1736122620000LL, 100.25, 101.25, 2, 0}, }; @@ -725,39 +809,72 @@ constexpr Trade kReplace_trades[] = { // 6317668264567744969ull -> 16001552197493914473ull // 7015387317176510425ull -> 12375923388653592921ull // 12078141763174815190ull -> 17283281126178883990ull +// expectation corrected (INT26 v19 hash re-pin, kCancel_rows, 32 of 32 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 8933562200260968840ull -> 8019365971493599225ull [H-THIN] +// 12907991694751175761ull -> 9755581588236959176ull [H-THIN] +// 15492238351520247563ull -> 8371403224338711542ull [H-THIN] +// 4949954141317803287ull -> 16042211801967482956ull [H-THIN] +// 6869043455093337913ull -> 16802932054993035276ull [H-THIN] +// 16700973384839330835ull -> 13204266665875046502ull [H-THIN] +// 13907977122257783994ull -> 16358615663034135167ull [H-THIN] +// 13690802804532944832ull -> 6541217908632789433ull [H-THIN] +// 6641415404822534272ull -> 16241388624320518101ull [H-THIN] +// 17617700982422257653ull -> 3812797098998110562ull [H-THIN] +// 8957395621418722884ull -> 18060365737908723675ull [H-THIN] +// 15533748013016881700ull -> 1812188758081721615ull [H-THIN] +// 10113927666003718936ull -> 5200369397060719481ull [H-THIN] +// 10748025209891027257ull -> 14528435239011413150ull [H-THIN] +// 7405357053482683011ull -> 16366047349482157266ull [H-THIN] +// 14210999101428183707ull -> 14462275783608418760ull [H-THIN] +// 5089886391592128332ull -> 15465277701067203145ull [H-THIN] +// 1890214210532379459ull -> 10822246540794760664ull [H-THIN] +// 15715071324930605218ull -> 5153708099721087119ull [H-THIN] +// 12760333961958415553ull -> 10351223234449594086ull [H-THIN] +// 1349906610605744215ull -> 17418701808057553558ull [H-THIN] +// 11872699637369758186ull -> 8964634856941185769ull [H-THIN] +// 6278706775858866003ull -> 8066351423005290858ull [H-THIN] +// 8489616688255836383ull -> 17339397096778631582ull [H-THIN] +// 8131243489711240877ull -> 10167217103912949158ull [H-THIN] +// 73524659065323965ull -> 7835551358700961936ull [H-THIN] +// 13120948974577109893ull -> 3057432646794745428ull [H-THIN] +// 13036340438400450584ull -> 3633706464540692363ull [H-THIN] +// 17320509982992261848ull -> 17115428998086447629ull [H-THIN] +// 16001552197493914473ull -> 6180321870109069012ull [H-THIN] +// 12375923388653592921ull -> 13623488446145227464ull [H-THIN] +// 17283281126178883990ull -> 7433824449803764975ull [H-THIN] constexpr std::uint64_t kCancel_rows[] = { - 8933562200260968840ull, - 12907991694751175761ull, - 15492238351520247563ull, - 4949954141317803287ull, - 6869043455093337913ull, - 16700973384839330835ull, - 13907977122257783994ull, - 13690802804532944832ull, - 6641415404822534272ull, - 17617700982422257653ull, - 8957395621418722884ull, - 15533748013016881700ull, - 10113927666003718936ull, - 10748025209891027257ull, - 7405357053482683011ull, - 14210999101428183707ull, - 5089886391592128332ull, - 1890214210532379459ull, - 15715071324930605218ull, - 12760333961958415553ull, - 1349906610605744215ull, - 11872699637369758186ull, - 6278706775858866003ull, - 8489616688255836383ull, - 8131243489711240877ull, - 73524659065323965ull, - 13120948974577109893ull, - 13036340438400450584ull, - 17320509982992261848ull, - 16001552197493914473ull, - 12375923388653592921ull, - 17283281126178883990ull, + 8019365971493599225ull, + 9755581588236959176ull, + 8371403224338711542ull, + 16042211801967482956ull, + 16802932054993035276ull, + 13204266665875046502ull, + 16358615663034135167ull, + 6541217908632789433ull, + 16241388624320518101ull, + 3812797098998110562ull, + 18060365737908723675ull, + 1812188758081721615ull, + 5200369397060719481ull, + 14528435239011413150ull, + 16366047349482157266ull, + 14462275783608418760ull, + 15465277701067203145ull, + 10822246540794760664ull, + 5153708099721087119ull, + 10351223234449594086ull, + 17418701808057553558ull, + 8964634856941185769ull, + 8066351423005290858ull, + 17339397096778631582ull, + 10167217103912949158ull, + 7835551358700961936ull, + 3057432646794745428ull, + 3633706464540692363ull, + 17115428998086447629ull, + 6180321870109069012ull, + 13623488446145227464ull, + 7433824449803764975ull, }; // expectation corrected (kCancel_reads, 19 of 19 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); the trades did not move: // 11036775732707302117ull -> 4251966616960422708ull @@ -819,31 +936,52 @@ constexpr std::uint64_t kCancel_rows[] = { // 332263738452834604ull -> 6330997617635358220ull // 11019138486722009680ull -> 7622013860668800144ull // 1811755191633478047ull -> 7135414897549443327ull +// expectation corrected (INT26 v19 hash re-pin, kCancel_reads, 19 of 19 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 5066383747043332732ull -> 16017138806894645973ull [H-THIN] +// 14642164039413495816ull -> 14395189169447138845ull [H-THIN] +// 1826995519720684135ull -> 15433550368465758174ull [H-THIN] +// 10600767793424807315ull -> 15319181961208510706ull [H-THIN] +// 18413531801076714261ull -> 7116101502612005176ull [H-THIN] +// 7162595064832704878ull -> 5624598517067648741ull [H-THIN] +// 8771487694918498925ull -> 15626337877376063418ull [H-THIN] +// 3727599241559000403ull -> 12303175751506120794ull [H-THIN] +// 2036989596869214901ull -> 16768996190976851778ull [H-THIN] +// 15857207205534857561ull -> 9166644978234881348ull [H-THIN] +// 5509984618602967315ull -> 18107466278445992216ull [H-THIN] +// 227260307711721321ull -> 512994913100055200ull [H-THIN] +// 14543777879962894095ull -> 9443988914314833904ull [H-THIN] +// 2123158589903514105ull -> 15301609864008799700ull [H-THIN] +// 11488931589024505587ull -> 18376839094291697164ull [H-THIN] +// 6035902334985171665ull -> 3352239734550209384ull [H-THIN] +// 6330997617635358220ull -> 9556582455608271345ull [H-THIN] +// 7622013860668800144ull -> 4597279093679219283ull [H-THIN] +// 7135414897549443327ull -> 12371316346718768834ull [H-THIN] constexpr std::uint64_t kCancel_reads[] = { - 5066383747043332732ull, - 14642164039413495816ull, - 1826995519720684135ull, - 10600767793424807315ull, - 18413531801076714261ull, - 7162595064832704878ull, - 8771487694918498925ull, - 3727599241559000403ull, - 2036989596869214901ull, - 15857207205534857561ull, - 5509984618602967315ull, - 227260307711721321ull, - 14543777879962894095ull, - 2123158589903514105ull, - 11488931589024505587ull, - 6035902334985171665ull, - 6330997617635358220ull, - 7622013860668800144ull, - 7135414897549443327ull, + 16017138806894645973ull, + 14395189169447138845ull, + 15433550368465758174ull, + 15319181961208510706ull, + 7116101502612005176ull, + 5624598517067648741ull, + 15626337877376063418ull, + 12303175751506120794ull, + 16768996190976851778ull, + 9166644978234881348ull, + 18107466278445992216ull, + 512994913100055200ull, + 9443988914314833904ull, + 15301609864008799700ull, + 18376839094291697164ull, + 3352239734550209384ull, + 9556582455608271345ull, + 4597279093679219283ull, + 12371316346718768834ull, }; // expectation corrected: kCancel_final 547718020446064204ull -> 12693965216151243664ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); the trades did not move. // expectation corrected (v19-E): kCancel_final 12693965216151243664ull -> 12078141763174815190ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kCancel_final 12078141763174815190ull -> 17283281126178883990ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kCancel_final = 17283281126178883990ull; +// expectation corrected (INT26 v19 hash re-pin): kCancel_final 17283281126178883990ull -> 7433824449803764975ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move. +constexpr std::uint64_t kCancel_final = 7433824449803764975ull; constexpr Trade kCancel_trades[] = { {1736122980000LL, 1736123340000LL, 101, 102, 2, 0}, }; @@ -947,39 +1085,72 @@ constexpr Trade kCancel_trades[] = { // 312036520819661464ull -> 12667708910662510744ull // 15975581040180058350ull -> 17116188563837257134ull // 15465526605242472570ull -> 18394579596403092954ull +// expectation corrected (INT26 v19 hash re-pin, kOca_rows, 32 of 32 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 8933562200260968840ull -> 8019365971493599225ull [H-THIN] +// 16488569157039504391ull -> 3232936220238691146ull [H-THIN] +// 15308668032647221442ull -> 8093402965097552867ull [H-THIN] +// 11025020666800289870ull -> 16342776694917511399ull [H-THIN] +// 17376859899643293076ull -> 1176409931075103109ull [H-THIN] +// 18031995174781967338ull -> 14796994139400300899ull [H-THIN] +// 9135991105341529701ull -> 4041329064345231230ull [H-THIN] +// 12521501026388244289ull -> 18203940660042607242ull [H-THIN] +// 17387999442892602416ull -> 11789920335390250025ull [H-THIN] +// 17523447275006301316ull -> 12162719525992402073ull [H-THIN] +// 700544763663270973ull -> 10376658214325705912ull [H-THIN] +// 6419555011776275829ull -> 985387310022141304ull [H-THIN] +// 9501522493702417732ull -> 11563487340980545165ull [H-THIN] +// 2198037920276336707ull -> 17750573796976151050ull [H-THIN] +// 3899965171878021222ull -> 8782861391928203675ull [H-THIN] +// 17400108061693215989ull -> 148042859240194154ull [H-THIN] +// 6268695127500094535ull -> 2639689295090791304ull [H-THIN] +// 15386469441856039635ull -> 10798952018967388820ull [H-THIN] +// 1827308857775093108ull -> 13015585966305206737ull [H-THIN] +// 9002315039531479122ull -> 619093614529828615ull [H-THIN] +// 4336820592134659556ull -> 16432223468538835037ull [H-THIN] +// 1568941454279137340ull -> 5709266478406133937ull [H-THIN] +// 3586528324041147724ull -> 11031153128114419525ull [H-THIN] +// 9921466732535290906ull -> 17759043185727148801ull [H-THIN] +// 2210251816763789972ull -> 778153452226410841ull [H-THIN] +// 12540125623729258183ull -> 6929057138192640130ull [H-THIN] +// 4922139678029842511ull -> 1014477579839442850ull [H-THIN] +// 14880777043208119420ull -> 14398065235087572465ull [H-THIN] +// 766085567512147215ull -> 10116121127485637382ull [H-THIN] +// 12667708910662510744ull -> 9104659107751229673ull [H-THIN] +// 17116188563837257134ull -> 12444616329691358185ull [H-THIN] +// 18394579596403092954ull -> 14761567357674824267ull [H-THIN] constexpr std::uint64_t kOca_rows[] = { - 8933562200260968840ull, - 16488569157039504391ull, - 15308668032647221442ull, - 11025020666800289870ull, - 17376859899643293076ull, - 18031995174781967338ull, - 9135991105341529701ull, - 12521501026388244289ull, - 17387999442892602416ull, - 17523447275006301316ull, - 700544763663270973ull, - 6419555011776275829ull, - 9501522493702417732ull, - 2198037920276336707ull, - 3899965171878021222ull, - 17400108061693215989ull, - 6268695127500094535ull, - 15386469441856039635ull, - 1827308857775093108ull, - 9002315039531479122ull, - 4336820592134659556ull, - 1568941454279137340ull, - 3586528324041147724ull, - 9921466732535290906ull, - 2210251816763789972ull, - 12540125623729258183ull, - 4922139678029842511ull, - 14880777043208119420ull, - 766085567512147215ull, - 12667708910662510744ull, - 17116188563837257134ull, - 18394579596403092954ull, + 8019365971493599225ull, + 3232936220238691146ull, + 8093402965097552867ull, + 16342776694917511399ull, + 1176409931075103109ull, + 14796994139400300899ull, + 4041329064345231230ull, + 18203940660042607242ull, + 11789920335390250025ull, + 12162719525992402073ull, + 10376658214325705912ull, + 985387310022141304ull, + 11563487340980545165ull, + 17750573796976151050ull, + 8782861391928203675ull, + 148042859240194154ull, + 2639689295090791304ull, + 10798952018967388820ull, + 13015585966305206737ull, + 619093614529828615ull, + 16432223468538835037ull, + 5709266478406133937ull, + 11031153128114419525ull, + 17759043185727148801ull, + 778153452226410841ull, + 6929057138192640130ull, + 1014477579839442850ull, + 14398065235087572465ull, + 10116121127485637382ull, + 9104659107751229673ull, + 12444616329691358185ull, + 14761567357674824267ull, }; // expectation corrected (kOca_reads, 12 of 12 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); the trades did not move: // 14283998503469032055ull -> 10089579219758468484ull @@ -1020,24 +1191,38 @@ constexpr std::uint64_t kOca_rows[] = { // 12870434045680047141ull -> 3016108360062111013ull // 5966302497310935881ull -> 14134368882450491401ull // 5749065519581012093ull -> 12755516166577050941ull +// expectation corrected (INT26 v19 hash re-pin, kOca_reads, 12 of 12 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 3105752370505168555ull -> 15189525287878030606ull [H-THIN] +// 13318576850138903270ull -> 12039483643790541963ull [H-THIN] +// 4943429593008048082ull -> 16889317257125145965ull [H-THIN] +// 7900987300136689534ull -> 14061180146549677313ull [H-THIN] +// 16889527581053605563ull -> 3457726981161275090ull [H-THIN] +// 8561664608116353912ull -> 15838028967223682041ull [H-THIN] +// 10377670066927504148ull -> 11019583341156392335ull [H-THIN] +// 14341448323725690491ull -> 15037407705806185768ull [H-THIN] +// 9080486838152177794ull -> 17212813844302948315ull [H-THIN] +// 3016108360062111013ull -> 8321492686618048122ull [H-THIN] +// 14134368882450491401ull -> 12537836709650543420ull [H-THIN] +// 12755516166577050941ull -> 5737128231549608782ull [H-THIN] constexpr std::uint64_t kOca_reads[] = { - 3105752370505168555ull, - 13318576850138903270ull, - 4943429593008048082ull, - 7900987300136689534ull, - 16889527581053605563ull, - 8561664608116353912ull, - 10377670066927504148ull, - 14341448323725690491ull, - 9080486838152177794ull, - 3016108360062111013ull, - 14134368882450491401ull, - 12755516166577050941ull, + 15189525287878030606ull, + 12039483643790541963ull, + 16889317257125145965ull, + 14061180146549677313ull, + 3457726981161275090ull, + 15838028967223682041ull, + 11019583341156392335ull, + 15037407705806185768ull, + 17212813844302948315ull, + 8321492686618048122ull, + 12537836709650543420ull, + 5737128231549608782ull, }; // expectation corrected: kOca_final 13344165461917220241ull -> 14677771083385306600ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); the trades did not move. // expectation corrected (v19-E): kOca_final 14677771083385306600ull -> 15465526605242472570ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kOca_final 15465526605242472570ull -> 18394579596403092954ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kOca_final = 18394579596403092954ull; +// expectation corrected (INT26 v19 hash re-pin): kOca_final 18394579596403092954ull -> 14761567357674824267ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move. +constexpr std::uint64_t kOca_final = 14761567357674824267ull; constexpr Trade kOca_trades[] = { {1736121960000LL, 1736122020000LL, 100.75, 100.75, 2, 0}, {1736122500000LL, 1736122980000LL, 100.5, 101, 2, 0}, @@ -1144,39 +1329,72 @@ constexpr Trade kOca_trades[] = { // 13079457144551358245ull -> 8782474388336378853ull // 10683567794089412281ull -> 12830062157342008889ull // 14699793179829041832ull -> 11660101618206053704ull +// expectation corrected (INT26 v19 hash re-pin, kReissue_rows, 32 of 32 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 11371589716169913082ull -> 5794085833535179433ull [H-THIN] +// 11912421295804525448ull -> 10206102349587901947ull [H-THIN] +// 16162904808422149189ull -> 16740711338258568218ull [H-THIN] +// 1884050390405525809ull -> 157145938453825824ull [H-THIN] +// 1630941943368694258ull -> 11549520461694658189ull [H-THIN] +// 1646315788396378413ull -> 375980444520009614ull [H-THIN] +// 3202536020749475123ull -> 1364356711154384662ull [H-THIN] +// 16739146824408715591ull -> 9267951354912006062ull [H-THIN] +// 12465965272475823462ull -> 18013247113906258621ull [H-THIN] +// 12416019024705234276ull -> 15135278226663906553ull [H-THIN] +// 8019931705879186711ull -> 6395419845332150882ull [H-THIN] +// 10511748891040354060ull -> 2608642583221907887ull [H-THIN] +// 7873647981647748428ull -> 1439014296977039245ull [H-THIN] +// 13583506357494126788ull -> 4897379921696966153ull [H-THIN] +// 6827456748487052757ull -> 13502194017489163244ull [H-THIN] +// 4860432548138722281ull -> 8254807225619272358ull [H-THIN] +// 10318257384229209728ull -> 4041173354564745731ull [H-THIN] +// 13270746490627165070ull -> 5823046275575919511ull [H-THIN] +// 15647754736958033867ull -> 11150667084723241358ull [H-THIN] +// 15257413490644512113ull -> 7066649565605271382ull [H-THIN] +// 3422115173668721064ull -> 9147117666804548605ull [H-THIN] +// 11954333827210587376ull -> 2943969262508479583ull [H-THIN] +// 16717809947662223375ull -> 2242050686395926078ull [H-THIN] +// 16431286081784348505ull -> 18297932585058907168ull [H-THIN] +// 14674774925924029409ull -> 7328278135585233712ull [H-THIN] +// 8888789072620710620ull -> 4496911868369281609ull [H-THIN] +// 7677274857978420999ull -> 4092312770444862044ull [H-THIN] +// 8389711543599766598ull -> 3103138085614071329ull [H-THIN] +// 11866583660361571056ull -> 1640897772442143723ull [H-THIN] +// 8782474388336378853ull -> 11067585398331222410ull [H-THIN] +// 12830062157342008889ull -> 358041756474409844ull [H-THIN] +// 11660101618206053704ull -> 8735900797794446825ull [H-THIN] constexpr std::uint64_t kReissue_rows[] = { - 11371589716169913082ull, - 11912421295804525448ull, - 16162904808422149189ull, - 1884050390405525809ull, - 1630941943368694258ull, - 1646315788396378413ull, - 3202536020749475123ull, - 16739146824408715591ull, - 12465965272475823462ull, - 12416019024705234276ull, - 8019931705879186711ull, - 10511748891040354060ull, - 7873647981647748428ull, - 13583506357494126788ull, - 6827456748487052757ull, - 4860432548138722281ull, - 10318257384229209728ull, - 13270746490627165070ull, - 15647754736958033867ull, - 15257413490644512113ull, - 3422115173668721064ull, - 11954333827210587376ull, - 16717809947662223375ull, - 16431286081784348505ull, - 14674774925924029409ull, - 8888789072620710620ull, - 7677274857978420999ull, - 8389711543599766598ull, - 11866583660361571056ull, - 8782474388336378853ull, - 12830062157342008889ull, - 11660101618206053704ull, + 5794085833535179433ull, + 10206102349587901947ull, + 16740711338258568218ull, + 157145938453825824ull, + 11549520461694658189ull, + 375980444520009614ull, + 1364356711154384662ull, + 9267951354912006062ull, + 18013247113906258621ull, + 15135278226663906553ull, + 6395419845332150882ull, + 2608642583221907887ull, + 1439014296977039245ull, + 4897379921696966153ull, + 13502194017489163244ull, + 8254807225619272358ull, + 4041173354564745731ull, + 5823046275575919511ull, + 11150667084723241358ull, + 7066649565605271382ull, + 9147117666804548605ull, + 2943969262508479583ull, + 2242050686395926078ull, + 18297932585058907168ull, + 7328278135585233712ull, + 4496911868369281609ull, + 4092312770444862044ull, + 3103138085614071329ull, + 1640897772442143723ull, + 11067585398331222410ull, + 358041756474409844ull, + 8735900797794446825ull, }; // expectation corrected (kReissue_reads, 22 of 22 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); the trades did not move: // 3939475029491512578ull -> 12411711249896042591ull @@ -1247,34 +1465,58 @@ constexpr std::uint64_t kReissue_rows[] = { // 12085354800256371026ull -> 15829080338314944658ull // 13244348858005104150ull -> 2272828286921366934ull // 1758314424234447340ull -> 16041091954465179468ull +// expectation corrected (INT26 v19 hash re-pin, kReissue_reads, 22 of 22 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 9397758857253294369ull -> 15035498073180564574ull [H-THIN] +// 14952922796737447431ull -> 6351174026430191720ull [H-THIN] +// 350142526787915315ull -> 17029808830897663162ull [H-THIN] +// 9287618455889763760ull -> 1960056261526077029ull [H-THIN] +// 8049051401679342672ull -> 9359327854260104443ull [H-THIN] +// 7334056035772970331ull -> 9193727488393939674ull [H-THIN] +// 16777030856659722344ull -> 2562235961360585845ull [H-THIN] +// 12861194332908904879ull -> 15091373470769183170ull [H-THIN] +// 7839065249474270945ull -> 9066532592892510086ull [H-THIN] +// 2385401613699651006ull -> 15993610558080257077ull [H-THIN] +// 3151001040546167918ull -> 14590383768983463647ull [H-THIN] +// 6768266663339224688ull -> 14847391113272722757ull [H-THIN] +// 211423388548179255ull -> 9991682699480081944ull [H-THIN] +// 10932414881536382884ull -> 7606589495236393781ull [H-THIN] +// 15509429580401113050ull -> 16170401377191376471ull [H-THIN] +// 272542696402235168ull -> 8660956339548506257ull [H-THIN] +// 12650608718358639686ull -> 17474680937306256469ull [H-THIN] +// 16954864996066484000ull -> 14583099152188767967ull [H-THIN] +// 2072712234614561342ull -> 1043496941119894989ull [H-THIN] +// 15829080338314944658ull -> 13855326176576914141ull [H-THIN] +// 2272828286921366934ull -> 10342641769074306343ull [H-THIN] +// 16041091954465179468ull -> 11480238464600989237ull [H-THIN] constexpr std::uint64_t kReissue_reads[] = { - 9397758857253294369ull, - 14952922796737447431ull, - 350142526787915315ull, - 9287618455889763760ull, - 8049051401679342672ull, - 7334056035772970331ull, - 16777030856659722344ull, - 12861194332908904879ull, - 7839065249474270945ull, - 2385401613699651006ull, - 3151001040546167918ull, - 6768266663339224688ull, - 211423388548179255ull, - 10932414881536382884ull, - 15509429580401113050ull, - 272542696402235168ull, - 12650608718358639686ull, - 16954864996066484000ull, - 2072712234614561342ull, - 15829080338314944658ull, - 2272828286921366934ull, - 16041091954465179468ull, + 15035498073180564574ull, + 6351174026430191720ull, + 17029808830897663162ull, + 1960056261526077029ull, + 9359327854260104443ull, + 9193727488393939674ull, + 2562235961360585845ull, + 15091373470769183170ull, + 9066532592892510086ull, + 15993610558080257077ull, + 14590383768983463647ull, + 14847391113272722757ull, + 9991682699480081944ull, + 7606589495236393781ull, + 16170401377191376471ull, + 8660956339548506257ull, + 17474680937306256469ull, + 14583099152188767967ull, + 1043496941119894989ull, + 13855326176576914141ull, + 10342641769074306343ull, + 11480238464600989237ull, }; // expectation corrected: kReissue_final 10597617675334596827ull -> 5214787630894246342ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); the trades did not move. // expectation corrected (v19-E): kReissue_final 5214787630894246342ull -> 14699793179829041832ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kReissue_final 14699793179829041832ull -> 11660101618206053704ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kReissue_final = 11660101618206053704ull; +// expectation corrected (INT26 v19 hash re-pin): kReissue_final 11660101618206053704ull -> 8735900797794446825ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move. +constexpr std::uint64_t kReissue_final = 8735900797794446825ull; constexpr Trade kReissue_trades[] = { {1736121660000LL, 1736123040000LL, 100.75, 99.75, 2, 0}, {1736123100000LL, 1736123460000LL, 100.75, 101, 2, 1}, @@ -1379,39 +1621,72 @@ constexpr Trade kReissue_trades[] = { // 15862752879538973131ull -> 17629340520421506187ull // 9351225767820645871ull -> 12781001967037913295ull // 11018457759974752048ull -> 4953750954703410960ull +// expectation corrected (INT26 v19 hash re-pin, kTrail_rows, 32 of 32 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 11371589716169913082ull -> 5794085833535179433ull [H-THIN] +// 15827480248058921916ull -> 2370845579388248975ull [H-THIN] +// 9179483745920742590ull -> 14433248242147310045ull [H-THIN] +// 6778316597629456467ull -> 6746208057179709300ull [H-THIN] +// 5172328729500611739ull -> 5141474228715449514ull [H-THIN] +// 538732652451172654ull -> 11607255782218151389ull [H-THIN] +// 6878958150024727018ull -> 15859825369433213587ull [H-THIN] +// 8673040670875734808ull -> 14000636333325119541ull [H-THIN] +// 1224102971077480171ull -> 8235513201704559620ull [H-THIN] +// 4557268190807619538ull -> 896476467432039189ull [H-THIN] +// 9136468491734931429ull -> 8318862700865263670ull [H-THIN] +// 6184955078515723301ull -> 7174198796690634212ull [H-THIN] +// 12362884735948053375ull -> 14047279371931353558ull [H-THIN] +// 15431697961609933942ull -> 10303898847732478779ull [H-THIN] +// 1411625524442821644ull -> 10919087129373839423ull [H-THIN] +// 12832334111417447482ull -> 2392471918185386717ull [H-THIN] +// 5598316292658291609ull -> 13975657224456405630ull [H-THIN] +// 1094550630882239090ull -> 5371073164686480833ull [H-THIN] +// 9032605003773573902ull -> 5788193476615953481ull [H-THIN] +// 655750707938363819ull -> 742735857153964210ull [H-THIN] +// 11787846682663979157ull -> 9511387276849347064ull [H-THIN] +// 4271553443345200153ull -> 8387023980895655564ull [H-THIN] +// 13505617900634614461ull -> 12129447825700318114ull [H-THIN] +// 14516777496449283573ull -> 6819473365388645714ull [H-THIN] +// 8741802295233492767ull -> 177891932096105070ull [H-THIN] +// 7191771264792402823ull -> 12347931419050467504ull [H-THIN] +// 407930561309081398ull -> 11096564370558447981ull [H-THIN] +// 10139811205313313301ull -> 5083341745670325276ull [H-THIN] +// 9692372728761498854ull -> 15979402051208842361ull [H-THIN] +// 17629340520421506187ull -> 10031744998823385352ull [H-THIN] +// 12781001967037913295ull -> 2405952843925529234ull [H-THIN] +// 4953750954703410960ull -> 15933138127006431029ull [H-THIN] constexpr std::uint64_t kTrail_rows[] = { - 11371589716169913082ull, - 15827480248058921916ull, - 9179483745920742590ull, - 6778316597629456467ull, - 5172328729500611739ull, - 538732652451172654ull, - 6878958150024727018ull, - 8673040670875734808ull, - 1224102971077480171ull, - 4557268190807619538ull, - 9136468491734931429ull, - 6184955078515723301ull, - 12362884735948053375ull, - 15431697961609933942ull, - 1411625524442821644ull, - 12832334111417447482ull, - 5598316292658291609ull, - 1094550630882239090ull, - 9032605003773573902ull, - 655750707938363819ull, - 11787846682663979157ull, - 4271553443345200153ull, - 13505617900634614461ull, - 14516777496449283573ull, - 8741802295233492767ull, - 7191771264792402823ull, - 407930561309081398ull, - 10139811205313313301ull, - 9692372728761498854ull, - 17629340520421506187ull, - 12781001967037913295ull, - 4953750954703410960ull, + 5794085833535179433ull, + 2370845579388248975ull, + 14433248242147310045ull, + 6746208057179709300ull, + 5141474228715449514ull, + 11607255782218151389ull, + 15859825369433213587ull, + 14000636333325119541ull, + 8235513201704559620ull, + 896476467432039189ull, + 8318862700865263670ull, + 7174198796690634212ull, + 14047279371931353558ull, + 10303898847732478779ull, + 10919087129373839423ull, + 2392471918185386717ull, + 13975657224456405630ull, + 5371073164686480833ull, + 5788193476615953481ull, + 742735857153964210ull, + 9511387276849347064ull, + 8387023980895655564ull, + 12129447825700318114ull, + 6819473365388645714ull, + 177891932096105070ull, + 12347931419050467504ull, + 11096564370558447981ull, + 5083341745670325276ull, + 15979402051208842361ull, + 10031744998823385352ull, + 2405952843925529234ull, + 15933138127006431029ull, }; // expectation corrected (kTrail_reads, 2 of 2 values), because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); the trades did not move: // 3939475029491512578ull -> 12411711249896042591ull @@ -1422,14 +1697,18 @@ constexpr std::uint64_t kTrail_rows[] = { // expectation corrected (V19-FIX, kTrail_reads, 2 of 2 values), because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree: // 14558537749344697025ull -> 9397758857253294369ull // 12264623362903320190ull -> 8570208265068282686ull +// expectation corrected (INT26 v19 hash re-pin, kTrail_reads, 2 of 2 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move: +// 9397758857253294369ull -> 15035498073180564574ull [H-THIN] +// 8570208265068282686ull -> 2064235671204825645ull [H-THIN] constexpr std::uint64_t kTrail_reads[] = { - 9397758857253294369ull, - 8570208265068282686ull, + 15035498073180564574ull, + 2064235671204825645ull, }; // expectation corrected: kTrail_final 2322910976412840875ull -> 11930736200467274982ull, because v19 folds the continuation over live state word-wise (native-consumer/v9) and the broker-state hash folds a running closed-row digest (pineforge-broker-state/v19); the trades did not move. // expectation corrected (v19-E): kTrail_final 11930736200467274982ull -> 11018457759974752048ull, because v19-E folds live adapter state (pineforge-source-adapter/v4: retired placement rows are erased, the append-only logs fold as running digests; pineforge-pine-scheduler/v3); the trades did not move; harvested with its switch against main 10f20197 (every old pin reproduced), this tree and V19-E's tip 6e97f272 (identical rows). // expectation corrected (V19-FIX): kTrail_final 11018457759974752048ull -> 4953750954703410960ull, because R5 lane V19-FIX moves the Pine hash values once, inside v19: the dead PineExecutionAdapter::path_order_ no longer folds into pineforge-source-adapter/v4, a script cancel retires every leg of each exit it names and K1 lets a withdrawn leg go, and an origin that can no longer be bound leaves its kernel cohort roster; the trades did not move; harvested with this TU's harvest switch against main 91d65ad6 (every old pin reproduced) and the lane's tree. -constexpr std::uint64_t kTrail_final = 4953750954703410960ull; +// expectation corrected (INT26 v19 hash re-pin): kTrail_final 4953750954703410960ull -> 15933138127006431029ull [H-THIN], because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the trades did not move. +constexpr std::uint64_t kTrail_final = 15933138127006431029ull; constexpr Trade kTrail_trades[] = { {1736121660000LL, 1736121840000LL, 100.25, 100.75, 2, 0}, }; diff --git a/tests/test_adapter_report_relower.cpp b/tests/test_adapter_report_relower.cpp index 83348973..d9449c8b 100644 --- a/tests/test_adapter_report_relower.cpp +++ b/tests/test_adapter_report_relower.cpp @@ -333,31 +333,56 @@ constexpr Point kOrdinary_curve[] = { // 17070412854470623372ull -> 9654818568811123148ull // 5698741489304391014ull -> 7514399512975361670ull // 8249112969542764486ull -> 375912627396258150ull +// expectation corrected (INT26 v19 hash re-pin, kOrdinary_hashes, 24 of 24 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the curves, rows and marks did not move: +// 10420073189004151623ull -> 243601838968427296ull [H-THIN] +// 13145042526267837939ull -> 14082045497352522076ull [H-THIN] +// 2622782777793463991ull -> 16788444728701273278ull [H-THIN] +// 17996446729054892726ull -> 4735244315032400709ull [H-THIN] +// 3969181056128787992ull -> 9274703563551556229ull [H-THIN] +// 16947117433974038ull -> 4437548837203718331ull [H-THIN] +// 423735939073054907ull -> 9447280110204500336ull [H-THIN] +// 18056612604689961163ull -> 644150700710845644ull [H-THIN] +// 12445011709498043361ull -> 12856503234686701952ull [H-THIN] +// 16382730376298748267ull -> 8228843622305023308ull [H-THIN] +// 12718112296054108390ull -> 11723584407685805665ull [H-THIN] +// 913929622865489206ull -> 12427688725613086017ull [H-THIN] +// 13218761675008378229ull -> 5717024028566739016ull [H-THIN] +// 10388057217060506235ull -> 9136288938210272546ull [H-THIN] +// 4373273430774112306ull -> 14265105010801500849ull [H-THIN] +// 11946915320520331422ull -> 5913118032193444201ull [H-THIN] +// 2472677882334572745ull -> 2601700310587933770ull [H-THIN] +// 4709222664471492344ull -> 16710877784426826175ull [H-THIN] +// 5785035152764109816ull -> 12222101465241554407ull [H-THIN] +// 15834701310167426825ull -> 1466897115669479138ull [H-THIN] +// 18416054639860847807ull -> 8818039625616895170ull [H-THIN] +// 9654818568811123148ull -> 17382764399684682567ull [H-THIN] +// 7514399512975361670ull -> 5569220102658162789ull [H-THIN] +// 375912627396258150ull -> 5858716210019128261ull [H-THIN] constexpr std::uint64_t kOrdinary_hashes[] = { - 10420073189004151623ull, - 13145042526267837939ull, - 2622782777793463991ull, - 17996446729054892726ull, - 3969181056128787992ull, - 16947117433974038ull, - 423735939073054907ull, - 18056612604689961163ull, - 12445011709498043361ull, - 16382730376298748267ull, - 12718112296054108390ull, - 913929622865489206ull, - 13218761675008378229ull, - 10388057217060506235ull, - 4373273430774112306ull, - 11946915320520331422ull, - 2472677882334572745ull, - 4709222664471492344ull, - 5785035152764109816ull, - 15834701310167426825ull, - 18416054639860847807ull, - 9654818568811123148ull, - 7514399512975361670ull, - 375912627396258150ull, + 243601838968427296ull, + 14082045497352522076ull, + 16788444728701273278ull, + 4735244315032400709ull, + 9274703563551556229ull, + 4437548837203718331ull, + 9447280110204500336ull, + 644150700710845644ull, + 12856503234686701952ull, + 8228843622305023308ull, + 11723584407685805665ull, + 12427688725613086017ull, + 5717024028566739016ull, + 9136288938210272546ull, + 14265105010801500849ull, + 5913118032193444201ull, + 2601700310587933770ull, + 16710877784426826175ull, + 12222101465241554407ull, + 1466897115669479138ull, + 8818039625616895170ull, + 17382764399684682567ull, + 5569220102658162789ull, + 5858716210019128261ull, }; constexpr Row kOrdinary_rows[] = { {1736121780000LL, 1736122140000LL, 101.5, 100.5, 2, -6, 4, 0.5, 5, 1, 0}, @@ -470,31 +495,56 @@ constexpr Point kCalcOnOrderFills_curve[] = { // 12518209463246424427ull -> 1572183010617309931ull // 2102874898517593241ull -> 6980933373957006777ull // 7311762479881515305ull -> 3071340150260882121ull +// expectation corrected (INT26 v19 hash re-pin, kCalcOnOrderFills_hashes, 24 of 24 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the curves, rows and marks did not move: +// 13492117694829868536ull -> 8076983691197291309ull [H-THIN] +// 12939649384498536620ull -> 2440556825337174781ull [H-THIN] +// 18283200234405460746ull -> 17660598049729719549ull [H-THIN] +// 17715026773738590286ull -> 584654546328644269ull [H-THIN] +// 16652393141204816958ull -> 10522330026569309375ull [H-THIN] +// 10681558687161056850ull -> 2930399316675077519ull [H-THIN] +// 12828138156283959725ull -> 17808710347154107934ull [H-THIN] +// 13337213902076129151ull -> 6715617711192628536ull [H-THIN] +// 2897666538282385134ull -> 6587502700890907205ull [H-THIN] +// 4846519892874967202ull -> 2192689119498288561ull [H-THIN] +// 4182996206551988888ull -> 8664008431780515081ull [H-THIN] +// 3828630408241748525ull -> 7930584285132978374ull [H-THIN] +// 6013095397646776827ull -> 17064940335416518660ull [H-THIN] +// 1201561623500117444ull -> 9016627554631535731ull [H-THIN] +// 16339691134963647305ull -> 16088314143365594964ull [H-THIN] +// 1360168785605943262ull -> 13783309705914047335ull [H-THIN] +// 10446097969456318121ull -> 9827199985728418388ull [H-THIN] +// 2226082304527453549ull -> 16177976828038662588ull [H-THIN] +// 5506525745160110349ull -> 12444021166412526756ull [H-THIN] +// 16314828352609565244ull -> 12728919766761920669ull [H-THIN] +// 14800719633294876469ull -> 17328862593452428978ull [H-THIN] +// 1572183010617309931ull -> 565050300121405896ull [H-THIN] +// 6980933373957006777ull -> 12671085486866481742ull [H-THIN] +// 3071340150260882121ull -> 9604569239120914630ull [H-THIN] constexpr std::uint64_t kCalcOnOrderFills_hashes[] = { - 13492117694829868536ull, - 12939649384498536620ull, - 18283200234405460746ull, - 17715026773738590286ull, - 16652393141204816958ull, - 10681558687161056850ull, - 12828138156283959725ull, - 13337213902076129151ull, - 2897666538282385134ull, - 4846519892874967202ull, - 4182996206551988888ull, - 3828630408241748525ull, - 6013095397646776827ull, - 1201561623500117444ull, - 16339691134963647305ull, - 1360168785605943262ull, - 10446097969456318121ull, - 2226082304527453549ull, - 5506525745160110349ull, - 16314828352609565244ull, - 14800719633294876469ull, - 1572183010617309931ull, - 6980933373957006777ull, - 3071340150260882121ull, + 8076983691197291309ull, + 2440556825337174781ull, + 17660598049729719549ull, + 584654546328644269ull, + 10522330026569309375ull, + 2930399316675077519ull, + 17808710347154107934ull, + 6715617711192628536ull, + 6587502700890907205ull, + 2192689119498288561ull, + 8664008431780515081ull, + 7930584285132978374ull, + 17064940335416518660ull, + 9016627554631535731ull, + 16088314143365594964ull, + 13783309705914047335ull, + 9827199985728418388ull, + 16177976828038662588ull, + 12444021166412526756ull, + 12728919766761920669ull, + 17328862593452428978ull, + 565050300121405896ull, + 12671085486866481742ull, + 9604569239120914630ull, }; constexpr Row kCalcOnOrderFills_rows[] = { {1736121780000LL, 1736122080000LL, 101.5, 100.5, 2, -6, 4, 0.5, 4.5, 1, 0}, @@ -607,31 +657,56 @@ constexpr Point kSuppressedTail_curve[] = { // 17070412854470623372ull -> 9654818568811123148ull // 5698741489304391014ull -> 7514399512975361670ull // 1169937375292956878ull -> 7096719386424781198ull +// expectation corrected (INT26 v19 hash re-pin, kSuppressedTail_hashes, 24 of 24 values), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the curves, rows and marks did not move: +// 10420073189004151623ull -> 243601838968427296ull [H-THIN] +// 13145042526267837939ull -> 14082045497352522076ull [H-THIN] +// 2622782777793463991ull -> 16788444728701273278ull [H-THIN] +// 17996446729054892726ull -> 4735244315032400709ull [H-THIN] +// 3969181056128787992ull -> 9274703563551556229ull [H-THIN] +// 16947117433974038ull -> 4437548837203718331ull [H-THIN] +// 423735939073054907ull -> 9447280110204500336ull [H-THIN] +// 18056612604689961163ull -> 644150700710845644ull [H-THIN] +// 12445011709498043361ull -> 12856503234686701952ull [H-THIN] +// 16382730376298748267ull -> 8228843622305023308ull [H-THIN] +// 12718112296054108390ull -> 11723584407685805665ull [H-THIN] +// 913929622865489206ull -> 12427688725613086017ull [H-THIN] +// 13218761675008378229ull -> 5717024028566739016ull [H-THIN] +// 10388057217060506235ull -> 9136288938210272546ull [H-THIN] +// 4373273430774112306ull -> 14265105010801500849ull [H-THIN] +// 11946915320520331422ull -> 5913118032193444201ull [H-THIN] +// 2472677882334572745ull -> 2601700310587933770ull [H-THIN] +// 4709222664471492344ull -> 16710877784426826175ull [H-THIN] +// 5785035152764109816ull -> 12222101465241554407ull [H-THIN] +// 15834701310167426825ull -> 1466897115669479138ull [H-THIN] +// 18416054639860847807ull -> 8818039625616895170ull [H-THIN] +// 9654818568811123148ull -> 17382764399684682567ull [H-THIN] +// 7514399512975361670ull -> 5569220102658162789ull [H-THIN] +// 7096719386424781198ull -> 8558103254181104877ull [H-THIN] constexpr std::uint64_t kSuppressedTail_hashes[] = { - 10420073189004151623ull, - 13145042526267837939ull, - 2622782777793463991ull, - 17996446729054892726ull, - 3969181056128787992ull, - 16947117433974038ull, - 423735939073054907ull, - 18056612604689961163ull, - 12445011709498043361ull, - 16382730376298748267ull, - 12718112296054108390ull, - 913929622865489206ull, - 13218761675008378229ull, - 10388057217060506235ull, - 4373273430774112306ull, - 11946915320520331422ull, - 2472677882334572745ull, - 4709222664471492344ull, - 5785035152764109816ull, - 15834701310167426825ull, - 18416054639860847807ull, - 9654818568811123148ull, - 7514399512975361670ull, - 7096719386424781198ull, + 243601838968427296ull, + 14082045497352522076ull, + 16788444728701273278ull, + 4735244315032400709ull, + 9274703563551556229ull, + 4437548837203718331ull, + 9447280110204500336ull, + 644150700710845644ull, + 12856503234686701952ull, + 8228843622305023308ull, + 11723584407685805665ull, + 12427688725613086017ull, + 5717024028566739016ull, + 9136288938210272546ull, + 14265105010801500849ull, + 5913118032193444201ull, + 2601700310587933770ull, + 16710877784426826175ull, + 12222101465241554407ull, + 1466897115669479138ull, + 8818039625616895170ull, + 17382764399684682567ull, + 5569220102658162789ull, + 8558103254181104877ull, }; constexpr Row kSuppressedTail_rows[] = { {1736121780000LL, 1736122140000LL, 101.5, 100.5, 2, -6, 4, 0.5, 5, 1, 0}, diff --git a/tests/test_publication_witness_pinned.inc b/tests/test_publication_witness_pinned.inc index 6892794d..9f3723f2 100644 --- a/tests/test_publication_witness_pinned.inc +++ b/tests/test_publication_witness_pinned.inc @@ -54,161 +54,318 @@ // ConfigFlags103M: trades 66 -> 65 // ConfigFlags104: trades 12 -> 13 // ConfigFlags104M: trades 12 -> 13 +// expectation corrected (INT26 v19 hash re-pin, 156 values over 156 rows), because the integrated tree's picks move v19 hash values inside the unreleased epoch: R5 lane H-THIN's hash step (the Pine host's excursion model leaves the source-layer fold, E19; a FIFO exit reserves its own entry's quantity, P10), R5 lane PAR-ORDERS-2's new transient coof_fill_forced_ fold and R5 lane PAR-MARGIN-2's post-fill margin waypoint and R5 lane PAR-CASHFEE's hash step (the fold folds the sizing snapshot's strategy.equity where a percent-of-equity quantity under a cash fee is recorded); old values are the pins every pick since PAR-ORDERS' re-pin (1518fa8f) kept, harvested with this TU's own switch on the integrated tree and at every pick boundary; the source folds, closed trades and recorded rows did not move: +// Config01: digest 0x13599d44631228e2ull -> 0xb668af594ea2f5d9ull [H-THIN] +// ConfigFlags101: digest 0x9d75aa3c41307c6aull -> 0xe7f27c137f45ac4bull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] +// Config01M: digest 0x3ebbd6d78581edeaull -> 0x1efcbeef8aa1dc6dull [H-THIN] +// ConfigFlags101M: digest 0x347646d359e21807ull -> 0xbee912e49e616f4eull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] +// Config02: digest 0x6dde6b5c5440d715ull -> 0x341cf20ba6b96fc3ull [H-THIN] +// ConfigFlags102: digest 0x91619a8b469ea6aeull -> 0xef9792602fc551c1ull [H-THIN] +// Config02M: digest 0xd04764a2d7139486ull -> 0x4983b8719bbfd192ull [H-THIN] +// ConfigFlags102M: digest 0x854413a4a59d0d33ull -> 0xeed018756d1228e9ull [H-THIN] +// Config03: digest 0x461a919ebdcd41e9ull -> 0x0fc93105580e996eull [PAR-MARGIN-2, H-THIN, PAR-CASHFEE] +// ConfigFlags103: digest 0xcf60de51b3acfcc3ull -> 0x9e412b4fb357ae55ull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] +// Config03M: digest 0x4511d5cd864e706full -> 0x2e2ff1ccdf841c8dull [H-THIN, PAR-CASHFEE] +// ConfigFlags103M: digest 0xdd7937eb65073a91ull -> 0xa68e0a951db111ddull [PAR-ORDERS-2, H-THIN, PAR-CASHFEE] +// Config04: digest 0xa0a03448564c9be0ull -> 0x63aa9da27d89b846ull [H-THIN, PAR-CASHFEE] +// ConfigFlags104: digest 0xaf23be135bf66e22ull -> 0xefbb1c6bfcb31f17ull [H-THIN, PAR-CASHFEE] +// Config04M: digest 0xe0f138c9c848b160ull -> 0xba3069b5b515a320ull [H-THIN, PAR-CASHFEE] +// ConfigFlags104M: digest 0xbf637ed723b215beull -> 0x0b1d2d1d8096f6b9ull [H-THIN, PAR-CASHFEE] +// Config05: digest 0x17fc80e7cecd9737ull -> 0x705de70e2d70af67ull [H-THIN] +// ConfigFlags105: digest 0x270ebbdd8cfa9d63ull -> 0x69dac2f5ec1d400cull [H-THIN] +// Config05M: digest 0x3e5a9fcbb1fdcd5eull -> 0x7f5b8acc384eb6daull [H-THIN] +// ConfigFlags105M: digest 0x680bdc701517be1aull -> 0x3e23588769a2f97bull [H-THIN] +// Config06: digest 0x19f57666c4c34f11ull -> 0x9f061ce6918f93c5ull [H-THIN] +// ConfigFlags106: digest 0x006c443d5ac275d3ull -> 0xb5e0aaa404ca93b3ull [H-THIN] +// Config06M: digest 0x9b6058dc837818a5ull -> 0x1ef42058337147b8ull [H-THIN] +// ConfigFlags106M: digest 0xdd38d808b0b2005full -> 0x3f82500e8ed30156ull [H-THIN] +// Config07: digest 0xd91ff183f38d87ceull -> 0x86c366bc672888faull [H-THIN, PAR-CASHFEE] +// ConfigFlags107: digest 0x579a2bc891f60711ull -> 0xbe9ea2687b1b417aull [H-THIN, PAR-CASHFEE] +// Config07M: digest 0x4225ca6a6ba7619eull -> 0x0bdf3ca651b69092ull [H-THIN, PAR-CASHFEE] +// ConfigFlags107M: digest 0xca1facefefeb3fceull -> 0x2a5b0d3d56b2cd03ull [H-THIN, PAR-CASHFEE] +// Config08: digest 0x904781b92a51efd9ull -> 0xa23953fc578a6b43ull [H-THIN, PAR-CASHFEE] +// ConfigFlags108: digest 0xccc9a3e471b37be3ull -> 0x10e8434816e387afull [H-THIN] +// Config08M: digest 0xf227bccff87ad1d6ull -> 0x1f60bb9c965465d8ull [H-THIN, PAR-CASHFEE] +// ConfigFlags108M: digest 0xf38c8ecf14fb7075ull -> 0x2271fc69af564e4cull [H-THIN] +// Config09: digest 0xc4943ac1cca48c3eull -> 0x9360d69c97c9db7bull [H-THIN, PAR-CASHFEE] +// ConfigFlags109: digest 0x33cf69335b29701full -> 0x30a8c80d3bd0ad61ull [H-THIN, PAR-CASHFEE] +// Config09M: digest 0xd60f90586ad5c873ull -> 0x9e5b912398d35af8ull [H-THIN, PAR-CASHFEE] +// ConfigFlags109M: digest 0xc446a8edce72ed55ull -> 0x5a3876b995a66c76ull [H-THIN, PAR-CASHFEE] +// Config10: digest 0xd207636458a7977dull -> 0x18f9e53074280722ull [H-THIN] +// ConfigFlags110: digest 0x1be5ae2e695135fdull -> 0x248ead6c9dbf66b7ull [H-THIN, PAR-CASHFEE] +// Config10M: digest 0x9e63b9317b0ee761ull -> 0x4be74baf8fc46644ull [H-THIN] +// ConfigFlags110M: digest 0x7aabfb779282bb56ull -> 0x54a9f81cdce9d069ull [H-THIN, PAR-CASHFEE] +// Config11: digest 0x777e1f3e0fba2d04ull -> 0xb150c52332d168ffull [H-THIN] +// ConfigFlags111: digest 0x4722a0ba517a8698ull -> 0x32f4fb6b779cc554ull [H-THIN] +// Config11M: digest 0x33ccb718a4da671eull -> 0xc8005b52c87cf782ull [H-THIN] +// ConfigFlags111M: digest 0xd39fafb2fda05ff0ull -> 0xdb5300166665f9c1ull [H-THIN] +// Config12: digest 0x4b461c21259a2e32ull -> 0xc51092de174909d7ull [H-THIN, PAR-CASHFEE] +// ConfigFlags112: digest 0x87321f2899dad267ull -> 0x68af9f1a740c44e9ull [H-THIN] +// Config12M: digest 0xd27a9a9ee92fb498ull -> 0x3edbdba7792b3233ull [H-THIN, PAR-CASHFEE] +// ConfigFlags112M: digest 0xa9764e2ca9500901ull -> 0xfd4565803d79a56bull [H-THIN] +// Config13: digest 0x5c4aa53ee5c2d4b9ull -> 0x254b2221a515f9e7ull [H-THIN, PAR-CASHFEE] +// ConfigFlags113: digest 0xb0d7186d612e1063ull -> 0x6c6163d6eb022517ull [H-THIN, PAR-CASHFEE] +// Config13M: digest 0x5ba51d9728c119b8ull -> 0x2e78fa9fbc049dedull [H-THIN, PAR-CASHFEE] +// ConfigFlags113M: digest 0x066c20032094d9bfull -> 0x6e9f5a9ece8a9c58ull [H-THIN, PAR-CASHFEE] +// Config14: digest 0x6e95423cd9f2042aull -> 0x523695406e13e23cull [H-THIN, PAR-CASHFEE] +// ConfigFlags114: digest 0x96a34f2bb0dc2df1ull -> 0x65aeb7fa3c3ab6bfull [H-THIN, PAR-CASHFEE] +// Config14M: digest 0x3c6809a3326959b5ull -> 0x9a3809499b3623c6ull [H-THIN, PAR-CASHFEE] +// ConfigFlags114M: digest 0xb11d614113e0ee96ull -> 0x63e140a82186a98dull [H-THIN, PAR-CASHFEE] +// Config15: digest 0x9072bdfaf315f3b2ull -> 0xad1c7175dc94f6bcull [H-THIN] +// ConfigFlags115: digest 0x9049c3bebf6874f2ull -> 0x0141076a069b89e3ull [H-THIN] +// Config15M: digest 0x21730b1c555a3119ull -> 0x1344d28fa92876b9ull [H-THIN] +// ConfigFlags115M: digest 0x2af33a20163cc455ull -> 0xd8441700cc46211dull [H-THIN] +// Config16: digest 0x5b7b329438d68317ull -> 0x2533d8cb0bd677c4ull [H-THIN] +// ConfigFlags116: digest 0x112cac461ad5e409ull -> 0xf6100c192c1b1a47ull [H-THIN, PAR-CASHFEE] +// Config16M: digest 0x74174f7e187f36a9ull -> 0xfff00bd45398ff09ull [H-THIN] +// ConfigFlags116M: digest 0xfee39f4d6acb5dd9ull -> 0x4e9b7658af4a3968ull [H-THIN, PAR-CASHFEE] +// CloseAllThenEntry: digest 0x64d991ad87b59143ull -> 0xc9a6feca41e82e29ull [H-THIN] +// CloseAllThenEntryM: digest 0x768b725ffcc7ca57ull -> 0xd0aa4f83cfd909c6ull [H-THIN] +// Reversals: digest 0xf5db5c66ecd1dbd2ull -> 0x23a3b43853bb2e1aull [H-THIN] +// ReversalsM: digest 0x36b59c9ef1737981ull -> 0x29160b951dc12d18ull [H-THIN] +// SameBarBatch: digest 0x13e5941fa9151818ull -> 0x67372c2ba662f6c8ull [H-THIN] +// SameBarBatchM: digest 0x10604224255b732aull -> 0x5517e412f630d8c8ull [H-THIN] +// CloseOrderAdds: digest 0xa7d126e01939e06dull -> 0xe5d547fd7ea6eb99ull [H-THIN] +// CloseOrderAddsM: digest 0x72762eb6d5e1230cull -> 0x2fc90068db6aa341ull [H-THIN] +// DelayedStops: digest 0xa655cdf979a2d473ull -> 0xf698a0012daa63b3ull [PAR-ORDERS-2, H-THIN] +// DelayedStopsM: digest 0x1d34635f60c4dbdbull -> 0x046314c00141b54cull [PAR-ORDERS-2, H-THIN] +// Storm01: digest 0x2940f65451a3189full -> 0x0d6f6de3707c0f23ull [H-THIN] +// Storm01M: digest 0xbfc8780fbe24d51cull -> 0x17fdf11d7effc7f0ull [H-THIN] +// Storm02: digest 0x951345d2c3fa83a0ull -> 0x5ab7e89bf0a32e17ull [PAR-ORDERS-2, H-THIN] +// Storm02M: digest 0x1521264d78122ebeull -> 0xae05144418043f11ull [PAR-ORDERS-2, H-THIN] +// Storm03: digest 0x25f90b9eeb5f057aull -> 0x1734fbc73a88fdafull [H-THIN] +// Storm03M: digest 0xe4da66913719f57aull -> 0x87784b723c37ec66ull [H-THIN] +// Storm04: digest 0x3157bd59f491a789ull -> 0x33ceb62ad976d38aull [H-THIN] +// Storm04M: digest 0xa1dd548b8f00457eull -> 0xb06e7fe199931e4aull [H-THIN] +// Storm05: digest 0x8eacccdcac89765cull -> 0x00106a73f4035d2bull [PAR-ORDERS-2, H-THIN] +// Storm05M: digest 0x67d9decf4dd40cb4ull -> 0xdef761ffd71cac33ull [PAR-ORDERS-2, H-THIN] +// Storm06: digest 0x4053ebf00e230b53ull -> 0x6bfcdec22c1a1c8full [H-THIN] +// Storm06M: digest 0x4d4c9f8c75e1901bull -> 0x44fe7480122bc682ull [H-THIN] +// Storm07: digest 0xf7e280a190e60133ull -> 0xacff98851a92d473ull [H-THIN] +// Storm07M: digest 0x3b29ea97a5b2dd0aull -> 0x196a55d2091dc429ull [H-THIN] +// Storm08: digest 0x9302e1c04c2dfedfull -> 0x03ba3cc50503307dull [PAR-ORDERS-2, H-THIN] +// Storm08M: digest 0x8944facb400c0524ull -> 0xcdb1ebd3491998e5ull [PAR-ORDERS-2, H-THIN] +// Storm09: digest 0x09195d4f14ec28ddull -> 0xc5746cc50cf41375ull [H-THIN] +// Storm09M: digest 0x5a6cd7deec2033ffull -> 0x8aa89637b4d04cf0ull [H-THIN] +// Storm10: digest 0x7ae3261a9cbdeff3ull -> 0x2697bb1c5389db54ull [PAR-ORDERS-2, H-THIN] +// Storm10M: digest 0x03595faccb3309cdull -> 0x741a981667c1a302ull [PAR-ORDERS-2, H-THIN] +// Storm11: digest 0xf46684d86365208aull -> 0x7cfb0ba740c2806bull [H-THIN] +// Storm11M: digest 0xb5205907e08e01bfull -> 0xcfc4532d945da385ull [H-THIN] +// Storm12: digest 0x8b151167a2310837ull -> 0x659852f60d3d358cull [PAR-ORDERS-2, H-THIN] +// Storm12M: digest 0xc49a83bf5709682bull -> 0xdb3db1a969f1b59aull [PAR-ORDERS-2, H-THIN] +// Storm13: digest 0xde3f322bf0c59a98ull -> 0xe613ff94ad1aad2aull [H-THIN] +// Storm13M: digest 0xf7d902eba9385f60ull -> 0xf7793304d1dbd532ull [H-THIN] +// Storm14: digest 0x1ac3a18ac2e4ed4bull -> 0x0b1595a2d45290f2ull [H-THIN] +// Storm14M: digest 0xd88e046854060dd6ull -> 0x9e9a3044d9dbcad8ull [H-THIN] +// Storm15: digest 0x6229ca73e971d1c1ull -> 0xc2912804f77858fbull [H-THIN] +// Storm15M: digest 0xa648d45a63e9a8afull -> 0xf32358c4a29e8066ull [H-THIN] +// Storm16: digest 0x8918c41ad3b998bdull -> 0x9f128f923d41d2e7ull [H-THIN] +// Storm16M: digest 0x6b216cf99a1880dfull -> 0x72b90fec2e8871d7ull [H-THIN] +// Storm17: digest 0x644e0ad8e5fd4833ull -> 0xd62326206a38e5f5ull [PAR-ORDERS-2, H-THIN] +// Storm17M: digest 0x46e2d77c04f174e3ull -> 0x9631182cfd1c6023ull [PAR-ORDERS-2, H-THIN] +// Storm18: digest 0xe7e874c21bf9c5fcull -> 0x86e8b0cc2cbda4aeull [PAR-ORDERS-2, H-THIN] +// Storm18M: digest 0x75b39ccdb9b4542dull -> 0xd9fb27e46899ccd7ull [PAR-ORDERS-2, H-THIN] +// Storm19: digest 0x8c49334fb012a382ull -> 0xe92413fbdc464242ull [PAR-ORDERS-2, H-THIN] +// Storm19M: digest 0x83ea12d967fea7e7ull -> 0xa9a584e70d470321ull [PAR-ORDERS-2, H-THIN] +// Storm20: digest 0xe93eebdfc01f57b4ull -> 0x93de366f51f2315dull [PAR-ORDERS-2, H-THIN] +// Storm20M: digest 0xfdb5a657600e2375ull -> 0x201adc8ab4ea9bdbull [PAR-ORDERS-2, H-THIN] +// Storm21: digest 0xc0aad15a28128003ull -> 0xd4a8eda3db34c6eeull [H-THIN] +// Storm21M: digest 0xbc3fbaacf5272ad1ull -> 0xefaa36484eb9c7a1ull [H-THIN] +// Storm22: digest 0xda5de455f464504dull -> 0x686821a1adb13279ull [PAR-ORDERS-2, H-THIN] +// Storm22M: digest 0xfdaa191a97b50614ull -> 0x28a2c0fefdac67afull [PAR-ORDERS-2, H-THIN] +// Storm23: digest 0xb91e295d6da6778cull -> 0xfac7dcb54dc93113ull [PAR-ORDERS-2, H-THIN] +// Storm23M: digest 0x78f7d0134456384full -> 0xe66e15ba3a360e59ull [PAR-ORDERS-2, H-THIN] +// Storm24: digest 0x6f40ccb0266ca278ull -> 0x9a05f4cc2a37c8b8ull [H-THIN] +// Storm24M: digest 0xa2eeb9f1d0e10619ull -> 0xa1f1acfcca37ef51ull [H-THIN] +// Legs01: digest 0xb34653b540b649dfull -> 0x2257c1f1b017aff6ull [H-THIN] +// Legs01M: digest 0xafbbc030265c824full -> 0xdeeb42cf30042b94ull [H-THIN] +// LegsStops01: digest 0x622c768899d0febaull -> 0xe82893532dd3da24ull [H-THIN] +// LegsStops01M: digest 0xd71b800ba07f1670ull -> 0x40cde458e42af83dull [H-THIN] +// Legs02: digest 0xe6cecd70ee4e816cull -> 0x290854a540c0effdull [H-THIN] +// Legs02M: digest 0x9932159d4daf8f18ull -> 0xa31bffc389143eaaull [H-THIN] +// LegsStops02: digest 0x032c556130d84ee8ull -> 0x7aa8ab78f1f5ada3ull [H-THIN] +// LegsStops02M: digest 0xb2d41090d3afed86ull -> 0xa1d4657aea1b015dull [H-THIN] +// Legs03: digest 0xe484f43d3f03e46aull -> 0x4a813820bb3a558dull [H-THIN] +// Legs03M: digest 0x5a7256077ac9caf7ull -> 0xc10d45c7cb3cbee5ull [H-THIN] +// LegsStops03: digest 0xd1cb8cd8c6459831ull -> 0xc02aaa3f7c355e7aull [H-THIN] +// LegsStops03M: digest 0x6f87644ed30f1f84ull -> 0x55a7fe1ea0fc100aull [H-THIN] +// Legs04: digest 0x9405751b42b03526ull -> 0x254b1a89ea0388ddull [H-THIN] +// Legs04M: digest 0xf80fabe52a3a310bull -> 0x079706336deb5323ull [H-THIN] +// LegsStops04: digest 0x3b9823513ae000abull -> 0xc6a5620646c92b3cull [H-THIN] +// LegsStops04M: digest 0x939d11e8a52ef4a6ull -> 0xbcad7c107485fbe8ull [H-THIN] +// Legs05: digest 0xbe440458b5a03c6bull -> 0x2b3d7db72415c3ecull [H-THIN] +// Legs05M: digest 0xab877ee06eeef9deull -> 0x88fd0678c5020748ull [H-THIN] +// LegsStops05: digest 0x9dd37a12391bd5f1ull -> 0x94435fe6bea6a39dull [H-THIN] +// LegsStops05M: digest 0x5895c3b5e20f05d2ull -> 0x2722fdb103c71c42ull [H-THIN] +// Legs06: digest 0x8273cddc66300d47ull -> 0x6e7d43e693bec002ull [H-THIN] +// Legs06M: digest 0x38b98f46b0793f2cull -> 0x2fc1828cff3085f2ull [H-THIN] +// LegsStops06: digest 0x5eafd9de638208b3ull -> 0x198f7d7e224884c0ull [H-THIN] +// LegsStops06M: digest 0x06d7e046fb01bdf4ull -> 0x507c89b50b4d4b2bull [H-THIN] +// TwoEntries: digest 0x8c5be2d95d0be7e5ull -> 0x06f2ecb7c1a3a480ull [H-THIN] +// TwoEntriesM: digest 0xfbbd9f232e1ccfbcull -> 0x7c956e01ced10ccbull [H-THIN] +// PyramidLots: digest 0x196e1c6f6d80b3feull -> 0x243689d4eeaf8d3bull [H-THIN] +// PyramidLotsM: digest 0xc771a9a6a70374c6ull -> 0x7c425d1716c9a5a2ull [H-THIN] +// LegBesideClose: digest 0x805b56fa470a4dbeull -> 0xfd494f8d3e8228d4ull [H-THIN] +// LegBesideCloseM: digest 0x8e0bb72d36ce8063ull -> 0x4a9e3df3ac353419ull [H-THIN] +// TwoLegsOnly: digest 0x0ed47ce9f98c045aull -> 0x1e97c755ea7f6e5full [H-THIN] +// TwoLegsOnlyM: digest 0xa713e15d8063372full -> 0x8b4283e07b576d5full [H-THIN] +// LegsApart: digest 0xcfcd1f1d87803a44ull -> 0x64ff417b6bcf212full [H-THIN] +// LegsApartM: digest 0x701aca7df9071fb5ull -> 0xaa0c742ddeacc4d5ull [H-THIN] constexpr Pinned kPinned[] = { - {"Config01", 0x13599d44631228e2ull, 144, 44, 144}, - {"ConfigFlags101", 0x9d75aa3c41307c6aull, 145, 45, 144}, - {"Config01M", 0x3ebbd6d78581edeaull, 144, 42, 144}, - {"ConfigFlags101M", 0x347646d359e21807ull, 145, 45, 144}, - {"Config02", 0x6dde6b5c5440d715ull, 48, 11, 48}, - {"ConfigFlags102", 0x91619a8b469ea6aeull, 144, 52, 144}, - {"Config02M", 0xd04764a2d7139486ull, 48, 11, 48}, - {"ConfigFlags102M", 0x854413a4a59d0d33ull, 144, 52, 144}, - {"Config03", 0x461a919ebdcd41e9ull, 144, 48, 144}, - {"ConfigFlags103", 0xcf60de51b3acfcc3ull, 184, 63, 144}, - {"Config03M", 0x4511d5cd864e706full, 144, 44, 144}, - {"ConfigFlags103M", 0xdd7937eb65073a91ull, 184, 65, 144}, - {"Config04", 0xa0a03448564c9be0ull, 144, 21, 144}, - {"ConfigFlags104", 0xaf23be135bf66e22ull, 48, 13, 48}, - {"Config04M", 0xe0f138c9c848b160ull, 144, 21, 144}, - {"ConfigFlags104M", 0xbf637ed723b215beull, 48, 13, 48}, - {"Config05", 0x17fc80e7cecd9737ull, 48, 14, 48}, - {"ConfigFlags105", 0x270ebbdd8cfa9d63ull, 48, 13, 48}, - {"Config05M", 0x3e5a9fcbb1fdcd5eull, 48, 14, 48}, - {"ConfigFlags105M", 0x680bdc701517be1aull, 48, 13, 48}, - {"Config06", 0x19f57666c4c34f11ull, 48, 5, 48}, - {"ConfigFlags106", 0x006c443d5ac275d3ull, 48, 6, 48}, - {"Config06M", 0x9b6058dc837818a5ull, 48, 5, 48}, - {"ConfigFlags106M", 0xdd38d808b0b2005full, 48, 6, 48}, - {"Config07", 0xd91ff183f38d87ceull, 144, 48, 144}, - {"ConfigFlags107", 0x579a2bc891f60711ull, 48, 20, 48}, - {"Config07M", 0x4225ca6a6ba7619eull, 144, 48, 144}, - {"ConfigFlags107M", 0xca1facefefeb3fceull, 48, 21, 48}, - {"Config08", 0x904781b92a51efd9ull, 48, 12, 48}, - {"ConfigFlags108", 0xccc9a3e471b37be3ull, 48, 15, 48}, - {"Config08M", 0xf227bccff87ad1d6ull, 48, 12, 48}, - {"ConfigFlags108M", 0xf38c8ecf14fb7075ull, 48, 15, 48}, - {"Config09", 0xc4943ac1cca48c3eull, 144, 66, 144}, - {"ConfigFlags109", 0x33cf69335b29701full, 144, 42, 144}, - {"Config09M", 0xd60f90586ad5c873ull, 144, 66, 144}, - {"ConfigFlags109M", 0xc446a8edce72ed55ull, 144, 42, 144}, - {"Config10", 0xd207636458a7977dull, 48, 18, 48}, - {"ConfigFlags110", 0x1be5ae2e695135fdull, 48, 11, 48}, - {"Config10M", 0x9e63b9317b0ee761ull, 48, 18, 48}, - {"ConfigFlags110M", 0x7aabfb779282bb56ull, 48, 11, 48}, - {"Config11", 0x777e1f3e0fba2d04ull, 144, 60, 144}, - {"ConfigFlags111", 0x4722a0ba517a8698ull, 48, 19, 48}, - {"Config11M", 0x33ccb718a4da671eull, 144, 60, 144}, - {"ConfigFlags111M", 0xd39fafb2fda05ff0ull, 48, 19, 48}, - {"Config12", 0x4b461c21259a2e32ull, 144, 48, 144}, - {"ConfigFlags112", 0x87321f2899dad267ull, 144, 18, 144}, - {"Config12M", 0xd27a9a9ee92fb498ull, 144, 48, 144}, - {"ConfigFlags112M", 0xa9764e2ca9500901ull, 144, 18, 144}, - {"Config13", 0x5c4aa53ee5c2d4b9ull, 48, 19, 48}, - {"ConfigFlags113", 0xb0d7186d612e1063ull, 48, 19, 48}, - {"Config13M", 0x5ba51d9728c119b8ull, 48, 19, 48}, - {"ConfigFlags113M", 0x066c20032094d9bfull, 48, 19, 48}, - {"Config14", 0x6e95423cd9f2042aull, 144, 40, 144}, - {"ConfigFlags114", 0x96a34f2bb0dc2df1ull, 144, 60, 144}, - {"Config14M", 0x3c6809a3326959b5ull, 144, 40, 144}, - {"ConfigFlags114M", 0xb11d614113e0ee96ull, 144, 60, 144}, - {"Config15", 0x9072bdfaf315f3b2ull, 144, 48, 144}, - {"ConfigFlags115", 0x9049c3bebf6874f2ull, 48, 6, 48}, - {"Config15M", 0x21730b1c555a3119ull, 144, 48, 144}, - {"ConfigFlags115M", 0x2af33a20163cc455ull, 48, 6, 48}, - {"Config16", 0x5b7b329438d68317ull, 144, 54, 144}, - {"ConfigFlags116", 0x112cac461ad5e409ull, 48, 19, 48}, - {"Config16M", 0x74174f7e187f36a9ull, 144, 54, 144}, - {"ConfigFlags116M", 0xfee39f4d6acb5dd9ull, 48, 19, 48}, - {"CloseAllThenEntry", 0x64d991ad87b59143ull, 40, 17, 40}, - {"CloseAllThenEntryM", 0x768b725ffcc7ca57ull, 40, 17, 40}, - {"Reversals", 0xf5db5c66ecd1dbd2ull, 40, 16, 40}, - {"ReversalsM", 0x36b59c9ef1737981ull, 40, 16, 40}, - {"SameBarBatch", 0x13e5941fa9151818ull, 36, 25, 36}, - {"SameBarBatchM", 0x10604224255b732aull, 36, 25, 36}, - {"CloseOrderAdds", 0xa7d126e01939e06dull, 36, 22, 36}, - {"CloseOrderAddsM", 0x72762eb6d5e1230cull, 36, 22, 36}, - {"DelayedStops", 0xa655cdf979a2d473ull, 66, 20, 40}, - {"DelayedStopsM", 0x1d34635f60c4dbdbull, 66, 20, 40}, - {"Storm01", 0x2940f65451a3189full, 43, 13, 43}, - {"Storm01M", 0xbfc8780fbe24d51cull, 43, 14, 43}, - {"Storm02", 0x951345d2c3fa83a0ull, 63, 12, 42}, - {"Storm02M", 0x1521264d78122ebeull, 63, 12, 42}, - {"Storm03", 0x25f90b9eeb5f057aull, 41, 17, 41}, - {"Storm03M", 0xe4da66913719f57aull, 41, 17, 41}, - {"Storm04", 0x3157bd59f491a789ull, 54, 22, 54}, - {"Storm04M", 0xa1dd548b8f00457eull, 54, 22, 54}, - {"Storm05", 0x8eacccdcac89765cull, 81, 17, 53}, - {"Storm05M", 0x67d9decf4dd40cb4ull, 82, 17, 53}, - {"Storm06", 0x4053ebf00e230b53ull, 40, 11, 40}, - {"Storm06M", 0x4d4c9f8c75e1901bull, 40, 11, 40}, - {"Storm07", 0xf7e280a190e60133ull, 55, 13, 55}, - {"Storm07M", 0x3b29ea97a5b2dd0aull, 55, 13, 55}, - {"Storm08", 0x9302e1c04c2dfedfull, 81, 14, 58}, - {"Storm08M", 0x8944facb400c0524ull, 81, 14, 58}, - {"Storm09", 0x09195d4f14ec28ddull, 48, 21, 48}, - {"Storm09M", 0x5a6cd7deec2033ffull, 48, 20, 48}, - {"Storm10", 0x7ae3261a9cbdeff3ull, 65, 16, 57}, - {"Storm10M", 0x03595faccb3309cdull, 66, 16, 57}, - {"Storm11", 0xf46684d86365208aull, 59, 22, 59}, - {"Storm11M", 0xb5205907e08e01bfull, 59, 22, 59}, - {"Storm12", 0x8b151167a2310837ull, 86, 20, 56}, - {"Storm12M", 0xc49a83bf5709682bull, 86, 20, 56}, - {"Storm13", 0xde3f322bf0c59a98ull, 46, 23, 46}, - {"Storm13M", 0xf7d902eba9385f60ull, 46, 23, 46}, - {"Storm14", 0x1ac3a18ac2e4ed4bull, 46, 18, 46}, - {"Storm14M", 0xd88e046854060dd6ull, 46, 18, 46}, - {"Storm15", 0x6229ca73e971d1c1ull, 43, 13, 43}, - {"Storm15M", 0xa648d45a63e9a8afull, 43, 14, 43}, - {"Storm16", 0x8918c41ad3b998bdull, 58, 18, 58}, - {"Storm16M", 0x6b216cf99a1880dfull, 58, 18, 58}, - {"Storm17", 0x644e0ad8e5fd4833ull, 86, 23, 54}, - {"Storm17M", 0x46e2d77c04f174e3ull, 80, 18, 54}, - {"Storm18", 0xe7e874c21bf9c5fcull, 80, 23, 49}, - {"Storm18M", 0x75b39ccdb9b4542dull, 80, 23, 49}, - {"Storm19", 0x8c49334fb012a382ull, 86, 23, 49}, - {"Storm19M", 0x83ea12d967fea7e7ull, 86, 23, 49}, - {"Storm20", 0xe93eebdfc01f57b4ull, 45, 12, 42}, - {"Storm20M", 0xfdb5a657600e2375ull, 45, 12, 42}, - {"Storm21", 0xc0aad15a28128003ull, 47, 14, 47}, - {"Storm21M", 0xbc3fbaacf5272ad1ull, 47, 14, 47}, - {"Storm22", 0xda5de455f464504dull, 76, 19, 50}, - {"Storm22M", 0xfdaa191a97b50614ull, 76, 19, 50}, - {"Storm23", 0xb91e295d6da6778cull, 89, 26, 54}, - {"Storm23M", 0x78f7d0134456384full, 89, 26, 54}, - {"Storm24", 0x6f40ccb0266ca278ull, 54, 25, 54}, - {"Storm24M", 0xa2eeb9f1d0e10619ull, 54, 25, 54}, - {"Legs01", 0xb34653b540b649dfull, 30, 6, 30}, - {"Legs01M", 0xafbbc030265c824full, 30, 6, 30}, - {"LegsStops01", 0x622c768899d0febaull, 30, 6, 30}, - {"LegsStops01M", 0xd71b800ba07f1670ull, 30, 6, 30}, - {"Legs02", 0xe6cecd70ee4e816cull, 30, 6, 30}, - {"Legs02M", 0x9932159d4daf8f18ull, 30, 6, 30}, - {"LegsStops02", 0x032c556130d84ee8ull, 30, 6, 30}, - {"LegsStops02M", 0xb2d41090d3afed86ull, 30, 6, 30}, - {"Legs03", 0xe484f43d3f03e46aull, 30, 6, 30}, - {"Legs03M", 0x5a7256077ac9caf7ull, 30, 6, 30}, - {"LegsStops03", 0xd1cb8cd8c6459831ull, 30, 6, 30}, - {"LegsStops03M", 0x6f87644ed30f1f84ull, 30, 6, 30}, - {"Legs04", 0x9405751b42b03526ull, 30, 6, 30}, - {"Legs04M", 0xf80fabe52a3a310bull, 30, 6, 30}, - {"LegsStops04", 0x3b9823513ae000abull, 30, 6, 30}, - {"LegsStops04M", 0x939d11e8a52ef4a6ull, 30, 6, 30}, - {"Legs05", 0xbe440458b5a03c6bull, 30, 6, 30}, - {"Legs05M", 0xab877ee06eeef9deull, 30, 6, 30}, - {"LegsStops05", 0x9dd37a12391bd5f1ull, 30, 6, 30}, - {"LegsStops05M", 0x5895c3b5e20f05d2ull, 30, 6, 30}, - {"Legs06", 0x8273cddc66300d47ull, 30, 6, 30}, - {"Legs06M", 0x38b98f46b0793f2cull, 30, 6, 30}, - {"LegsStops06", 0x5eafd9de638208b3ull, 30, 6, 30}, - {"LegsStops06M", 0x06d7e046fb01bdf4ull, 30, 6, 30}, - {"TwoEntries", 0x8c5be2d95d0be7e5ull, 30, 2, 30}, - {"TwoEntriesM", 0xfbbd9f232e1ccfbcull, 30, 2, 30}, - {"PyramidLots", 0x196e1c6f6d80b3feull, 30, 2, 30}, - {"PyramidLotsM", 0xc771a9a6a70374c6ull, 30, 2, 30}, - {"LegBesideClose", 0x805b56fa470a4dbeull, 30, 2, 30}, - {"LegBesideCloseM", 0x8e0bb72d36ce8063ull, 30, 2, 30}, - {"TwoLegsOnly", 0x0ed47ce9f98c045aull, 30, 2, 30}, - {"TwoLegsOnlyM", 0xa713e15d8063372full, 30, 2, 30}, - {"LegsApart", 0xcfcd1f1d87803a44ull, 30, 2, 30}, - {"LegsApartM", 0x701aca7df9071fb5ull, 30, 2, 30}, + {"Config01", 0xb668af594ea2f5d9ull, 144, 44, 144}, + {"ConfigFlags101", 0xe7f27c137f45ac4bull, 145, 45, 144}, + {"Config01M", 0x1efcbeef8aa1dc6dull, 144, 42, 144}, + {"ConfigFlags101M", 0xbee912e49e616f4eull, 145, 45, 144}, + {"Config02", 0x341cf20ba6b96fc3ull, 48, 11, 48}, + {"ConfigFlags102", 0xef9792602fc551c1ull, 144, 52, 144}, + {"Config02M", 0x4983b8719bbfd192ull, 48, 11, 48}, + {"ConfigFlags102M", 0xeed018756d1228e9ull, 144, 52, 144}, + {"Config03", 0x0fc93105580e996eull, 144, 48, 144}, + {"ConfigFlags103", 0x9e412b4fb357ae55ull, 184, 63, 144}, + {"Config03M", 0x2e2ff1ccdf841c8dull, 144, 44, 144}, + {"ConfigFlags103M", 0xa68e0a951db111ddull, 184, 65, 144}, + {"Config04", 0x63aa9da27d89b846ull, 144, 21, 144}, + {"ConfigFlags104", 0xefbb1c6bfcb31f17ull, 48, 13, 48}, + {"Config04M", 0xba3069b5b515a320ull, 144, 21, 144}, + {"ConfigFlags104M", 0x0b1d2d1d8096f6b9ull, 48, 13, 48}, + {"Config05", 0x705de70e2d70af67ull, 48, 14, 48}, + {"ConfigFlags105", 0x69dac2f5ec1d400cull, 48, 13, 48}, + {"Config05M", 0x7f5b8acc384eb6daull, 48, 14, 48}, + {"ConfigFlags105M", 0x3e23588769a2f97bull, 48, 13, 48}, + {"Config06", 0x9f061ce6918f93c5ull, 48, 5, 48}, + {"ConfigFlags106", 0xb5e0aaa404ca93b3ull, 48, 6, 48}, + {"Config06M", 0x1ef42058337147b8ull, 48, 5, 48}, + {"ConfigFlags106M", 0x3f82500e8ed30156ull, 48, 6, 48}, + {"Config07", 0x86c366bc672888faull, 144, 48, 144}, + {"ConfigFlags107", 0xbe9ea2687b1b417aull, 48, 20, 48}, + {"Config07M", 0x0bdf3ca651b69092ull, 144, 48, 144}, + {"ConfigFlags107M", 0x2a5b0d3d56b2cd03ull, 48, 21, 48}, + {"Config08", 0xa23953fc578a6b43ull, 48, 12, 48}, + {"ConfigFlags108", 0x10e8434816e387afull, 48, 15, 48}, + {"Config08M", 0x1f60bb9c965465d8ull, 48, 12, 48}, + {"ConfigFlags108M", 0x2271fc69af564e4cull, 48, 15, 48}, + {"Config09", 0x9360d69c97c9db7bull, 144, 66, 144}, + {"ConfigFlags109", 0x30a8c80d3bd0ad61ull, 144, 42, 144}, + {"Config09M", 0x9e5b912398d35af8ull, 144, 66, 144}, + {"ConfigFlags109M", 0x5a3876b995a66c76ull, 144, 42, 144}, + {"Config10", 0x18f9e53074280722ull, 48, 18, 48}, + {"ConfigFlags110", 0x248ead6c9dbf66b7ull, 48, 11, 48}, + {"Config10M", 0x4be74baf8fc46644ull, 48, 18, 48}, + {"ConfigFlags110M", 0x54a9f81cdce9d069ull, 48, 11, 48}, + {"Config11", 0xb150c52332d168ffull, 144, 60, 144}, + {"ConfigFlags111", 0x32f4fb6b779cc554ull, 48, 19, 48}, + {"Config11M", 0xc8005b52c87cf782ull, 144, 60, 144}, + {"ConfigFlags111M", 0xdb5300166665f9c1ull, 48, 19, 48}, + {"Config12", 0xc51092de174909d7ull, 144, 48, 144}, + {"ConfigFlags112", 0x68af9f1a740c44e9ull, 144, 18, 144}, + {"Config12M", 0x3edbdba7792b3233ull, 144, 48, 144}, + {"ConfigFlags112M", 0xfd4565803d79a56bull, 144, 18, 144}, + {"Config13", 0x254b2221a515f9e7ull, 48, 19, 48}, + {"ConfigFlags113", 0x6c6163d6eb022517ull, 48, 19, 48}, + {"Config13M", 0x2e78fa9fbc049dedull, 48, 19, 48}, + {"ConfigFlags113M", 0x6e9f5a9ece8a9c58ull, 48, 19, 48}, + {"Config14", 0x523695406e13e23cull, 144, 40, 144}, + {"ConfigFlags114", 0x65aeb7fa3c3ab6bfull, 144, 60, 144}, + {"Config14M", 0x9a3809499b3623c6ull, 144, 40, 144}, + {"ConfigFlags114M", 0x63e140a82186a98dull, 144, 60, 144}, + {"Config15", 0xad1c7175dc94f6bcull, 144, 48, 144}, + {"ConfigFlags115", 0x0141076a069b89e3ull, 48, 6, 48}, + {"Config15M", 0x1344d28fa92876b9ull, 144, 48, 144}, + {"ConfigFlags115M", 0xd8441700cc46211dull, 48, 6, 48}, + {"Config16", 0x2533d8cb0bd677c4ull, 144, 54, 144}, + {"ConfigFlags116", 0xf6100c192c1b1a47ull, 48, 19, 48}, + {"Config16M", 0xfff00bd45398ff09ull, 144, 54, 144}, + {"ConfigFlags116M", 0x4e9b7658af4a3968ull, 48, 19, 48}, + {"CloseAllThenEntry", 0xc9a6feca41e82e29ull, 40, 17, 40}, + {"CloseAllThenEntryM", 0xd0aa4f83cfd909c6ull, 40, 17, 40}, + {"Reversals", 0x23a3b43853bb2e1aull, 40, 16, 40}, + {"ReversalsM", 0x29160b951dc12d18ull, 40, 16, 40}, + {"SameBarBatch", 0x67372c2ba662f6c8ull, 36, 25, 36}, + {"SameBarBatchM", 0x5517e412f630d8c8ull, 36, 25, 36}, + {"CloseOrderAdds", 0xe5d547fd7ea6eb99ull, 36, 22, 36}, + {"CloseOrderAddsM", 0x2fc90068db6aa341ull, 36, 22, 36}, + {"DelayedStops", 0xf698a0012daa63b3ull, 66, 20, 40}, + {"DelayedStopsM", 0x046314c00141b54cull, 66, 20, 40}, + {"Storm01", 0x0d6f6de3707c0f23ull, 43, 13, 43}, + {"Storm01M", 0x17fdf11d7effc7f0ull, 43, 14, 43}, + {"Storm02", 0x5ab7e89bf0a32e17ull, 63, 12, 42}, + {"Storm02M", 0xae05144418043f11ull, 63, 12, 42}, + {"Storm03", 0x1734fbc73a88fdafull, 41, 17, 41}, + {"Storm03M", 0x87784b723c37ec66ull, 41, 17, 41}, + {"Storm04", 0x33ceb62ad976d38aull, 54, 22, 54}, + {"Storm04M", 0xb06e7fe199931e4aull, 54, 22, 54}, + {"Storm05", 0x00106a73f4035d2bull, 81, 17, 53}, + {"Storm05M", 0xdef761ffd71cac33ull, 82, 17, 53}, + {"Storm06", 0x6bfcdec22c1a1c8full, 40, 11, 40}, + {"Storm06M", 0x44fe7480122bc682ull, 40, 11, 40}, + {"Storm07", 0xacff98851a92d473ull, 55, 13, 55}, + {"Storm07M", 0x196a55d2091dc429ull, 55, 13, 55}, + {"Storm08", 0x03ba3cc50503307dull, 81, 14, 58}, + {"Storm08M", 0xcdb1ebd3491998e5ull, 81, 14, 58}, + {"Storm09", 0xc5746cc50cf41375ull, 48, 21, 48}, + {"Storm09M", 0x8aa89637b4d04cf0ull, 48, 20, 48}, + {"Storm10", 0x2697bb1c5389db54ull, 65, 16, 57}, + {"Storm10M", 0x741a981667c1a302ull, 66, 16, 57}, + {"Storm11", 0x7cfb0ba740c2806bull, 59, 22, 59}, + {"Storm11M", 0xcfc4532d945da385ull, 59, 22, 59}, + {"Storm12", 0x659852f60d3d358cull, 86, 20, 56}, + {"Storm12M", 0xdb3db1a969f1b59aull, 86, 20, 56}, + {"Storm13", 0xe613ff94ad1aad2aull, 46, 23, 46}, + {"Storm13M", 0xf7793304d1dbd532ull, 46, 23, 46}, + {"Storm14", 0x0b1595a2d45290f2ull, 46, 18, 46}, + {"Storm14M", 0x9e9a3044d9dbcad8ull, 46, 18, 46}, + {"Storm15", 0xc2912804f77858fbull, 43, 13, 43}, + {"Storm15M", 0xf32358c4a29e8066ull, 43, 14, 43}, + {"Storm16", 0x9f128f923d41d2e7ull, 58, 18, 58}, + {"Storm16M", 0x72b90fec2e8871d7ull, 58, 18, 58}, + {"Storm17", 0xd62326206a38e5f5ull, 86, 23, 54}, + {"Storm17M", 0x9631182cfd1c6023ull, 80, 18, 54}, + {"Storm18", 0x86e8b0cc2cbda4aeull, 80, 23, 49}, + {"Storm18M", 0xd9fb27e46899ccd7ull, 80, 23, 49}, + {"Storm19", 0xe92413fbdc464242ull, 86, 23, 49}, + {"Storm19M", 0xa9a584e70d470321ull, 86, 23, 49}, + {"Storm20", 0x93de366f51f2315dull, 45, 12, 42}, + {"Storm20M", 0x201adc8ab4ea9bdbull, 45, 12, 42}, + {"Storm21", 0xd4a8eda3db34c6eeull, 47, 14, 47}, + {"Storm21M", 0xefaa36484eb9c7a1ull, 47, 14, 47}, + {"Storm22", 0x686821a1adb13279ull, 76, 19, 50}, + {"Storm22M", 0x28a2c0fefdac67afull, 76, 19, 50}, + {"Storm23", 0xfac7dcb54dc93113ull, 89, 26, 54}, + {"Storm23M", 0xe66e15ba3a360e59ull, 89, 26, 54}, + {"Storm24", 0x9a05f4cc2a37c8b8ull, 54, 25, 54}, + {"Storm24M", 0xa1f1acfcca37ef51ull, 54, 25, 54}, + {"Legs01", 0x2257c1f1b017aff6ull, 30, 6, 30}, + {"Legs01M", 0xdeeb42cf30042b94ull, 30, 6, 30}, + {"LegsStops01", 0xe82893532dd3da24ull, 30, 6, 30}, + {"LegsStops01M", 0x40cde458e42af83dull, 30, 6, 30}, + {"Legs02", 0x290854a540c0effdull, 30, 6, 30}, + {"Legs02M", 0xa31bffc389143eaaull, 30, 6, 30}, + {"LegsStops02", 0x7aa8ab78f1f5ada3ull, 30, 6, 30}, + {"LegsStops02M", 0xa1d4657aea1b015dull, 30, 6, 30}, + {"Legs03", 0x4a813820bb3a558dull, 30, 6, 30}, + {"Legs03M", 0xc10d45c7cb3cbee5ull, 30, 6, 30}, + {"LegsStops03", 0xc02aaa3f7c355e7aull, 30, 6, 30}, + {"LegsStops03M", 0x55a7fe1ea0fc100aull, 30, 6, 30}, + {"Legs04", 0x254b1a89ea0388ddull, 30, 6, 30}, + {"Legs04M", 0x079706336deb5323ull, 30, 6, 30}, + {"LegsStops04", 0xc6a5620646c92b3cull, 30, 6, 30}, + {"LegsStops04M", 0xbcad7c107485fbe8ull, 30, 6, 30}, + {"Legs05", 0x2b3d7db72415c3ecull, 30, 6, 30}, + {"Legs05M", 0x88fd0678c5020748ull, 30, 6, 30}, + {"LegsStops05", 0x94435fe6bea6a39dull, 30, 6, 30}, + {"LegsStops05M", 0x2722fdb103c71c42ull, 30, 6, 30}, + {"Legs06", 0x6e7d43e693bec002ull, 30, 6, 30}, + {"Legs06M", 0x2fc1828cff3085f2ull, 30, 6, 30}, + {"LegsStops06", 0x198f7d7e224884c0ull, 30, 6, 30}, + {"LegsStops06M", 0x507c89b50b4d4b2bull, 30, 6, 30}, + {"TwoEntries", 0x06f2ecb7c1a3a480ull, 30, 2, 30}, + {"TwoEntriesM", 0x7c956e01ced10ccbull, 30, 2, 30}, + {"PyramidLots", 0x243689d4eeaf8d3bull, 30, 2, 30}, + {"PyramidLotsM", 0x7c425d1716c9a5a2ull, 30, 2, 30}, + {"LegBesideClose", 0xfd494f8d3e8228d4ull, 30, 2, 30}, + {"LegBesideCloseM", 0x4a9e3df3ac353419ull, 30, 2, 30}, + {"TwoLegsOnly", 0x1e97c755ea7f6e5full, 30, 2, 30}, + {"TwoLegsOnlyM", 0x8b4283e07b576d5full, 30, 2, 30}, + {"LegsApart", 0x64ff417b6bcf212full, 30, 2, 30}, + {"LegsApartM", 0xaa0c742ddeacc4d5ull, 30, 2, 30}, }; From ebe67484ad06356c533369896a2f6c69f9e3f8fe Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 14:47:48 +0800 Subject: [PATCH 82/83] INT26 (vii): every doc anchor re-anchored once, on the integrated tree One pass over every page check_doc_anchors.py scans, after the last pick, the behaviour pins and the hash re-pin, so the digits are the final tree's. Digits only: no sentence changes. How, in order: 1. The multi-origin exact map (INT24's int24_exact_reanchor.py, INT19's method with --exclusive-first): main 0d76a099 first, then each lane's tip in pick order (H-MEASURE 2b494497, H-DOCGATES 68b974f2, REL10 bda36e1f and 5582fff2, K-OCA-KEEP 399601b9, PAR-ORDERS 6945fc19, PAR-ORDERS-2 9f7a025c, PAR-MARGIN 38442cec, PAR-MARGIN-2 3ea143e5, PERF-KEDGE f1f3febd, CI-RUNNER f313049f, H-THIN 09f0bbda, PAR-CASHFEE b44b10b4): each anchor carried through an exact difflib line map from the tree its digits were written against. 1721 anchors: 533 moved, 1047 kept, 141 stuck (digits a pick's digit merge wrote, which no single origin carries). 2. The gate's own --fix (the unambiguous symbol misses), then anchor_origin_fix.py for the rest: walk every INT26 commit and lane tip, newest first, to the commit where the anchor was last TRUE by the gate's own judge(), carry its lines through the exact map to this tree, and re-judge here -- 22 by the same digits, 26 by the same line and position (the lane tip's own digits: H-THIN 09f0bbda, H-DOCGATES 68b974f2, PERF-KEDGE f1f3febd). 3. By hand, each checked against the code it names: `max_open_lots` pineforge.h:505; `exit_from_bracket` pine_strategy_host.cpp:870 (in adapter_label_bracket_trades); `declare_security_sites_to_kernel` pine_strategy_host.cpp:1158; `leg_activation` pine_adapter.cpp:869-874 (the bind the base cited); `submit_margin_call_slice` pine_adapter.cpp:13979 and `schedule_margin_call_path` pine_adapter.cpp:14519; MG4's `AdmitWithHostMargin` pine_adapter.cpp:13079-13145 and 13078-13080 (main's 13280-13345 and 13279-13281, carried by content, which INT25's stale_scan found kept at main's digits); design row 135's native_execution_consumer.cpp:1438-1454 (main's 1429-1445, the same). Results on this tree: - check_doc_anchors.py: 1721 anchors, 1721 OK, 0 bad. - stale_scan.py against 0d76a099: 2 reports, both false: main cited `max_open_lots` native_run_spec.hpp:596 while its field sat on 595; the field is on 596 here. - verify_carry.py over the same origins: 1668 EXACT (an origin cites the same text), 27 EDITED (a pick rewrote the cited lines; each still passes the gate's claim), 26 NEW (prose the picks and INT26 wrote). - check_doc_lint.py: no offenders. docs/build.sh: Doxygen warnings 0. - The floor citations name the assignment lines (ci_verify.py:260 and :390). Co-Authored-By: Claude Opus 5.5 (1M context) --- CONTRIBUTING.md | 2 +- docs/adr/0001-kernel-adapter-boundary.md | 150 +++++------ docs/design/native-feature-parity.md | 280 +++++++++---------- docs/native-refactor-progress.md | 22 +- docs/pages/contributing-llm.md | 10 +- docs/pages/native-engine.md | 32 +-- docs/pages/pine-to-native.md | 326 +++++++++++------------ 7 files changed, 411 insertions(+), 411 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f3bf200..a38f3d70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,7 +65,7 @@ Two consequences that are easy to miss: | Add a generic broker/matching capability | the kernel (`src/engine_*`, `src/native_*`) | make it opt-in, add its ADR 0001 ruling row, add a kernel-only test, and add an `examples/native/` host if the ruling is *native-only* | | Add or change a TA class | the right `ta_*.cpp` partition + its declaration in `` | add a unit test against a hand-computed series | | Change what codegen may emit | the contract, not this repo's runtime | say so in the PR; the transpiler lives in `pineforge-codegen-oss` | -| Add a runtime `PF_API` export | `src/c_abi.cpp` + `include/pineforge/pineforge.h` | update `EXPECTED_RUNTIME` check_c_abi_runtime.py:28, the ctypes harnesses, and the README symbol table — all in the same commit | +| Add a runtime `PF_API` export | `src/c_abi.cpp` + `include/pineforge/pineforge.h` | update `EXPECTED_RUNTIME` check_c_abi_runtime.py:29, the ctypes harnesses, and the README symbol table — all in the same commit | | Add a C kernel-driving export | `src/native_c_host.cpp` + `include/pineforge/native_c_api.h` | update that header's COVERAGE block, and retire the 1.0 C boundary row the export closes; `scripts/check_native_c_api_surface.py` proves the block is exactly the host's public surface and fails on a closed gap | | Document something | `docs/pages/`, `README.md`, this file | cite the tree by `file:line`; the anchor guard checks that the line still holds the symbol | diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index 1055faac..d9c3b90c 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -77,7 +77,7 @@ summary of it. (`native_host.hpp:893`); also `on_native_input` (`native_host.hpp:875`), `on_native_tick` (`native_host.hpp:878`), `on_native_timeframe_bar` (`native_host.hpp:885`), `on_native_bar_open` — at the modeled opening, before its matching pass - (`native_host.hpp:889`, `on_native_bar_open` `native_execution_consumer.cpp:7084-7086`) — + (`native_host.hpp:889`, `on_native_bar_open` `native_execution_consumer.cpp:7096-7098`) — `on_native_recalculate` for every calculation of the run (`native_host.hpp:913`), `on_native_sub_bar` (`native_host.hpp:928`) and post-fill `on_native_applied` (`native_host.hpp:938`). `on_bar` is `final` (`native_host.hpp:857`). @@ -89,7 +89,7 @@ summary of it. `NativeCoordinate::input_interval_index` (`market_driver.hpp:79`) names the input slot that supplied the point for a host that needs input cadence. Both coordinate values are folded by `hash_coordinate` - (`native_execution_consumer.cpp:1051-1065`) in the v19 continuation. The Pine + (`native_execution_consumer.cpp:1060-1074`) in the v19 continuation. The Pine adapter keeps its pending-book and chart-sampling policy in the source layer, reading the named input coordinate through `projection_bar_index` (`pine_adapter.cpp:678-684`) where Pine still needs input cadence, and @@ -111,10 +111,10 @@ summary of it. bare host needs no override at all, while `HostSized` hands the units to the host — and an unresolved `HostSized` whose `resolve_execution_terms` (`native_host.hpp:947`) returns no units is a `TermsUnresolved` rejection - (`TermsUnresolved` `native_execution_consumer.cpp:4976`). + (`TermsUnresolved` `native_execution_consumer.cpp:4987`). - **Observing executions.** Implement `on_native_applied` (`native_host.hpp:938`) — notifications drain FIFO after the outer callback returns — and/or poll `native_events(after_ordinal)` - (`native_host.hpp:1317`). Do *not* implement + (`native_host.hpp:1335`). Do *not* implement `NativeExecutionConsumer`: it is the kernel's internal matcher, declared `final` (`src/native_execution_consumer.hpp:58`) and bound by the host. - **The rest of the host surface**, all on `NativeStrategyHost`: @@ -122,11 +122,11 @@ summary of it. | What | Surface | |---|---| | Run state | `native_state()` → `NativeStateView` — kind, phase, `NativeCompletion` BatchComplete / StreamEnded, failure, high water (`NativeCompletion` `native_host.hpp:50`, `NativeStateView` `:282`, `NativeRunPhase` `:41`) | - | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:4949`) | + | Failure model | `NativeFailureCode` (`native_host.hpp:63-77`); `Failed` is latched, nothing resumes. A throwing callback latches `CallbackException` (`CallbackException` `native_execution_consumer.cpp:4960`) | | Cohorts | `cohort_open` (`native_host.hpp:1282`) / `cohort_add` (`:1285`) / `cohort_remove` (`:1288`) — the handle a `BindCohort` owner names | | Mid-callback execution | `current_execution_point` (`native_host.hpp:1087`), `inspect_current_execution` (`native_host.hpp:1099`) / `execute_current` (`native_host.hpp:1104`) | | Margin seams | four, not one: the run spec's own `margin` model (`NativeMarginModel` `native_run_spec.hpp:240`) with its three hooks — `resolve_margin_requirement` (`native_host.hpp:976`), `margin_check_allowed` (`native_host.hpp:988`), `resolve_margin_call_units` (`native_host.hpp:995`) — and `validate_execution_precommit` (`native_host.hpp:960`), whose `AdmitWithHostMargin` verdict hands the opening check to the host | - | Risk seam | `NativeRiskLimits` (`native_run_spec.hpp:310`) with `native_risk_state()` (`native_host.hpp:1311`); the breach appends a `NativeRiskEvent` to the command history | + | Risk seam | `NativeRiskLimits` (`native_run_spec.hpp:310`) with `native_risk_state()` (`native_host.hpp:1329`); the breach appends a `NativeRiskEvent` to the command history | | State reads | `physical_position()` (`native_host.hpp:1292`) / `native_marked_equity(mark)` (`native_host.hpp:1301`) / `native_open_lots(mark)` (`native_host.hpp:1297`), `trail_state(handle)` (`native_host.hpp:1092`), and the rest of the query surface in `docs/pages/native-engine.md`, "Reading the run back" | | Lookahead hazard | the `Bar` given to `on_native_bar_open` is the *complete* script bar; `current_partial_bar()` (`native_host.hpp:1073`) is the lookahead-free bar so far, and `NativeOpenBarView::OpenOnly` (`native_run_spec.hpp:109`) masks that one callback | - **The run spec is generic, and no longer narrow.** `NativeRunSpec` @@ -180,7 +180,7 @@ summary of it. `compute_equity_stats` answers its all-NaN value with no error (`src/engine_metrics.cpp:203`, consumed by `fill_metrics_section`, `src/engine_report.cpp:119-139`) — which is why the policy exists. - **Reproducibility.** Two digests, one wrapping the other. `native_continuation_hash()` - (`native_host.hpp:1358`) returns the consumer's event/continuation hash. **A digest names + (`native_host.hpp:1376`) returns the consumer's event/continuation hash. **A digest names the run's inputs** (rule 2): the timezone identity enters it as the zone's *content* — kind, effective definition and `TimezoneIdentityDescriptor::resource_digest`, an FNV-1a over the zone files the resolver read (`native_calendar.hpp:240`) — and never as @@ -194,7 +194,7 @@ summary of it. folds that hash with position and lot state under the pinned domain `pineforge-broker-state/v19` (`engine_state_hash.cpp:33`), the closed rows as their count and a running digest each final row folds into once; it is the one exported to C - (`c_abi.cpp:504`). A host folds its **own** durable state into it through + (`c_abi.cpp:493`). A host folds its **own** durable state into it through `hash_host_extension` (`engine.hpp:405`), whose deprecated spelling `hash_source_extension` (`engine.hpp:411`) the generic default forwards to. Per-bar rows of the same hash are recorded under `KernelRecorded` with the recording switch on. @@ -207,10 +207,10 @@ into kernel driving — never by special-casing the kernel *for a specific scrip kernel and own these quirks, each at its site: - **Seams the adapter overrides.** The kernel is driven, not patched. `PineStrategyHost` overrides - `resolve_execution_terms` (`pine_strategy_host.cpp:676`) for TV sizing and fill spelling; - `validate_execution_precommit` (`pine_strategy_host.cpp:701`), whose - `validate_precommit` (`pine_adapter.cpp:12730`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:12948`) to own the opening margin decision; `on_native_tick` + `resolve_execution_terms` (`pine_strategy_host.cpp:529`) for TV sizing and fill spelling; + `validate_execution_precommit` (`pine_strategy_host.cpp:554`), whose + `validate_precommit` (`pine_adapter.cpp:12907`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:13079`) to own the opening margin decision; `on_native_tick` (`pine_strategy_host.cpp:299`) / `on_native_applied` (`pine_strategy_host.cpp:350`) 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 @@ -222,20 +222,20 @@ 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 (`tests/fixtures/e19_allin_trim` tapes the all-in residual's own entry-bar call). `process_orders_on_close` becomes - `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2318-2319`); + `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2152-2153`); calc cadence and language publication are `PineScheduler`'s (`pine_scheduler.hpp:20`). - **Batching and open-order priority.** Same-bar command batching and its deferred queues - (`PendingSameBarCommand` pine_adapter.hpp:1601, `pending_bracket_legs_` pine_adapter.hpp:1985-2003); the retained parent-before-child ordering of live handles — an + (`PendingSameBarCommand` pine_adapter.hpp:1584, `pending_bracket_legs_` pine_adapter.hpp:1996-2014); 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:1116-1186`). Kernel priority is acceptance/incarnation order. + `update_l4c_priority` `pine_adapter.cpp:975-1045`). Kernel priority is acceptance/incarnation order. - **Exit-leg lifecycle.** Activation / suspension / revival barriers (`select_exit_activation` src/compat/pine/exit_activation.cpp:42, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:10, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:46) 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:9435-9440`); the half-tick - trigger threshold (`source_trigger_threshold` pine_adapter.cpp:333) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11514`). + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:9588-9593`); the half-tick + trigger threshold (`source_trigger_threshold` pine_adapter.cpp:258) and raw-vs-booked fill spelling, behind the + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11679`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:370-376`, 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 @@ -252,12 +252,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:1996-2007`); the POOC reservation-growth population predicate + ledger (`close_logical_units_` `pine_adapter.hpp:2024-2035`); 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:13686`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:13722`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:13868`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:13937`) 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 @@ -268,7 +268,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:16421`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16715`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -317,9 +317,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:8119`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:8272`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:663`), and the two fills Pine refuses to - recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5479`). + recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5552`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the three session flags, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` @@ -372,7 +372,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:370-376`). `strategy.close` batching and the entry-id ledger - are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:1996-2007`) and + are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2024-2035`) 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:417`); what enables the generic model is the presence of `NativeRunSpec::margin`. So is the string-sentinel decoding of adapter-written comments: @@ -404,16 +404,16 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `src/c_abi.cpp` implements the **58** codegen-facing runtime `PF_API` symbols that `scripts/check_c_abi_runtime.py` pins by name, and `src/native_c_host.cpp` implements the **43** additive symbols of ``. A C host creates a host from a - callback table (`strategy_native_host_create_v1` `native_c_api.h:2639`), runs a batch - (`strategy_native_run_v1` `native_c_api.h:2652`), and **submits, replaces, cancels and - executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2680`), `_replace_v1` - (`native_c_api.h:2702`), `_cancel_v1` (`native_c_api.h:2742`), `_cancel_all_v1` (`native_c_api.h:2746`), `_cancel_where_v1` (`native_c_api.h:2767`), - `_execute_current_v1` (`native_c_api.h:2788`) — under the kernel's own legality rule. Streaming needs no new + callback table (`strategy_native_host_create_v1` `native_c_api.h:2658`), runs a batch + (`strategy_native_run_v1` `native_c_api.h:2671`), and **submits, replaces, cancels and + executes** orders — `strategy_native_submit_v1` (`native_c_api.h:2699`), `_replace_v1` + (`native_c_api.h:2721`), `_cancel_v1` (`native_c_api.h:2761`), `_cancel_all_v1` (`native_c_api.h:2765`), `_cancel_where_v1` (`native_c_api.h:2786`), + `_execute_current_v1` (`native_c_api.h:2807`) — under the kernel's own legality rule. Streaming needs no new symbol: the whole `strategy_stream_*` family takes that handle unchanged. The header's COVERAGE block carries one line per public member of `NativeStrategyHost` with either its C spelling or the reason it has none, and `scripts/check_native_c_api_surface.py` proves the block is exactly that class's public surface. `strategy_create` / `run_backtest` stay - codegen-emitted (`include/pineforge/pineforge.h:631`), which is why a C host frees its report + codegen-emitted (`include/pineforge/pineforge.h:593`), which is why a C host frees its report with `strategy_native_report_free_v1`. The 1.0 boundary is the table "The 1.0 C boundary" in `docs/pages/native-engine.md`: every C++ capability the C surface does not expose, with its reason and the checker row that pins it (a COVERAGE `[--]` row, an `ENUM_TWINS` exclusion or a @@ -426,7 +426,7 @@ citations, 264 are in `src/source/` and its headers; 6 sit in kernel files `native_selected_strategy.cpp`), both of which include `native_host.hpp`. Each C host includes ``, which includes `native_c_api.h`. Every one links `PineForge::kernel` and checks its own numbers before it prints its summary line. `PINEFORGE_BUILD_EXAMPLES` - (`CMakeLists.txt:37`, default OFF) builds them (`add_subdirectory` `CMakeLists.txt:332`) and + (`CMakeLists.txt:37`, default OFF) builds them (`add_subdirectory` `CMakeLists.txt:342`) and registers each as a CTest row that runs through `examples/native/run_example.cmake`, which fails on a nonzero exit, a signal, a timeout **or** a missing summary line — so a row asserts both, which a bare `PASS_REGULAR_EXPRESSION` cannot. `scripts/test_example_runner.py` proves @@ -573,18 +573,18 @@ name is usually a plain word. The fourth audit found these gaps (AUDIT4-opus X10 | a request on a quantity grid whose pending group deduction its units absorb -- it settles them all in one fill (`settles_in_one_fill`, `src/native_execution_consumer.cpp`, which asks `WorkingRequestCore::effective_host_units` whether the pending total takes any), so a whole-scope `ScopeFraction` resolves to its scope's held total unfloored and a host-sized close answered with that total is its own quantity, at the match and at the current execution alike | any host, by the OCA-Reduce groups and the quantity grid it declares (C: a fraction, and a close sized by `on_close_units`); the Pine adapter does not reach it: it emits no `ScopeFraction`, and it answers a close of its scope's whole total with `ExecutionGridPolicy::ExplicitUnits`, whose grid check never asks whether the request settles in one fill | **one definition of settling in one fill** (R5 lane K-OCA-KEEP, K-ULP5's finding 2). Rule 2 (a), mechanism: K-ULP4's grid rule admits the book's own quantity for a request that settles it in one fill, and K-ULP5's absorption makes a sub-ulp pending total take nothing, for a host of any venue; the two now agree. Rule 2 (b), knob: none. What moved: a run in which such a request closes its scope's total now closes it whole -- a whole-scope fraction was floored onto the grid and closed short, leaving a dust lot (before K-ULP5 such a run stopped wherever the floored units absorbed the total too, code 6, discriminator 7), and a host-sized close answered with its scope's total was refused `InvalidTerms` and left the lot open, before K-ULP5 as after: the `grid-absorbed-*` rows of `tests/test_native_group_keep_handle.cpp`, whose fraction closed 1.1000000000000001 of a 1.1000000000000227 lot on 2a03c658. K-ULP4's own battery answers every scenario's hash as before, K-ULP5's absorption and C-host batteries are unchanged (no grid), `grid-fraction-oca-pending` of `tests/test_native_unrepresentable_refusal.cpp` (a pending total that takes 0.3) is still floored, and no pinned hash moved. Pinned by `tests/test_native_group_keep_handle.cpp` (kernel-only). | | the margin model's check points on an intrabar path -- `NativeMarginCheckKind::IntrabarSample` (`include/pineforge/native_host.hpp`; C `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE`), offered at every delivered sample after the script bar's first of a `lower_tf` path matched as `ContinuousSegments`, at that sample's price, immediately before it is matched (`NativeExecutionConsumer::deliver_intrabar_script`, `src/native_execution_consumer.cpp`) | every host with a margin model and such a path, through `margin_check_allowed`; the Pine adapter admits it on a magnified run's leveraged long and, outside `process_orders_on_close`, its short at any margin (R5 lane PAR-MARGIN-2) (`PineExecutionAdapter::intrabar_sample_checked`, `src/source/pine_adapter.cpp`) | **a check point of the kernel's own** (R5 lane PAR-MARGIN, item 3; H-MEASURE's acid finding A). Rule 2 (a), mechanism: a run with no whole-bar waypoint model measures its maintenance requirement where its path is -- at each delivered sample -- the rule the docs and `native_run_spec.hpp` stated and the kernel had not kept (it checked the first sample and after fills only); nothing in it knows a venue, and a host that checks somewhere else suppresses the points it does not share, as for every other kind. Rule 2 (b), knob: none -- the path is the run's own declaration, and a one-price distribution path (`synthesized`, `DistributionSamples`) is not offered the point, since a request armed at a discrete point is matched only at a later one. What moved: a margin run on a continuous intrabar path whose book crosses its line inside a bar now books that call at the crossing sample (H-MEASURE's intrabar probe: 55 points offered, a slice at 105 where none was booked). TradingView's magnified broker does the same at its own intrabars, 2 minutes on a 15-minute chart (`tests/fixtures/intrabar_margin`: the first call at the first lower-timeframe low that crosses on 4 of 4 tapes; a model of the check at TradingView's 2-minute intrabars books every row of all 20 magnified tapes, R5 lane PAR-MARGIN-2). No pinned value moved. Pinned by `tests/test_native_margin_intrabar_samples.cpp` (kernel-only) and the "intrabar margin on tapes" section of `tests/test_adapter_margin_schedule_differential.cpp`. | | the path a post-fill margin check measures -- `NativeExecutionConsumer::margin_segment_origin` (`src/native_execution_consumer.cpp`), the phase the `AfterApplied` re-arm hands `margin_sizing_price`: the waypoint the segment into the fill's driver point starts from, with the fill price as the point's own mark | every host with a margin model on a path walked by its modeled waypoints (no intrabar path), through the `AfterApplied` point `margin_check_allowed` admits | **a mechanism correction** (R5 lane PAR-MARGIN-2). Rule 2 (a), mechanism: a request matched at a driver point was reached on the segment into it, so the book its fill leaves still faces that point's waypoint -- a limit filled on its way down to the bar's low faces the low, a short filled on its way up faces the high -- and the scan that started strictly after the point's phase measured a path the book had not finished; the waypoints the path already passed stay out, as `fx_roll_margin_check_at`'s continuous roll measures from its segment's origin. Nothing in it knows a venue. Rule 2 (b), knob: none. What moved: a post-fill point after a fill matched on a segment now measures that segment's destination waypoint -- on a high-first bar O100 H101 L80 C81 a buy limit at 95 measures 80 where it measured the close 81, and a low-first bar or a short filled on its way up is called where nothing was (`tests/test_native_margin_post_fill_path.cpp`, kernel-only); a fill at a waypoint's own price, the open's included, measures what it did. TradingView books that call on the fill's bar (lab tv tapes `tests/fixtures/margin_entry_bar/pm2-m7-lim-*` at the low, `pm2-m7-slim-*` and `pm2-m7-s1lim-*` at the high, 12 of 12; with the adapter's admissions `pm2-m7-poocl-*` and `pm2-m7b-lim-*` too), as `ab9714be`'s entry-bar suffix did; the differential's M7-B pin moved to that answer, 10 @80. One v19 witness value moved with it: `tests/test_publication_witness.cpp` Config03, a short filled on its way up a bar, now called at that bar's high (461a919ebdcd41e9 -> 4593ef08ab33e045); the corpus is 312/312, moved 0. | -| the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:1825` and `:1934`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:295`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | -| the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1478`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | -| the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1616`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | +| the presented bar -- `current_bar_`, `bar_index_`, `prev_bar_timestamp_` (`include/pineforge/engine.hpp`) | the kernel at every callback point: the consumer presents the bar at the point's instant (`src/native_execution_consumer.cpp`). For the body it runs, the Pine host then presents its own script bar. `scheduler_publish_source_bar` and `scheduler_publish_suppressed_tail` write the whole `current_bar_`, stamped at the bar's label (`current_bar_ = bar` `pine_strategy_host.cpp:1416` and `:1525`). They also write the slot's `bar_index_`, restored after a temporary publication, and each published slot's `prev_bar_timestamp_`. `PineScheduler::input` holds `bar_index_` at the input's own index for one input (`InputBarIndexScope` `pine_scheduler_native.cpp:295`) | **retained, the host's view; the fourth audit's E21 check e**. Execution reads its cursor, not this bar: `execute_current` converts at its cursor's rate and neither writes nor restores the host's clock (Section B's row). The broker-state hash folds none of the three, and `scripts/broker_state_hash_waivers.txt` waives `current_bar_` (a convenience copy) and `bar_index_` (a reconstructed cursor). While the Pine body runs, the host's bar is what these read: generated code's `time` and OHLC reads, the presented-clock readers `open_trade_profit` and `active_account_currency_fx`, and `strategy_stream_state_hash`, which folds `current_bar_` and `prev_bar_timestamp_` (`src/engine_stream.cpp`) | +| the run's words on the codegen-ABI class -- `input_tf_`, `script_tf_`, `script_tf_seconds_`, `last_bar_index_`, `last_bar_time_`, `qty_step_` (`include/pineforge/engine.hpp`) | the Pine host alone. `scheduler_prepare_script_run` sets the timeframe words and the feed's last bar from its scheduler's run spec (`script_tf_seconds_` `pine_strategy_host.cpp:1071`). `apply_realtime_tail_horizon` narrows the last bar to a live probe's horizon, and `prepare_native_begin` copies the symbol's `qty_step` | **retained as the Pine host's run facts**. Generated code reads the timeframe words. The kernel reads them for the report (`src/engine_report.cpp`), the implicit chart feed and the chart-day partition (`src/engine_aux_security.cpp`), all of which a bare host reaches with them empty. A bare host's timeframes are its `NativeRunSpec`'s, which the consumer parses into its own. `last_bar_index_`, `last_bar_time_` and `qty_step_` sit in the hashed region and are waived as feed and symbol metadata; the timeframe words sit outside it. `script_tf_seconds_`'s comment used to place `script_tf_`'s writers in `engine_run.cpp`; it now names the one writer (R5 lane H-DOCGATES) | +| the security-evaluator registry the Pine host fills -- `security_eval_states_`, `security_input_tf_`, `security_next_input_ms_` (`include/pineforge/engine.hpp`) | the Pine host. `declare_security_sites_to_kernel` hands a routable run's sites to the kernel and empties the registry (`security_eval_states_` `pine_strategy_host.cpp:1209`). `scheduler_prepare_security_sequence` sets the input timeframe and resets the next-input cursor, and the auxiliary drive and the scheduler advance that cursor (`src/source/pine_aux_security.cpp`, `src/source/pine_scheduler.cpp`) | **retained by design §2.ii row m**: the kernel keeps the registry, the aggregator and one generic step, and the Pine semantics run in `src/source/pine_security_eval.cpp`. `security_next_input_ms_` is waived: the source scheduler's deferred-boundary state is hashed on the source side. `strategy_stream_state_hash` folds the registry | | the auxiliary-feed seams -- `source_aux_security_feed_enabled`, `source_aux_security_input_view` (`include/pineforge/engine.hpp`, behind `PINEFORGE_HAS_AUX_SECURITY_FEED_V1`) -- and `last_error_` | the kernel declares both seams with an inert default (disabled, no bars: `src/engine_consumer.cpp`). It asks them once, in `prepare_native_security_feeds` (`src/engine_aux_security.cpp`), for the auxiliary slice it feeds the `request.security` evaluators. The Pine host overrides both from `strategy_set_aux_security_feed`'s bars (`src/source/pine_aux_security.cpp`). It writes `last_error_` when that setter refuses its input and when its run's preparation throws | **retained with the `auxiliary_feed` adapter-policy ruling above**. The Pine host drives its own auxiliary slice (lane N7's three measurements), and these seams are how that slice reaches the kernel's feed preparation. A bare host declares `NativeRunSpec::auxiliary_feed` and never meets them. `last_error_` is the handle's one error slot, which any layer that refuses a call writes | | `source_stream_entry_comment` (`include/pineforge/engine.hpp`) | nobody. The kernel's default (`src/engine_consumer.cpp`) and the Pine host's override (`src/source/pine_strategy_host.cpp`) both do nothing, so `stream_observe_entry` keeps the lot's own comment (`src/engine_stream.cpp`) | **dead, kept for the script ABI**. It is a virtual of `BacktestEngine`, which every generated strategy derives from, so deleting it moves every generated script's vtable: an `engine_script_run` epoch. It goes at the next one (`docs/native-refactor-progress.md`, "Scheduled for the next script-ABI epoch") | | the magnifier report facts -- `bar_magnifier_enabled_`, `diag_magnifier_sub_bars_processed_`, `diag_magnifier_sample_ticks_processed_` (`include/pineforge/engine.hpp`) | the kernel, from the run spec's intrabar path and its driver statistics (`src/native_execution_consumer.cpp`). The Pine host writes them again at every bar callback, from its scheduler's magnifier switch and the context's driver statistics (`on_native_bar_open`, `on_native_bar`) | **retained, report-only**: the report's `bar_magnifier_enabled` and magnifier totals (`src/engine_report.cpp`), outside the hashed region. The kernel publishes them for a bare host itself (the C-surface row "kernel-owned report facts" above), so the Pine host's second write is an adapter-thinness question, not a boundary one | -| `broker_fill_event_seq_` (`include/pineforge/engine.hpp`, in the hashed region) | the Pine host alone, one step per applied broker instruction (`broker_fill_event_seq_` `pine_strategy_host.cpp:595`, in `on_native_applied`); the kernel only resets and folds it | **retained, hashed: the adapter's counter on the kernel's class**. Its placement facts (`signal_close_mc_fill_seq`) compare against it in fill-time gates that cross a bar; the execution authority is the kernel's own ordinal. It is folded into the broker-state hash (`src/engine_state_hash.cpp`), so moving it to the source layer's hash extension moves every run's hash: an epoch decision, not a docs change | -| `broker_state_hashes_` (`include/pineforge/engine.hpp`) | the kernel under `KernelRecorded` (`src/native_execution_consumer.cpp`), and the Pine host at each script point it marks while recording is on (`scheduler_record_broker_hash` `pine_strategy_host.cpp:1961`) | **retained, per driving mode** (design Q4): the report's per-bar hash series, waived as append-only report output that execution never consults | -| the position book's mirror -- `position_qty_`, `position_entry_price_`, `position_entry_count_`, `pyramid_entries_` (`include/pineforge/engine.hpp`, all hashed) | the kernel books and settles them. The Pine host writes them in two places. After every applied event it erases each lot at or below `internal::kQtyEpsilon` and recomputes the position's size, average price and count (`pyramid_entries_` `pine_strategy_host.cpp:600-626`, in `on_native_applied`, restating `ab9714be`'s partial-exit settlement). Before every script body it overwrites `position_entry_count_` with its own entry-slot count (`source_entry_slot_count` `pine_strategy_host.cpp:1886`) | **OPEN, two rulings nobody has made**. The dust sweep erases a lot inside a live book, which the fourth audit found with neither a row nor a tape (its item 32): lane H-THIN's A4-DUST. The count overwrite gives one hashed member two writers with two meanings: the kernel's lots in the current direction, Pine's opened entry slots. It is recorded here first and has no lane yet | -| fields of the kernel's rows the Pine host amends -- `PyramidEntry::time`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it dates their fills at the chart bar's open (`on_native_applied`; design row AG2); their bar indices are the kernel's own script-bar indices, which it books for every host (design row AG1, R5 lane K-IDX), so the host writes no index. It writes no excursion: since R5 lane H-THIN the kernel's sampler books every Pine lot's run-up and drawdown, as it does for any host that keeps the `owns_lot_excursions()` default. It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:1277`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | -| the range-end report -- `range_end_trades_`, the extremes `max_equity_`, `min_equity_`, `max_drawdown_`, `max_runup_`, and `stream_warmup_mode_` (`include/pineforge/engine.hpp`) | the Pine host. `scheduler_record_range_end` clears the report rows, has the kernel's producer append them at the curve's last point and re-folds the four extremes from the re-marked curve (`range_end_trades_` `pine_strategy_host.cpp:1714`). `PineScheduler` raises `stream_warmup_mode_` for a stream's warmup, and the host clears it at the first realtime input or print | **adapter-policy, the `report_open_position_at_end` row above** (design RP5 and §3.7 part 2): report shape, not a mark-to-market row, and the kernel's run-end producer is gated out of every Pine run. The extremes are hashed; the report rows are waived as report-only | -| `host_mutation_guard_inert_` (`include/pineforge/engine.hpp`) | the Pine host, once, in its constructor (`host_mutation_guard_inert_ = true` `pine_strategy_host.cpp:168`) | **retained, the host's declared choice**: an in-run call of a frozen C setter is a no-op on a Pine handle, as `ab9714be`'s legacy consumer refused it, and throws on a native host. Outside the hashed region | +| `broker_fill_event_seq_` (`include/pineforge/engine.hpp`, in the hashed region) | the Pine host alone, one step per applied broker instruction (`broker_fill_event_seq_` `pine_strategy_host.cpp:437`, in `on_native_applied`); the kernel only resets and folds it | **retained, hashed: the adapter's counter on the kernel's class**. Its placement facts (`signal_close_mc_fill_seq`) compare against it in fill-time gates that cross a bar; the execution authority is the kernel's own ordinal. It is folded into the broker-state hash (`src/engine_state_hash.cpp`), so moving it to the source layer's hash extension moves every run's hash: an epoch decision, not a docs change | +| `broker_state_hashes_` (`include/pineforge/engine.hpp`) | the kernel under `KernelRecorded` (`src/native_execution_consumer.cpp`), and the Pine host at each script point it marks while recording is on (`scheduler_record_broker_hash` `pine_strategy_host.cpp:1552`) | **retained, per driving mode** (design Q4): the report's per-bar hash series, waived as append-only report output that execution never consults | +| the position book's mirror -- `position_qty_`, `position_entry_price_`, `position_entry_count_`, `pyramid_entries_` (`include/pineforge/engine.hpp`, all hashed) | the kernel books and settles them. The Pine host writes them in two places. After every applied event it erases each lot at or below `internal::kQtyEpsilon` and recomputes the position's size, average price and count (`pyramid_entries_` `pine_strategy_host.cpp:435-461`, in `on_native_applied`, restating `ab9714be`'s partial-exit settlement). Before every script body it overwrites `position_entry_count_` with its own entry-slot count (`source_entry_slot_count` `pine_strategy_host.cpp:1477`) | **OPEN, two rulings nobody has made**. The dust sweep erases a lot inside a live book, which the fourth audit found with neither a row nor a tape (its item 32): lane H-THIN's A4-DUST. The count overwrite gives one hashed member two writers with two meanings: the kernel's lots in the current direction, Pine's opened entry slots. It is recorded here first and has no lane yet | +| fields of the kernel's rows the Pine host amends -- `PyramidEntry::time`, `Trade::exit_time`, `Trade::close_cause`, `Trade::exit_id`, `Trade::exit_comment`, `Trade::entry_incarnation`, `Trade::exit_from_bracket` (`include/pineforge/engine.hpp`) | the Pine host, inside the applied notification that booked them or at a bar callback. On an aggregated or magnified chart it dates their fills at the chart bar's open (`on_native_applied`; design row AG2); their bar indices are the kernel's own script-bar indices, which it books for every host (design row AG1, R5 lane K-IDX), so the host writes no index. It writes no excursion: since R5 lane H-THIN the kernel's sampler books every Pine lot's run-up and drawdown, as it does for any host that keeps the `owns_lot_excursions()` default. It states the close cause from its own labels, clearing the kernel's `Bracket` on its rows (R5 lanes L12 and F3). It relabels an intraday-loss close's `exit_id` and `exit_comment`, swaps `entry_incarnation` on the short-seed report rows (`project_short_seed_report_rows`), and sets `exit_from_bracket` by order family (`exit_from_bracket` `pine_strategy_host.cpp:870`, in `adapter_label_bracket_trades`) | **retained: the source layer's labels and report dates on the kernel's rows**. A closed row is final for the hash only once its notification returns (`closed_rows_digest`), so the amendments made inside it are folded as the host leaves them. The hash folds a lot's time, entry bar index and two excursions (`src/engine_state_hash.cpp`), and a closed row's entry and exit times, prices, quantity and P&L (`fold_closed_row` `src/native_execution_consumer.cpp`). The labels are not folded. The kernel reads `exit_from_bracket` in one place, `closed_trade_close_cause`, after the row's recorded cause; no kernel path sets it | +| the range-end report -- `range_end_trades_`, the extremes `max_equity_`, `min_equity_`, `max_drawdown_`, `max_runup_`, and `stream_warmup_mode_` (`include/pineforge/engine.hpp`) | the Pine host. `scheduler_record_range_end` clears the report rows, has the kernel's producer append them at the curve's last point and re-folds the four extremes from the re-marked curve (`range_end_trades_` `pine_strategy_host.cpp:1307`). `PineScheduler` raises `stream_warmup_mode_` for a stream's warmup, and the host clears it at the first realtime input or print | **adapter-policy, the `report_open_position_at_end` row above** (design RP5 and §3.7 part 2): report shape, not a mark-to-market row, and the kernel's run-end producer is gated out of every Pine run. The extremes are hashed; the report rows are waived as report-only | +| `host_mutation_guard_inert_` (`include/pineforge/engine.hpp`) | the Pine host, once, in its constructor (`host_mutation_guard_inert_ = true` `pine_strategy_host.cpp:96`) | **retained, the host's declared choice**: an in-run call of a frozen C setter is a no-op on a Pine handle, as `ab9714be`'s legacy consumer refused it, and throws on a native host. Outside the hashed region | | `fold_exit_trail_peak_` (`include/pineforge/engine.hpp`, hashed) | nobody but the per-run reset to NaN (`reset_run_state`, `src/engine_run.cpp`) | **dead, kept by the hash** (design §3.7, "A dead branch found and reported"). Its readers test `isnan` and never take the branch, and the member's own comment says so. The broker-state hash folds it (`src/engine_state_hash.cpp`), so deleting it moves every run's hash and `BacktestEngine`'s layout: an epoch item | | `NativeHostCache` (`src/native_execution_consumer.hpp`) | the Pine adapter adopts its own subclass per run (`PineRunCache` in `src/source/pine_host_reads.hpp`, through `adopt_host_cache`). The consumer owns it, drops it at every run begin and never reads it | **retained as generic** (R5 lane PERF-P7): lookup state a host derives from its own bookkeeping and cannot keep in its own object, because a generated script's ABI fixes that object's layout. Nothing in it is hashed, recorded or read by the kernel, and the host verifies what it reads back. The header is not installed, so the in-tree source layer is its only user | @@ -681,13 +681,13 @@ Every line number in this section names its symbol on this tree, which | `NativeRunSpec` field | ruling | what the adapter runs instead | measurement and ruling of record | executed native consumers | |---|---|---|---|---| | `price_grid`, `grid_rounding` | **native-only** | TradingView's per-order-kind tick rules on top of `None`: `source_trigger_threshold` (`pine_adapter.cpp:258`), `source_level_on_price_grid` (`:244`), `nearest_tick` (`:182`) / `source_bar_fill_tick` (`:203`) / `directional_tick` (`:227`), 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`, `tests/test_native_price_grid.cpp` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13953`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:15008`), `chart_day_key` (`pine_adapter.cpp:13850`), `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:242`) and `configure_native` (`pine_strategy_host.cpp:243`) 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:12849-12850`): 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:2193-2195`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). 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:2387-2395`, `margin.maintenance_long` `pine_adapter.cpp:2402`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1713): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2298) | 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:2325-2327`) | 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:1584`): 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:1565`) 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 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` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13661`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14759`), `chart_day_key` (`pine_adapter.cpp:13558`), `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:242`) and `configure_native` (`pine_strategy_host.cpp:243`) 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:13026-13027`): 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:2194-2196`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). 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:2221-2229`, `margin.maintenance_long` `pine_adapter.cpp:2236`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1306): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2191) | 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:2159-2161`) | 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:1206`): 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:1158`) 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 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 close ending within it of an interior lot boundary is one selected `Flatten` of whole lots (`source_fifo_prefix_openings`, `src/source/pine_adapter.cpp`), 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.9) and kept them: made exact, the sweep fails its own tape row (`test_pine_dust_sweep_paired`) | `tests/test_native_quantity_tolerance.cpp` | @@ -713,30 +713,30 @@ number names its symbol on this tree. | C++ hook | C route | witness in `tests/test_native_c_api.c` | |---|---|---| -| `WaitForApplied::first_match` / `WaitForApplied::scope` | `arm_first_match` (`native_c_api.h:2178`), `pf_native_request_v1`'s fifth published layout; a Book-scoped child may be the host-sized close `on_close_units` sizes | the arm-relation scenario | -| `native_sized_units` | `strategy_native_sized_units_v1` (`native_c_api.h:3053`), reading a SIZED request's own sizing block | the sizing-query scenario | -| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2564`): price, opening shape, grid policy, beside the units half `on_close_units` | the terms-hook scenario | -| `validate_execution_precommit` | `on_precommit` (`native_c_api.h:2573`): the plan, the inspection and the projected account, the closed rows' P&L borrowed for the call | the precommit scenario | -| `resolve_anchored_level` | `on_anchored_level` (`native_c_api.h:2580`) | the anchored-level scenario | -| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2591`): a 64-bit digest folded after the kernel's own bytes | the hash-extension scenario | +| `WaitForApplied::first_match` / `WaitForApplied::scope` | `arm_first_match` (`native_c_api.h:2197`), `pf_native_request_v1`'s fifth published layout; a Book-scoped child may be the host-sized close `on_close_units` sizes | the arm-relation scenario | +| `native_sized_units` | `strategy_native_sized_units_v1` (`native_c_api.h:3072`), reading a SIZED request's own sizing block | the sizing-query scenario | +| `resolve_execution_terms`, price half | `on_execution_terms` (`native_c_api.h:2583`): price, opening shape, grid policy, beside the units half `on_close_units` | the terms-hook scenario | +| `validate_execution_precommit` | `on_precommit` (`native_c_api.h:2592`): the plan, the inspection and the projected account, the closed rows' P&L borrowed for the call | the precommit scenario | +| `resolve_anchored_level` | `on_anchored_level` (`native_c_api.h:2599`) | the anchored-level scenario | +| `hash_host_extension` | `on_hash_extension` (`native_c_api.h:2610`): a 64-bit digest folded after the kernel's own bytes | the hash-extension scenario | **What stays as it is, and why.** | surface | ruling | reason | |---|---|---| -| `cancel_all` (`native_host.hpp:1267`) / `cancel_where` answer a count, not a `CancelResult` per request (E12 f7) | **retained** | A bulk cancel is many commands in one call, and each withdrawn request records its own `CancelledEvent` with its own `CancelReason` — dependants of a cancelled owner included — in the history a host already polls (`strategy_native_events_v1`). That is the per-request answer; the count is the call's summary. A result vector would restate the history in a second, allocating shape that C could not take without a caller-sized array. The C spellings answer the same count (`strategy_native_cancel_all_v1` `native_c_api.h:2746`). | +| `cancel_all` (`native_host.hpp:1267`) / `cancel_where` answer a count, not a `CancelResult` per request (E12 f7) | **retained** | A bulk cancel is many commands in one call, and each withdrawn request records its own `CancelledEvent` with its own `CancelReason` — dependants of a cancelled owner included — in the history a host already polls (`strategy_native_events_v1`). That is the per-request answer; the count is the call's summary. A result vector would restate the history in a second, allocating shape that C could not take without a caller-sized array. The C spellings answer the same count (`strategy_native_cancel_all_v1` `native_c_api.h:2765`). | | `cohort_add` (`native_host.hpp:1285`) / `cohort_remove` answer `void` (E12 f7) | **retained; the typed receipt is a named follow-up** | The kernel judges every enrolment into a `CohortReceipt` (Applied, InvalidHandle, UnknownOrigin, TerminalOrigin) and folds it into the continuation (`hash_cohort_receipt` `native_execution_consumer.cpp:890`, since v19 once, at the enrolment, into the running digest the continuation carries), so a replay that diverges there diverges in the hash. Nothing that matches or settles reads it: a roster is a relation the close reads at its match, and a refused enrolment is a member the close does not take. A host that needs the receipt AT the call needs `NativeStrategyHost::cohort_add_result` first, a kernel-header change outside this lane; its C spelling would then be an `_ext_v1` with a receipt word. Until then `strategy_native_cohort_add_v1`'s `PF_NATIVE_OK` means the enrolment was issued. | -| `native_sized_units` (`native_host.hpp:1314`) / `native_liquidation_price` answer `std::optional` (E12 f7) | **retained** | Observation queries, not commands: each empty is documented cause by cause in its own comment (unconfigured, non-positive money or denominator, a below-one-step quotient; no margin model, no maintenance fraction for the side, a flat book, no finite solution), and every cause is a fact the host can read itself. A typed reason would name what the caller already knows. C answers `PF_NATIVE_ABSENT` and NaN (`strategy_native_liquidation_price_v1` `native_c_api.h:3025`). | -| `pf_native_working_v1` without the leg's anchor or owner relation (E13 f4) | **carried** | The C++ host reads them off `NativeWorkingRequest::definition`, so the C readout's relation tail carries them: `anchor` (`native_c_api.h:1683`) through `arm_scope`, the request as the kernel holds it now (an armed leg reads ABSOLUTE with its installed level). | +| `native_sized_units` (`native_host.hpp:1314`) / `native_liquidation_price` answer `std::optional` (E12 f7) | **retained** | Observation queries, not commands: each empty is documented cause by cause in its own comment (unconfigured, non-positive money or denominator, a below-one-step quotient; no margin model, no maintenance fraction for the side, a flat book, no finite solution), and every cause is a fact the host can read itself. A typed reason would name what the caller already knows. C answers `PF_NATIVE_ABSENT` and NaN (`strategy_native_liquidation_price_v1` `native_c_api.h:3044`). | +| `pf_native_working_v1` without the leg's anchor or owner relation (E13 f4) | **carried** | The C++ host reads them off `NativeWorkingRequest::definition`, so the C readout's relation tail carries them: `anchor` (`native_c_api.h:1694`) through `arm_scope`, the request as the kernel holds it now (an armed leg reads ABSOLUTE with its installed level). | | `hash_source_extension` (`engine.hpp:411`) | **C++-only** | The deprecated spelling of `hash_host_extension`, kept so an existing C++ subclass compiles and folds unchanged. A C host has only ever had the current spelling, `on_hash_extension`. | | `NativeReportPolicy::KernelRecordedAtHostMarks` (`native_run_spec.hpp:64`) | **C++-only** (lane E22's ruling, restated) | Under it the host names each report point from inside its own callbacks, and the C table has no call that marks one: a C host could only declare a series nobody records. `pf_native_report_policy_e` (`native_c_api.h:661`) leaves its value unnamed, and the enum guard holds that exclusion. A C++ host now reaches the same kernel producer through `NativeStrategyHost::mark_native_report_point`; a mark outside a running HostMarks run returns false. | -| `strategy_set_path_order` (`pineforge.h:967`) clamps an out-of-range mode to AUTO | **retained** | A `void` ABI v4 setter cannot answer a refusal. Its values are now documented as `pf_native_path_order_e`'s (`native_c_api.h:904`); a C host that wants an unknown word refused declares `path_order` in `pf_native_run_spec_ext_v1`, which answers `PF_NATIVE_E_TAG`. | -| the enumerated `int32` fields of `pf_pending_order_v1_t` (`strategy_pending_order_get` `pineforge.h:1061`) | **no C names in the header; generator-owned** | The mirror is generated from the Pine source host's resting-order record by `scripts/gen_pending_order_mirror.py` (held by its `--check` source guard) and self-described at run time by `strategy_pending_order_layout`. Its enumerated fields are the source layer's order vocabulary, not a kernel enumeration. A C name for them belongs in that generator, which owns the struct, so that it cannot drift from the mirror — never hand-written beside it. | -| `strategy_configure_native_v1` (`c_abi.cpp:896`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:3982`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | -| `strategy_configure_native_fx_curve_v1` (`pineforge.h:584`) | **typed additive route** | `strategy_configure_native_fx_curve_ext_v1` writes the C twin of `NativeFxCurveValidation` and its offending index. The old `-1` route remains unchanged, while a C caller can now distinguish a decreasing timestamp, non-positive rate, allocation failure or wrong phase. | +| `strategy_set_path_order` (`pineforge.h:931`) clamps an out-of-range mode to AUTO | **retained** | A `void` ABI v4 setter cannot answer a refusal. Its values are now documented as `pf_native_path_order_e`'s (`native_c_api.h:904`); a C host that wants an unknown word refused declares `path_order` in `pf_native_run_spec_ext_v1`, which answers `PF_NATIVE_E_TAG`. | +| the enumerated `int32` fields of `pf_pending_order_v1_t` (`strategy_pending_order_get` `pineforge.h:1026`) | **no C names in the header; generator-owned** | The mirror is generated from the Pine source host's resting-order record by `scripts/gen_pending_order_mirror.py` (held by its `--check` source guard) and self-described at run time by `strategy_pending_order_layout`. Its enumerated fields are the source layer's order vocabulary, not a kernel enumeration. A C name for them belongs in that generator, which owns the struct, so that it cannot drift from the mirror — never hand-written beside it. | +| `strategy_configure_native_v1` (`c_abi.cpp:885`) versus `strategy_configure_native_ext_result_v1` (`native_c_host.cpp:3982`) | **retained legacy base plus typed additive route** | A base specification the kernel rejects still returns `-1` and latches `Failed`, preserving its frozen caller contract; a Ready misuse retains that Contract latch. The C boundary refuses arguments, a Running phase and a Completed hash hook without entering kernel configure, so a read cannot reconfigure its host. The extended result call validates before its first configure, writes `pf_native_spec_error_t` / `pf_native_spec_field_t`, and names reused-host session-key and run-number refusals; no old signature or layout moved. | +| `strategy_configure_native_fx_curve_v1` (`pineforge.h:546`) | **typed additive route** | `strategy_configure_native_fx_curve_ext_v1` writes the C twin of `NativeFxCurveValidation` and its offending index. The old `-1` route remains unchanged, while a C caller can now distinguish a decreasing timestamp, non-positive rate, allocation failure or wrong phase. | | `on_native_timeframe_bar` (`native_host.hpp:885`) and `NativeTimeframeBarContext::interval` (`native_host.hpp:809`) | **query routed** | The callback signature stays frozen. `strategy_native_timeframe_bar_interval_v1` is valid only during that callback and copies the kernel calendar interval without deriving it from the delivered bar or delivery timestamp. | -| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7663-7675`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1085-1090`) sees the same values, so no hash contract moves. | -| `pf_native_lot_excursion_v1` (`native_c_api.h:1869`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | +| `magnifier_sub_bars_total` / `magnifier_sample_ticks_total` (`engine_report.cpp:45-46`) | **kernel-owned report facts** | The generic intrabar driver already increments `NativeDriverStatistics::sub_bars_processed` / `sample_ticks_processed` for every retained path (`native_execution_consumer.cpp:7711-7723`); the kernel now publishes those same counters for bare hosts. They are report fields, not new durable state: the existing `native_continuation_hash` fold of `NativeDriverStatistics` (`native_execution_consumer.cpp:1086-1091`) sees the same values, so no hash contract moves. | +| `pf_native_lot_excursion_v1` (`native_c_api.h:1888`) | **additive fee tail** | `entry_commission` carries the C++ `ClosedLotExcursionFacts::entry_commission` in account currency. The callback table publishes a trailing layout marker so F4 callers still receive the base facts length; current callers receive the fee tail without changing `PF_NATIVE_API_VERSION`. | ## TradingView-calibrated kernel mechanisms (rule 2's own account) @@ -782,19 +782,19 @@ The table records, at the tree the doc wave read, what each block described and | kernel site | the mechanism | (a) generic because | (b) a knob would be | pinned by | |---|---|---|---|---| -| `utc_floor_day_ms` `src/timeframe.cpp:154` (the `TradingView` comment `sha256:1868eeadaab06b664c5ba966a86e70d6de2ad74d3645afe1846ed736d0c2afce` :146-149) | daily boundaries are anchored at symbol-local midnight | the anchor is `spec.timezone`'s data, not a platform's; another venue is driven by naming its zone | a "which midnight" field over a zone the spec already names | `tests/test_native_calendar.cpp`, `tests/test_session_day_anchors.cpp` | -| `session_period_last_traded_close_ms` `src/timeframe.cpp:814` (the `TradingView` comment `sha256:a650d166e35a1e22188ff5c3ee50c5534519730913a3fe713cf0b93b0f66d4ca` :808-817) | a D/W/M period is finished at the last traded close of its last session | every venue calendar must answer "is this period over"; the answer is computed from `spec.session` and `spec.timezone` | a per-run completion-rule switch over a choice the session template already makes | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_session_calendar_extra.cpp` | +| `utc_floor_day_ms` `src/timeframe.cpp:154` (the `TradingView` comment `sha256:1868eeadaab06b664c5ba966a86e70d6de2ad74d3645afe1846ed736d0c2afce` :147-150) | daily boundaries are anchored at symbol-local midnight | the anchor is `spec.timezone`'s data, not a platform's; another venue is driven by naming its zone | a "which midnight" field over a zone the spec already names | `tests/test_native_calendar.cpp`, `tests/test_session_day_anchors.cpp` | +| `session_period_last_traded_close_ms` `src/timeframe.cpp:814` (the `TradingView` comment `sha256:a650d166e35a1e22188ff5c3ee50c5534519730913a3fe713cf0b93b0f66d4ca` :804-813) | a D/W/M period is finished at the last traded close of its last session | every venue calendar must answer "is this period over"; the answer is computed from `spec.session` and `spec.timezone` | a per-run completion-rule switch over a choice the session template already makes | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_session_calendar_extra.cpp` | | the session-day split `crosses_boundary` `src/timeframe.cpp:902-908` | calendar periods split on the session-day clock, not the civil one | continuous-session instruments need a session-day clock whatever platform asks; the class comes from `spec.type` | hash-visible surface for what `spec.type` decides | `tests/test_session_day_anchors.cpp` | | the aggregated bar's label `bar_label_ms` `src/timeframe.cpp:1064-1071` | an aggregated bar is dated by the period it opens | a label rule is needed to key any bucket; this one is the period key | a label-policy field for a key the aggregator owns | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_calendar_wm_open_utc_fastpath.cpp` | | real-end and chart-close completion (`Real-end completion` `src/timeframe.cpp:1191-1212`, `Chart-close completion` `:1236-1255`) | a thin or session-clipped intraday bucket completes on the bar whose end reaches the bucket end, or on the session's last bar | both are "the period is over" tests over feed data; a gap-free 24x7 feed is bit-identical either way, which the comment states and the tests pin | a completion-mode field over a bucket the calendar already closes | `tests/test_calendar_aggregation_wm.cpp`, `tests/test_native_htf_subscriptions.cpp` | -| the native period partition installed by `set_native_periods` `src/timeframe.cpp:1617` (the holiday-pause `TradingView` comment `sha256:7bd87c1e6dfe78729221d412952249a8cb427934ef498d3a4deb6812f7af05d7` :1405-1410) | a session that pauses and reopens the same day is one session day | the rule reads the feed's own stamps; the holiday is data | a holiday table in the kernel | `tests/test_calendar_aggregation_wm.cpp` | +| the native period partition installed by `set_native_periods` `src/timeframe.cpp:1617` (the holiday-pause `TradingView` comment `sha256:7bd87c1e6dfe78729221d412952249a8cb427934ef498d3a4deb6812f7af05d7` :1401-1406) | a session that pauses and reopens the same day is one session day | the rule reads the feed's own stamps; the holiday is data | a holiday table in the kernel | `tests/test_calendar_aggregation_wm.cpp` | | `bar_path_uses_high_first` `src/engine_path_resolve.cpp:28`, `compute_ohlc_path_legs` `src/magnifier.cpp:97`, `entry_stop_first_touch` `src/engine_internal.hpp:101-109` | with only OHLC the intrabar walk order is unknowable, so the open-proximity heuristic picks the first leg | **and the knob exists**: `NativeRunSpec::path_order` (`NativePathOrder` `native_run_spec.hpp:335`) states `HighFirst` / `LowFirst` explicitly | already spelled, and `Auto` is the default that moves no identity | `tests/test_native_run_spec.cpp`, `tests/test_native_price_grid.cpp` | | `round_to_mintick` `include/pineforge/engine.hpp:753` (the `TradingView` comment `sha256:ba90d695d99260d555d1f44bd05110f4977eeb1e54323e6362ea6bd451572399` :737-750) | nearest-tick rounding, ties away from zero | a tick ladder is the instrument's, declared as `price_tick`; the generic surface over it is `NativePriceGrid` + `NativeGridRounding` | TradingView's **per-order-kind** quantization is exactly the knob that would not be generic — it is the measured content of the `price_grid` native-only ruling above | `tests/test_native_price_grid.cpp` | | `ta::ATR` `include/pineforge/ta.hpp:321` (the `TradingView` comment `sha256:4f6f6939aec2773684b114a13dd002eb58990d6d96c3056ad87361753faf7a0a` :333-340) | average true range over Wilder's `RMA` | Wilder's ATR is the textbook definition; the comment records which convention and the tape that pinned the recompute behaviour | a seeding/averaging field on an indicator whose definition is the convention | `tests/test_ta_rma_warmup.cpp`, `tests/test_recompute.cpp` | | `ta::StdDev` `include/pineforge/ta.hpp:430` (the `TradingView` comment `sha256:7cc1bbcd32dfe673285d51a8ca7080890ac551858099b523480d998f46408666` :420-429) | the population (biased) standard deviation | biased vs sample is a textbook choice, and the kernel states which | a per-instance switch is the *right* answer where two conventions genuinely compete, which is why `ta::EmaSeeding` exists and this one does not need it | `tests/test_ta_indicators_extras.cpp` | | `float_band_eq` `include/pineforge/ta_compare_band.hpp:38` | an absolute 1e-10 float comparison for indicator equality | an epsilon band is needed by any float comparison; the width is the calibration | a band-width field on a comparison the indicators share | `tests/test_dmi_parity.cpp`, `tests/test_ta_osc_edge.cpp` | -| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8621-8624` (lookahead) and `clear_if_gapped` `:8615-8618` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | -| `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2228`, `::gaps` `include/pineforge/native_c_api.h:2232` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:799`, `pf_native_gaps_e` `:810`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | +| the two delivery rules, `projected_first_index` `src/native_execution_consumer.cpp:8685-8688` (lookahead) and `clear_if_gapped` `:8679-8682` (gaps) | deliver a completed bucket at its **first** contributing bar (lookahead), and **clear** the series on a bar it publishes nothing on (gaps) | both are delivery rules over a bucket the aggregator already closed; neither reads a platform. They are spec fields: `NativeTimeframeSubscription::lookahead` / `::gaps` | already spelled, and both default to the rule that moves no identity | `tests/test_native_htf_subscriptions.cpp` | +| `pf_native_subscription_v1::lookahead` `include/pineforge/native_c_api.h:2247`, `::gaps` `include/pineforge/native_c_api.h:2251` | the C spelling of those two rules | since R5 lane E7 both words are typed and named after the kernel's own delivery rules (`pf_native_lookahead_e` `include/pineforge/native_c_api.h:799`, `pf_native_gaps_e` `:810`); the C header no longer names `barmerge` at all, and the migration page maps Pine's words onto them — a *pointer to the reader's vocabulary*, not a justification | — | `tests/test_native_c_api.c` | | the authoritative-feed partition `src/engine_aux_security.cpp` | a feed is the venue's own bars of one timeframe; its stamps are the period partition | already ruled: design §2.ii row s, and the residual table's own row | the policy knob is installing the feed or not | `tests/test_native_htf_subscriptions.cpp`, `tests/test_native_security_feed.cpp` | | `session_template_knows_early_close` `src/engine_security.cpp` | the instrument-class classification | already ruled: design §2.ii row t; `SymInfo::type`'s vocabulary is fixed by the frozen C ABI | — | `tests/test_native_wm_buckets.cpp` | | `skip_entry_bar_high` / `skip_entry_bar_low` `include/pineforge/engine.hpp:153` | the intrabar-fill excursion mask: the part of a bar traversed before a priced entry filled is not that trade's excursion | the mask is generic — an excursion is measured from the fill — and since R5 lane E6 no host raises the flags: the excursion owner declares where its fill sat and the kernel derives the pair from the bar's path (`declare_opened_lot_entry_bar_mask` `src/engine_path_resolve.cpp:95`); with no declaration the kernel samples the whole bar | already spelled the generic way: `owns_lot_excursions()` + `closed_lot_excursion` hand the whole measurement to a host | `tests/test_l11a_host_excursion.cpp` | @@ -807,14 +807,14 @@ The table records, at the tree the doc wave read, what each block described and | the indicator library -- `include/pineforge/ta.hpp`, `src/ta_moving_averages.cpp`, `src/ta_oscillators.cpp`, `src/ta_extremes_volume.cpp`, `src/ta_volatility_trend.cpp`, `src/ta_misc.cpp`, `include/pineforge/window_sum.hpp`, `include/pineforge/math.hpp` | each indicator implements its Pine `ta.*` / `math.*` definition -- warm-up and na rules, the window call-site ring, the compensated window sum, tie and epsilon rules -- and its comments record the TradingView tape or export that calibrated each number (the `ta::ATR` and standard-deviation rows above are two of them) | an indicator is a function of its inputs, the same for every venue; generated strategies call exactly these classes, and where two textbook conventions genuinely compete the choice is a per-instance option (`ta::EmaSeeding`), never a branch on the platform | a convention switch on every indicator, for a choice the script already made by calling that Pine function | the kernel-only TA suites `tests/test_ta_*.cpp` and the corpus parity gate | | the formatting library -- `include/pineforge/str_utils.hpp`, `src/str_utils.cpp` (`str_tostring`, `str_format_values`, `str_format`) | the number text of Pine's `str.tostring` / `str.format` built-ins: a number's shortest round-trip decimal spelling, scaled and rounded half-up on its digits, the default, `percent`, `volume` (K / M / B / T) and pattern modes, MessageFormat placeholders and quoting, `NaN` and `Infinity` -- the published rendering TradingView's exported tapes pin (R5 lane C6), ported from the codegen formatter (lane C6b) by R5 lane B-ENGINE; the `mintick` mode, which generated code delegates to it, is kept as it was | a formatting library is a function of its inputs, the same for every venue, as the indicator library is: it spells a number the way the source language's own `str.*` library defines, and no kernel order, fill, settlement, margin or hash code calls it -- of the thirty-five kernel translation units only `src/str_utils.cpp` names these functions | a rendering switch on functions whose definition the calling script chose by calling them | `tests/test_str_number_format.cpp` (lane C6b's 42-case edge battery, every numeric string of lane C6's TradingView exports, byte-identical output on macOS arm64 and Linux x86-64) and `tests/test_str_utils.cpp` | | the language calendar functions -- `include/pineforge/session_time.hpp`, `src/session_time.cpp` (`timeframe_time`, `local_hour` and its siblings, `session_in_market`, `session_trading_day_open_ms`, ...) | the definitions of Pine's `time()` / `time_close()` / bare-time / `session.is*` built-ins for generated code: session and timezone parsing, the invalid- and 24-hour-session spellings, `time("60")` day keying, `session.is*` on daily charts, each comment naming the tape that pinned it | the inputs are data -- a session string, a timezone, a timeframe -- so another venue is driven by its own session template; nothing branches on a platform, and a native host's calendar is the separate generic `native_calendar` | a rules switch on functions whose definition the generated script chose by calling them | `tests/test_session_time.cpp`, `tests/test_pine_time_day_stamp_grid.cpp` and the corpus parity gate | -| `present_session_day` `src/native_execution_consumer.cpp:7286`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (`tests/fixtures/session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | `tests/test_native_session_day_facts.cpp` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), the C scenario of `tests/test_native_c_api.c`, `tests/test_session_day_facts_adapter.cpp` (the adapter selects them flag for flag), and `tests/test_native_session_day_utc.cpp` (kernel-only: on a UTC calendar the facts read an in-session instant without resolving its interval, and match the zone path at every decision point) | -| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7927-7943` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | -| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7904-7925` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | -| `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6618` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | -| `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:6342` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | -| the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:334`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1541`, `:1556`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:1854`). A bare host never calls the builder, so its calendar stays nominal | `tests/test_o_close_pct_day_anchor.cpp` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | +| `present_session_day` `src/native_execution_consumer.cpp:7298`, presenting `NativeDecisionContext::in_session` `include/pineforge/market_driver.hpp:161` and the three facts after it | a script bar's session-day facts: in session on the run's calendar; it opens / closes its session day when the bar before / after it is out of session or on another session day, that bar being the one the run holds (batch input, stream warmup) and otherwise the calendar's previous or next eligible input slot across declared breaks; the run's first bar opens its day, a batch's final bar closes it, a D/W/M bar holds whole days | the session, the timezone and the session day are the run's own calendar (`spec.session`, `spec.timezone`, `native_calendar::session_day_ordinal`), the neighbours are the run's own input, and a second venue drives it through that data alone. The reading is TradingView's session DAY, measured on its tapes by lanes E25/E26 (`tests/fixtures/session_islastbar`: 255/255 NYSE:F last bars, 255/255 first, 370/370 ETH last), which a bare host now reproduces from the kernel alone | a switch over the run-end convention or the neighbour rule would put a hash-visible choice on the surface for what the run's own input already decides. The one host that reads its batch end differently — a live probe recomputing a batch whose last input is still forming — reads a second fact, `closes_session_day_open_ended`, not a switch; the facts are presentation and fold into no digest | `tests/test_native_session_day_facts.cpp` (kernel-only: the tapes, the audit's Tokyo `2230-0500` session, streams, a fill recalculation), the C scenario of `tests/test_native_c_api.c`, `tests/test_session_day_facts_adapter.cpp` (the adapter selects them flag for flag), and `tests/test_native_session_day_utc.cpp` (kernel-only: on a UTC calendar the facts read an in-session instant without resolving its interval, and match the zone path at every decision point) | +| `NativeExecutionConsumer::script_bucket_completions` `src/native_execution_consumer.cpp:7956-7972` | before a run, how an input span aggregates into script bars: which input bars complete one, and which complete one on a period boundary, under the run's own timeframes, zone and session (R5 lane B-ADAPTER, item 4) | any host that aggregates a finer feed asks it before its first input; the answer is the run's own `TimeframeAggregator` over the span and spec fields -- the real-end / chart-close completion and the session-day split this table already rules generic -- and nothing branches on a platform | none: a const query with no spec field, nothing hashed and no state moved; the host decides what the answer means for it (the Pine scheduler reads it instead of running its own preview aggregator) | `tests/test_native_script_bucket_completions.cpp` (kernel-only: the Pine scheduler's preview on the K1 zones, across DST, windowed sessions and 1D/1W/1M) | +| `PhysicalExecutionContext::account_fx` `include/pineforge/execution.hpp:92`, read by `build_close_trade_with_costs` `src/engine_orders.cpp:70-87` and handed by `append_open_position_report_rows` `src/native_execution_consumer.cpp:7933-7954` | a closing or mark-to-market row converts at the account-currency rate its execution names -- a mark at a report point's instant, a fill at its own cursor -- and at the presented clock's rate when it names none, as before (R5 lane B-ADAPTER, item 3) | the rate is data a host of any venue supplies with the row it asks for; nothing branches on a platform, and the row's currency is the basis its owner's magnitudes are recorded in | none: no spec field; a caller names the rate per call, and a call naming none converts exactly as before | `tests/test_adapter_range_end_fx.cpp` (a stepped FX curve at seven instants around the range end, plain and aggregated charts) | +| `NativeExecutionConsumer::execute_current` `src/native_execution_consumer.cpp:6630` | a current execution converts at its cursor's rate, threaded through its terms facts, inspection, preview and settlement contexts, and neither writes nor restores the host's presented clock, so its own hooks and the host after it see the frame's clock; its preview `inspect_current_execution` still converts at the presented clock (R5 lane B-ADAPTER, item 5) | an execution converting at its own instant's rate is the same for every host and venue, and the host's presented clock stays its own | none: the rate is the one the run already chose (the cursor's instant), carried as data | `tests/test_native_current_execution_rate.cpp` (kernel-only) | +| `NativeExecutionConsumer::validate_current_execution` `src/native_execution_consumer.cpp:6355` | a current execution refuses a request bound to a host roster (`BindCohort`) as `UnsupportedRequest`, before its point is taken and whatever the roster holds: it admits exactly the owners the evaluation's current shape executes (`current_shape` `src/native_order.cpp:383`: `Independent`, `BindOpening`, `BindOpenings`). Since R4 slice C it had admitted `BindCohort` as well, which the evaluation then left unevaluated, so a roster holding a live lot failed the run with "native current evaluated allowance mismatch" and an empty one answered `UnreadyOwner`; its read-only preview (`inspect_current_execution`) settled what the execution could not (R5 lane PAR-ORDERS-2) | one legality rule, the evaluation's own, decides a command before any point is taken, the same for every host and for the C surface (`strategy_native_execute_current_v1` answers through this validation); a roster read at the match is not a membership fixed at the command, which is what the documented contract names (`docs/pages/native-engine.md` "Selected exposure and current execution") | none: no spec field or hook. A refused request stays queued and fills at the next match like any market close; a host that wants a cohort's lots closed at once fixes its roster first (`BindOpenings`), as the Pine adapter's immediate closes do | `tests/test_native_current_cohort_refusal.cpp` (kernel-only: a live and an empty roster refused, then filled as queued closes; a fixed roster executing at once) , `tests/test_native_cohort_sliced_close.cpp` (its preview rows: the roster close's preview is refused, the same selection's settles) and `tests/test_adapter_live_state_equivalence.cpp` (reversals seed 3246599, which the failure used to end) | +| the chart symbol's daily partition -- `prepare_chart_day_partition` `src/engine_aux_security.cpp:334`, kept in `chart_day_partition_` `include/pineforge/engine.hpp:1218` and presented to the language runtime through `RuntimeAmbient::day_partition` `src/runtime_ambient.hpp:38` (the TradingView comment above `NativeDayPartition` in `include/pineforge/timeframe.hpp`) | on an intraday chart whose run installed its own symbol's `D` feed, the chart-level D period follows that feed's stamps: the exchange's trade-date bars, a holiday-merged day included. It governs `time("D")`, `timeframe.change("1D")`, `ta.change(time("D"))`, the default-anchored `ta.vwap` and `crosses_boundary(DAY)`. Without the feed, on a 1D chart or on another symbol's clock every rule is the nominal session day | the stamps are the venue's own data, as for the authoritative-feed partition above: another venue is driven by installing its own daily bars, and nothing branches on a platform | the policy knob is installing the feed or not, so a "which daily partition" field would restate it. Only the Pine host builds it (`prepare_chart_day_partition` `pine_strategy_host.cpp:1134`, `:1149`) and presents it for each bar it publishes (`AmbientDayPartitionScope` `pine_strategy_host.cpp:1445`). A bare host never calls the builder, so its calendar stays nominal | `tests/test_o_close_pct_day_anchor.cpp` (the partition is built from the D feed, and none without it) and `tests/test_chart_day_memo.cpp`; the tape is `lab tv` `o-cme-dayanchor-full`, recorded at the site | | the bar-time accessors -- `_decompose_bar_time` and `_bar_hour` through `_bar_weekofyear` `include/pineforge/engine.hpp:1066-1093` | the presented bar's stamp broken into calendar fields in UTC, memoized per stamp | a function of the stamp alone, and UTC is the engine's storage timezone | none: they read no spec field and fold into no digest. They are protected `BacktestEngine` members, part of the codegen contract. Their comment claimed that generated code reads them for Pine's bare `hour` / `minute` / `dayofweek`. It does not: the codegen transpiler emits `pine_hour(current_bar_.timestamp, syminfo_.timezone)` and its siblings on the exchange timezone (`pineforge_codegen/codegen/tables.py` at the corpus's pinned commit `f3285d79`), which are the language calendar functions above. No generated strategy in `corpus/` names a `_bar_*` accessor. The comment now says so (R5 lane H-DOCGATES) | `tests/test_chart_timezone.cpp` (`_bar_hour()` is the UTC hour) and `tests/test_intraday_rollover_chart_tz_l4a.cpp` | -| the conventional offset spellings of `timezone_accepted` `include/pineforge/native_calendar.hpp:201` (its comment's "TV sign" line) | `UTC+05:30` and `GMT-4` mean what they say: five and a half hours ahead of UTC, four behind (ISO 8601's sign), where a POSIX `TZ` string of the same shape means the opposite; a POSIX rule string keeps POSIX's own sign | a zone spelling is data, and the sign is the one a venue printing `UTC+8` means; nothing branches on a platform | a sign-convention switch over a spelling whose meaning is fixed | `tests/test_native_calendar.cpp` (`resolve_civil("UTC+05:30", …)` and `resolve_civil("GMT-4", …)` against their fixed instants) | +| the conventional offset spellings of `timezone_accepted` `include/pineforge/native_calendar.hpp:208` (its comment's "TV sign" line) | `UTC+05:30` and `GMT-4` mean what they say: five and a half hours ahead of UTC, four behind (ISO 8601's sign), where a POSIX `TZ` string of the same shape means the opposite; a POSIX rule string keeps POSIX's own sign | a zone spelling is data, and the sign is the one a venue printing `UTC+8` means; nothing branches on a platform | a sign-convention switch over a spelling whose meaning is fixed | `tests/test_native_calendar.cpp` (`resolve_civil("UTC+05:30", …)` and `resolve_civil("GMT-4", …)` against their fixed instants) | | the grid and lot helpers -- `tick_grid_price` `include/pineforge/engine.hpp:799`, `price_grid_decimals` `include/pineforge/engine.hpp:833`, `level_on_price_grid` `include/pineforge/engine.hpp:843`, `apply_qty_step` `include/pineforge/engine.hpp:932`, `apply_exit_qty_step` `include/pineforge/engine.hpp:952` (their TradingView calibration comments sit above each) | nearest-tick and price-grid quantization and the lot-increment floors, each calibrated on TradingView tapes | **not a kernel mechanism**: no kernel or source-layer code calls any of them (`tests/test_f6_dead_kernel_members.cpp`, witness 2). The adapter runs its own copies of TradingView's per-order-kind tick rules (ruled native-only in the `price_grid` row above) and sizes its own lots; a native host declares a price grid and a quantity grid in its run spec | nothing to spell. They are rule 5's legacy, kept only because the twin-parity freeze (`scripts/check_twin_parity.py`, base `ab9714be`) holds the checks of the suites that reach them, and they leave with those checks | `tests/test_stop_tick_rounding_l4d.cpp`, `tests/test_level_grid_snap_l4d.cpp`, `tests/test_qty_step_epsilon_floor_l4b.cpp` | The rows above account for the whole population. R5 lane H-DOCGATES re-took it on its own tree diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index 865ed2c4..af4e2298 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,123 +47,123 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2570-2581; `entry()` pine_adapter.cpp:5127, `order()` pine_adapter.cpp:10323) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5698-5745; `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:8417 → `Flatten{}` pine_adapter.cpp:8446-8447; `Reduce{ExplicitUnits}` pine_adapter.cpp:7555-7564; `ReverseTo` pine_adapter.cpp:6377-6378) | **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:2540-3747 → `host.replace` pine_adapter.cpp:2556-2636) | **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:4551-4597, `cancel_all` pine_adapter.cpp:17646-17661; `live_by_source_key_` pine_adapter.hpp:1968; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3897-3962) | **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:4190; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7240-7591) | **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:5351-5386, `cohort_for` pine_adapter.cpp:2501; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7310-7327) | **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:2579-2585) | **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:2600-2611; `entry()` pine_adapter.cpp:5197, `order()` pine_adapter.cpp:10476) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5710-5757; `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:8333 → `Flatten{}` pine_adapter.cpp:8362-8363; `Reduce{ExplicitUnits}` pine_adapter.cpp:7449-7458; `ReverseTo` pine_adapter.cpp:6289-6290) | **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:2570-3777 → `host.replace` pine_adapter.cpp:2586-2666) | **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:4596-4642, `cancel_all` pine_adapter.cpp:17940-17955; `live_by_source_key_` pine_adapter.hpp:1996; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3927-3992) | **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:4052; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7121-7471) | **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:5421-5456, `cohort_for` pine_adapter.cpp:2335; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7454-7471) | **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:2609-2615) | **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:8302; lowering `submit_one` pine_adapter.cpp:8884-8896, `BindCohort` pine_adapter.cpp:9385-9391, `submit_leg` pine_adapter.cpp:9409-9433; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8482-8485; `materialize_relative_exits` pine_adapter.cpp:16936-16977, pine_adapter.hpp:1793-1805) | **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:5624-5658. `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:1231-1247; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:18697-18726) | **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:1970-1988); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:975-1054); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3301-3468, `materialize_snapshot` pine_adapter.cpp:10723-10747), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1557-1580, `CloseCallsiteState` pine_adapter.hpp:1651-1666; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15248, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15482) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:670-672 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:1010-1017; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:714-746) | 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:1677); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1751). 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:8455; lowering `submit_one` pine_adapter.cpp:9037-9049, `BindCohort` pine_adapter.cpp:9538-9544, `submit_leg` pine_adapter.cpp:9562-9586; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8635-8638; `materialize_relative_exits` pine_adapter.cpp:17230-17271, pine_adapter.hpp:1810-1822) | **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:5636-5670. `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:1231-1247; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19030-19059) | **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:1998-2016); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:975-1054); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3331-3498, `materialize_snapshot` pine_adapter.cpp:10876-10900), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1563-1586, `CloseCallsiteState` pine_adapter.hpp:1657-1672; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15469, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15703) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:669-671 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:869-874; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:714-746) | 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:1677); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1751). What `tests/test_adapter_margin_revival_cancel.cpp` replays on the NYSE:F 15m chart of seven lab tv tapes: badapter-v19dp1-control (no cancel: the margin call closes 928 through X) and v19fix-gapped-stop-control (X's stop at 10.15, gapped by the 05-01 open and parked: the same close), and badapter-v19dp1-cancel, v19fix-cancel-before-pair, v19fix-cancel-at-1000, v19fix-gapped-stop-cancel and v19fix-gapped-stop-cancel-after-pair (a cancel on 05-01 09:30 while X is dormant, on 04-30 15:45 before the reversal pair and on 04-30 10:00 while X is live, and on 05-01 09:30 and after the pair while X is parked; one tape file: margin calls 12 @10.39 and 92 @10.57, 836 open). Untaped, the same row holds the tapes' trades for `strategy.cancel_all` on 05-01 and before the pair and for `strategy.cancel` after the pair, and on the same bars with no quantity grid — where the pair holds X in the cancel's own callback — every spelling books what the cancel on the dormant exit books | ### 1.2 Trailing | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:5710-5732; `NativeTrailState` native_host.hpp:669-674, `trail_state` native_host.hpp:1092 | K | — | F:B1 O:B1 S:T1 | — | +| TR1 | Trail arm → track → trigger, with a state query | **yes** — `Trail{offset, arm_price}` native_order.hpp:290-293; states `TrailWaitArm` native_order.hpp:642-660; arm / track / fire `TrailWaitArm` native_execution_consumer.cpp:5722-5744; `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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11622-11634) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4464-4469) **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:11535-11672) stays A. | -| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:3854-3900) | **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:287-289) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9435-9440, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11535-11672, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8452-8678, `trail_one_shot` pine_adapter.cpp:9524-9653 | 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:11787-11799) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4473-4478) **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:11700-11837) stays A. | +| TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:3884-3930) | **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:287-289) is A. | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9588-9593, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11700-11837, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8605-8831, `trail_one_shot` pine_adapter.cpp:9677-9806 | 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:5740-5744, pine_adapter.cpp:5740) | **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:10989-12193) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:4816, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4935-4969 | 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:2428-2483) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4976-4978 **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: rounded equity `source_money_round` pine_adapter.cpp:2380, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:378 (→ SZ10) | -| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2389-2391) | **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:2441, `typed_entry_units` pine_adapter.cpp:2459; `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:2449; `percent_commission_live_equity` pine_adapter.cpp:4190-4231) | **no** — only a dead primitive, `reserve_percent_commission` (since deleted), with no caller in `src/` **Closed:** `Sized::reserve_percent_fee` divides the sizing cash by `1 + fee_value / 100` under `NativeFeeKind::Percent` and is an exact no-op for every other fee kind; the dead `reserve_percent_commission` primitive is gone (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K | L3 | F:C3 O:C6 S:S3 | **Disagree:** F lists the reserve under quirk yet offers a knob / O kernel, "partial" / S knob. R5-1: fee-net option is generic. TradingView's cash-commission reserve stays adapter policy over the kernel's `CashValue`: a percent-of-equity default quantity under a cash-per-order or cash-per-contract fee takes its percentage of `strategy.equity` (`PineSizingSnapshot::strategy_equity`, the open entries' cash fees charged) and leaves out the fee its own order pays (`default_sizing_cash`, R5 lane PAR-CASHFEE, §3.10; `tests/test_cash_fee_sizing_tapes.cpp` on the tapes in `tests/fixtures/cash_fee_sizing`) | -| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2513-2528, `PineSizingSnapshot` pine_adapter.hpp:147-159; fill-time exceptions `default_sized` pine_adapter.cpp:6483-6509) | **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:592, 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:4992-4995; 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:12321-12349, `compute_exit_reservation` pine_adapter.cpp:4244-4363) | **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:4296-4615, `reserved_exit_qty` pine_adapter.cpp:8811-9121) | **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:369-376, `source_money_floor_lot` pine_adapter.cpp:378; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4276-4291) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6544), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7750), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12578), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:12954, pine_adapter.cpp:13007) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15296) | 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:2428), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2441, `typed_entry_units` pine_adapter.cpp:2459), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18292) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7013-7363; ledger `close_batch_bar_` pine_adapter.hpp:2011-2022; 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:1074) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | +| SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5832-5836, pine_adapter.cpp:5832) | **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:10989-12193) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:4958, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4935-4969 | 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:2458-2513) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4987-4989 **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: rounded equity `source_money_round` pine_adapter.cpp:2403, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:378 (→ SZ10) | +| SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2412-2421) | **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:2471, `typed_entry_units` pine_adapter.cpp:2489; `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:2450; `percent_commission_live_equity` pine_adapter.cpp:4191-4232) | **no** — only a dead primitive, `reserve_percent_commission` (since deleted), with no caller in `src/` **Closed:** `Sized::reserve_percent_fee` divides the sizing cash by `1 + fee_value / 100` under `NativeFeeKind::Percent` and is an exact no-op for every other fee kind; the dead `reserve_percent_commission` primitive is gone (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K | L3 | F:C3 O:C6 S:S3 | **Disagree:** F lists the reserve under quirk yet offers a knob / O kernel, "partial" / S knob. R5-1: fee-net option is generic. TradingView's cash-commission reserve stays adapter policy over the kernel's `CashValue`: a percent-of-equity default quantity under a cash-per-order or cash-per-contract fee takes its percentage of `strategy.equity` (`PineSizingSnapshot::strategy_equity`, the open entries' cash fees charged) and leaves out the fee its own order pays (`default_sizing_cash`, R5 lane PAR-CASHFEE, §3.10; `tests/test_cash_fee_sizing_tapes.cpp` on the tapes in `tests/fixtures/cash_fee_sizing`) | +| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2347-2362, `PineSizingSnapshot` pine_adapter.hpp:147-159; fill-time exceptions `default_sized` pine_adapter.cpp:6627-6653) | **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:592, 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:5004-5007; 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:12497-12525, `compute_exit_reservation` pine_adapter.cpp:4289-4408) | **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:4341-4660, `reserved_exit_qty` pine_adapter.cpp:8964-9274) | **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:369-376, `source_money_floor_lot` pine_adapter.cpp:378; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4321-4336) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | +| 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:6688), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7894), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12755), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13131, pine_adapter.cpp:13184) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15517) | 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:2458), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2471, `typed_entry_units` pine_adapter.cpp:2489), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18622) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7157-7507; ledger `close_batch_bar_` pine_adapter.hpp:2039-2050; 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:933) | 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:2361) | **yes** — `allowed_open_directions` native_run_spec.hpp:597 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2940-2948 | 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:595 tests the resulting book native_execution_consumer.cpp:2954-2957 | 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:12849-12852) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2966-2969 | 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:6062-6108, cap left unset `max_open_lots` pine_adapter.cpp:2193-2195) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. 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:8779-8785). | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:12903-12968 and leaves the kernel gate unset pine_adapter.cpp:12902-12904) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2984 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:2830 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13552-13553, `margin_pct` pine_adapter.cpp:12966) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:13652-13830, `submit_margin_call_units` pine_adapter.cpp:13791-13837, `schedule_margin_call_path` pine_adapter.cpp:17799-17876) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2936 **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:13629-13665) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; `tests/test_adapter_margin_schedule_differential.cpp` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). | +| MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2195) | **yes** — `allowed_open_directions` native_run_spec.hpp:597 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2956-2964 | 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:595 tests the resulting book native_execution_consumer.cpp:2970-2973 | 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:13026-13029) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2975-2978 | 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:6137-6186, cap left unset `max_open_lots` pine_adapter.cpp:2194-2196) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. 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:8888-8894). | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13079-13145 and leaves the kernel gate unset pine_adapter.cpp:13078-13080) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2993 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:2839 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13729-13730, `margin_pct` pine_adapter.cpp:13143) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:13834-14045, `submit_margin_call_units` pine_adapter.cpp:14006-14052, `schedule_margin_call_path` pine_adapter.cpp:18093-18194) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2952 **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:13760-13796) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; `tests/test_adapter_margin_schedule_differential.cpp` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). | | MG8 | Liquidation price query (`margin_liquidation_price` pine_strategy_host.cpp:114-130) | **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:15900-15916, `apply_fx_opening_margin_slice` pine_adapter.cpp:17877-17905) | **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:16391-16430) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:12938-12941) | **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:13513-13527, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14539-14569) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14582-14816) | **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:13329-13402) | **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:1311 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:13969, 4× shortfall `4.0 * minimum` pine_adapter.cpp:14028, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14774-14814, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14298-14425, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14298) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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, `tests/fixtures/margin_entry_bar/pm2-*`). 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 (`tests/fixtures/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; `tests/fixtures/half_tick_rounding`, PAR-MARGIN-2). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3650-3717, `record_market_review` pine_adapter.cpp:14820-14878) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16121-16137, `apply_fx_opening_margin_slice` pine_adapter.cpp:18195-18223) | **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:16685-16724) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13115-13118) | **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:13690-13704, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14759-14789) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14802-15036) | **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:13506-13579) | **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:13718, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13777, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14523-14563, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14047-14174, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14047) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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, `tests/fixtures/margin_entry_bar/pm2-*`). 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 (`tests/fixtures/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; `tests/fixtures/half_tick_rounding`, PAR-MARGIN-2). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3680-3747, `record_market_review` pine_adapter.cpp:15040-15098) | 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:7036-7069 (call site `on_native_recalculate` native_execution_consumer.cpp:7061) | K | — | F:E1 O:F1 | — | -| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2318-2319) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7540-7544 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15735) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7084-7088; `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:7027 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7078 (the complete bar unless `OpenOnly`) **Closed:** `current_partial_bar()` native_host.hpp:1073 is the lookahead-free bar so far, through the last path point consumed (a fill inside a segment reads it at the segment's origin), and `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks `on_native_bar_open`'s bar to `H = L = C = open` as presentation only — the complete bar is restored before the open match (`tests/test_native_calc_timing.cpp`). | K | L5 | F:E3 | (single-source) The adapter needs the full bar, so the guard is opt-in. | +| CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:7222-7255 (call site `on_native_recalculate` native_execution_consumer.cpp:7247) | K | — | F:E1 O:F1 | — | +| CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2152-2153) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7588-7592 | K | — | F:E2 O:F6 S:C1 | — | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15956) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7096-7100; `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:7039 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7090 (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:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663; `begin_coof_recalc` pine_adapter.cpp:5448, `flush_coof_tail` pine_adapter.cpp:15878, `end_coof_recalc` pine_adapter.cpp:5469) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6933, native_execution_consumer.cpp:6933-6963; commands are legal there `commands_allowed` native_execution_consumer.cpp:1406-1414; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5624-5658, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | -| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7166-7170; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:203-205. | -| 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:7534-7653 **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:2449-2505) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7240-7482; `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. | -| CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:624 | 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:152-238) 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:5595), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1632-1639 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663; `begin_coof_recalc` pine_adapter.cpp:5518, `flush_coof_tail` pine_adapter.cpp:16099, `end_coof_recalc` pine_adapter.cpp:5541) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6945, native_execution_consumer.cpp:6945-6975; commands are legal there `commands_allowed` native_execution_consumer.cpp:1407-1415; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5636-5670, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7178-7182; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:203-205. | +| 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:7582-7701 **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:2281-2337) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7475-7719; `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. | +| CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:613 | 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:152-238) 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:5687), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1638-1645 | n/a | A | — | F:E4 O:F5 S:C2 | R5-5: the adapter keeps its path | ### 1.6 Higher timeframes and auxiliary feeds | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1483-1486, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1391-1547; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1484 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:486), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | -| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI `strategy_set_native_security_feed` c_abi.cpp:839) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1429-1445 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1517; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1447-1452 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:488 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8309 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:46-56). | +| HT1 | HTF series of the same symbol (`request.security`): evaluators registered `scheduler_configure_security_evaluators` pine_strategy_host.cpp:1106-1109, pumped from the scheduler `scheduler_prepare_security_sequence` pine_strategy_host.cpp:1014-1170; state lives in `BacktestEngine` engine.hpp:356-558, engine.hpp:356-402, engine_security.cpp:27 | **no** sanctioned path — `configure_security_evaluators` is an empty virtual engine.hpp:1322 whose only caller is pine_strategy_host.cpp:1107 **Closed:** `NativeRunSpec::subscriptions` (`NativeTimeframeSubscription` native_run_spec.hpp:486), or `declare_timeframe_subscriptions` native_host.hpp:1136 from inside `on_native_run_begin`, with `on_native_timeframe_bar` native_host.hpp:885 and `native_series_bar(index)` native_host.hpp:1119 reading them back; the kernel registers the declared series after that callback returns, so a host's own evaluators survive (`tests/test_native_htf_subscriptions.cpp`, `tests/test_native_htf_subscriptions_stream.cpp`). | K | L6 | F:F1 O:G-1 S:H1 | **Disagree:** S partial (it counts kernel-side storage → HT2); F, S v1 / O deferrable. R5-6: v1. | +| HT2 | Authoritative (exchange) HTF bars substituted into a series (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129, C ABI `strategy_set_native_security_feed` c_abi.cpp:842) | **partial (inert)** — pre-run staging is accepted `refuse_source_mutation` native_execution_consumer.cpp:1438-1454 but never consumed: `prepare_native_security_feeds` is protected engine.hpp:1639, its sole call `prepare_native_security_feeds` pine_strategy_host.cpp:1139; in-run it latches `UnsupportedSource` native_execution_consumer.cpp:1456-1461 **Closed:** a bare host's subscriptions consume authoritative bars: `NativeTimeframeSubscription::authoritative_bars` native_run_spec.hpp:488 is staged into the feed store by the kernel's own `prepare_native_security_feeds` native_execution_consumer.cpp:8355 call at begin, so a declared series is the venue's own bars (`tests/test_native_htf_subscriptions.cpp`). | K | L6 | F:F2 O:G-2 S:H1 | F says "no" on the same facts. "native" in these names means TradingView's native-timeframe bars, not the native host (the feed store's comment `never from the chart's intraday prints` `sha256:3cc5174fa9a60fc0e2ca7870ef1cccd875abaf5d8e11248e633922612c18d9d8` engine_aux_security.cpp:46-56). | | HT3 | Lower-timeframe arrays (`request.security_lower_tf`; `register_security_lower_tf_eval` include/pineforge/source/pine_strategy_host.hpp:630) | **no** — `IntrabarPath::lower_tf` drives matching only **Closed:** `on_native_sub_bar` native_host.hpp:928 delivers every retained `lower_tf` sub-bar after its matching path, so a host that put the lower bars in the path reads them bar by bar (`tests/test_native_calc_timing.cpp`); the series form is the auxiliary feed of HT4 (`tests/test_native_auxiliary_feed.cpp`). | K | L5 (sub-bar hook); series form optional in L6 | F:F4 O:G-3 S:H2 | **Disagree on lane:** F via `on_native_sub_bar` (the host already owns the lower bars it put in the path) / O, S via the series API. v1 = the L5 hook (R5-5), landed; the array form Pine returns stays the adapter's. | | HT4 | Auxiliary finer feed driving `request.security` (`set_aux_security_feed` pine_aux_security.cpp:21-220, `feed_aux_security_for_chart_bar` pine_aux_security.cpp:241-413; the kernel base returns false `set_aux_security_feed` engine_consumer.cpp:158-161) | **no** **Closed:** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed` native_run_spec.hpp:515) plus `NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars, routed by time alone, with `declare_auxiliary_feed` at begin and `append_auxiliary_bars` on a realtime stream (`tests/test_native_auxiliary_feed.cpp`, `tests/test_native_auxiliary_feed_stream.cpp`). | K+A | L6 | F:F3 S:H2 | **Disagree:** F quirk (TV split-feed harness construct) / S generic. R5-1 + R5-6: aux feed registration is generic; chart-slice mapping and deferred first-bucket publication stay A. | | HT5 | Publication mode: lookahead, gaps, session / early-close boundaries (`prepare_historical_security_lookahead_projections` pine_scheduler.cpp:42-132, `calling_bar_complete` pine_aux_security.cpp:255-336) | **no** for a bare host **Closed for a bare host:** `NativeTimeframeSubscription::lookahead` and `::gaps` are the two delivery rules, and the calendar boundaries — session close, early close by instrument class, the lazy seal of a clipped script interval — are the kernel's own (`tests/test_native_htf_subscriptions.cpp`). | K+A | L6 | S:H3 F:F1 O:G-1 | **Disagree:** F "no `lookahead_on`, by construction" / O `lookahead = false` default, available / S `lookahead_on` field, status partial. R5-6: user question (§4 U1); the exact TV projection stays A. | -| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:875; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7906-7923, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7913, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | +| HT6 | Self-aggregation recipe (interim) | **yes** — `TimeframeAggregator` is public and engine-free timeframe.hpp:288-486, fed from `on_native_input` native_host.hpp:875; the Pine scheduler asks the kernel's `script_bucket_completions` native_execution_consumer.cpp:7956-7973, which feeds the same class `TimeframeAggregator` native_execution_consumer.cpp:7963, how its input span buckets (R5 lane B-ADAPTER) | K | L0 (document) | O:G-4 F:F1 | R5-6 | ### 1.7 Fill price, slippage, commission, FX | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:5698-5745, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6281-6289; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6103-6113) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:4958-4976) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:584 applied once native_execution_consumer.cpp:4611-4612, clamped by `protect_limit` native_execution_consumer.cpp:6111 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:12032-12041) is A | +| FP1 | Trigger geometry on the modeled path; fill at the level or at the gap price; market-if-touched | **yes** — `native_matching` native_execution_consumer.cpp:5698-5745, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6293-6301; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6281-6291) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:5103-5121) | K | — | F:G3 O:H1 O:H2 O:H3 | — | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:584 applied once native_execution_consumer.cpp:4620-4621, clamped by `protect_limit` native_execution_consumer.cpp:6289 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11954-11963) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:388-395) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:181-184, `source_bar_fill` pine_adapter.cpp:11805-11814, `directional_tick` pine_adapter.cpp:226-230) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4073; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6363-6364; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:181-184, `source_bar_fill` pine_adapter.cpp:11970-11979, `directional_tick` pine_adapter.cpp:226-230) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4082; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6375-6376; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:257-305) 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:8721). **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:264-312 (used `source_trigger_threshold` pine_adapter.cpp:9106-9111, `exit_limit_trigger` pine_adapter.cpp:9640-9661), `source_bar_fill_tick` pine_adapter.cpp:202-215, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:296-300, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13784-13790 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11757) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | +| FP6 | Account-currency FX | **partial** — scalar `account_fx` + immutable `NativeFxCurve` native_host.hpp:1202 in batch; the stream then refused a non-empty curve (today only tick-driven input is refused under one, `refuse_fx_curve_tick_input` native_execution_consumer.cpp:8785). **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:264-312 (used `source_trigger_threshold` pine_adapter.cpp:9029-9034, `exit_limit_trigger` pine_adapter.cpp:9563-9584), `source_bar_fill_tick` pine_adapter.cpp:202-215, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:221-225, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13808-13814 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11679) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| -| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:2613-2626, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1021-1025 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). **Lowered by R5 lane H-THIN:** the Pine host keeps the kernel's `owns_lot_excursions()` default, so the sampler is the Pine excursion; the host model, its entry-bar masks and its caches are deleted, and the source layer's fold no longer carries them. **Measured against TradingView first (R5 lane H-MEASURE, `tests/test_e19_excursion_tape.cpp`):** the host-owned model was `ab9714be`'s, not TradingView's — where it and the kernel sampler differ the kernel's number is TradingView's (entry-bar over-mask, exit-bar over-fold after an at-open exit, a `calc_on_order_fills` scratch TradingView reports as 0 / 0); both miss a bar's open print ahead of a same-bar stop fill. On the corpus the two models differ on 341 of 431 390 trades, and of the 326 of those a TradingView export identifies, the kernel's number is TradingView's on 315 and the host's on none. The lowering moved the cells of 335 trades in 19 corpus probes: 324 now equal TradingView's, 11 moved closer, none farther; the all-in residual's own entry-bar call is tape `tests/fixtures/e19_allin_trim`. | +| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:2622-2635, closing-row fold `max_runup` engine_orders.cpp:157-171; `owns_lot_excursions` native_host.hpp:1021-1025 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). **Lowered by R5 lane H-THIN:** the Pine host keeps the kernel's `owns_lot_excursions()` default, so the sampler is the Pine excursion; the host model, its entry-bar masks and its caches are deleted, and the source layer's fold no longer carries them. **Measured against TradingView first (R5 lane H-MEASURE, `tests/test_e19_excursion_tape.cpp`):** the host-owned model was `ab9714be`'s, not TradingView's — where it and the kernel sampler differ the kernel's number is TradingView's (entry-bar over-mask, exit-bar over-fold after an at-open exit, a `calc_on_order_fills` scratch TradingView reports as 0 / 0); both miss a bar's open print ahead of a same-bar stop fill. On the corpus the two models differ on 341 of 431 390 trades, and of the 326 of those a TradingView export identifies, the kernel's number is TradingView's on 315 and the host's on none. The lowering moved the cells of 335 trades in 19 corpus probes: 324 now equal TradingView's, 11 moved closer, none farther; the all-in residual's own entry-bar call is tape `tests/fixtures/e19_allin_trim`. | | RP2 | Closed-trade list, trade statistics, report struct | **yes** — `fill_report` engine_report.cpp:39-66, `get_trade` engine.hpp:1791-1828, `compute_trade_stats` engine_metrics.cpp:122 | K | — | F:H2 O:I3 S:X2 | — | | RP3 | Equity curve | **no** — `update_equity_extremes` engine.hpp:1362 / `record_equity_point` engine.hpp:1380 are protected and were called only by the adapter **Closed:** `NativeReportPolicy::KernelRecorded` native_run_spec.hpp:63 has the consumer fold the extremes and append one point per script calculation, labelled with the script interval's open; `KernelRecordedAtHostMarks` records the same series at the host's own marks (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I4 S:X2 | S's "partial" folds RP2 in | | RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve engine_metrics.cpp:203; `fill_metrics_section` engine_report.cpp:119-139 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) | -| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1306) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1408) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | +| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:1306) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1815 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1031) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. | | RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1391 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1833 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) | -| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9286, C ABI `strategy_stream_order_actions_len` c_abi.cpp:663-680 | K | — | F:H5 | (single-source) | -| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1358, native_execution_consumer.cpp:10682; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | -| RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:538; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1931 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | +| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1293, enabled `stream_observe_actions_` native_execution_consumer.cpp:9359, C ABI `strategy_stream_order_actions_len` c_abi.cpp:652-669 | K | — | F:H5 | (single-source) | +| RP8 | Continuation hash | **yes** — `native_continuation_hash` native_host.hpp:1376, native_execution_consumer.cpp:10765; the spec is folded by `hash_spec` native_execution_consumer.cpp:238-260 | K | — | F:I1 O:J2 S:X3 | — | +| RP9 | Broker-state hash: scalar + per-bar recording | **partial** — scalar `broker_state_hash` engine_state_hash.cpp:22-118, C ABI `strategy_broker_state_hash` c_abi.cpp:527; per-bar rows are appended only by the source scheduler `scheduler_record_broker_hash` pine_strategy_host.cpp:1552 **Closed:** under `KernelRecorded` with `set_broker_state_hash_recording(true)` the consumer appends one `broker_state_hash()` row after each report point, so `broker_state_hash_len == equity_curve_len == script_bars_processed` on a completed run in batch and across a stream's warmup and realtime legs. A Failed or Aborted run can expose a shorter prefix. A row is the run's continuation identity at that bar, per driving mode — the broker half alone is mode-invariant (`tests/test_native_report_truth.cpp`). | K | L2 | F:I2 O:J1 S:X3 | O's "yes" covers the scalar only. §3.9 records the old quadratic adapter fold and closed-row kernel walk as history; V19-E and V19-A replaced them with running digests. | | RP10 | Fold the host's own durable state into the hash | **partial** — the protected virtual `hash_source_extension` engine.hpp:411 is overridable but source-named; the kernel default folds `"source:none"` engine_state_hash.cpp:19-21 **Closed:** `hash_host_extension(BrokerStateHashSink&)` engine.hpp:405 is the generic seam — protected virtual on `BacktestEngine`, called once per hash, last — and `hash_source_extension` engine.hpp:411 is its deprecated spelling, which the generic default forwards to (`tests/test_native_host_hash_extension.cpp`). | K | L2 (the virtual rides the shared bump) | F:I3 O:J1 S:X3 | — | ### 1.9 Other runtime surface @@ -171,12 +171,12 @@ 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:1856, refused in-run by `guard_native_mutation` engine.hpp:1860) | **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:2254-2255, `chart_timezone` pine_adapter.cpp:2295-2297) | **yes** — spec fields `ticker` native_run_spec.hpp:568-578; `parse_session` native_calendar.hpp:180-462; 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:2088-2089, `chart_timezone` pine_adapter.cpp:2129-2131) | **yes** — spec fields `ticker` native_run_spec.hpp:568-578; `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:2158, `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:896, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:940, probe `strategy_execution_contract` c_abi.cpp:873, stream `strategy_stream_begin` c_abi.cpp:624 **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:548 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) | +| OT5 | C-level run configuration, stream, FX curve, contract probe | **yes** — `strategy_configure_native_v1` c_abi.cpp:885, FX curve `strategy_configure_native_fx_curve_v1` c_abi.cpp:956, probe `strategy_execution_contract` c_abi.cpp:876, stream `strategy_stream_begin` c_abi.cpp:613 **Extended:** `pf_native_run_spec_ext_v1`'s policy tail now carries `intrabar`, `path_order`, `abort_reporting` and the slot-label / feed-tolerance policies (`PF_NATIVE_SPEC_EXT_INTRABAR`, `PF_NATIVE_SPEC_EXT_FEED_POLICY`), so nothing of `NativeRunSpec` but `identity` is unreachable from C (`tests/test_native_c_api.c`). | K | — | O:J4 | The v1 C spec `pf_native_run_spec_v1` pineforge.h:510 omitted `intrabar`, `path_order` and `abort_reporting`; they now travel in the extension's policy tail, `pf_native_abort_reporting_e` native_c_api.h:911 among them (F §2.iii) | | OT6 | C-level order submission + strategy callbacks | **no** — none of the original runtime `PF_API` symbols; then listed as a refusal on native-engine.md **Closed:** `strategy_native_host_create_v1` takes a C callback table and `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` / `_execute_current_v1` command the same kernel under the same legality rule; the 41 `strategy_native_*` declarations plus two `strategy_configure_native_ext_*` declarations are a second, disjoint inventory beside the 58 runtime exports (and eight generated per-strategy exports) (`tests/test_native_c_api.c`, `tests/test_native_c_api_frozen_header.cpp`). | K | L13 | F:J6 O:J5 | R5-7: post-v1. S covers it in its §2.3 without a row. | -| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1454 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7286), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1385 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:849; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | +| OT7 | *(quirk)* Pine language state: series, barstate / session flags, position-view freezing (`PineLanguageState` pine_language_state.hpp:12-68; `publish_series` pine_scheduler_native.cpp:207-242, `chart_index` pine_scheduler_native.cpp:509-621) | n/a | A | — | S:R1 | (single-source) **Closed (E25, lane E20's finding 2; E26, E25's findings 1 and 3):** `session.islastbar` reads one script bar ahead on every driving path: the chart timeframe's next retained bar, an aggregated chart's next bucket (a scheduler lookahead, deleted when the rule moved into the kernel), the calendar for the live tail's bar and for every bar of a stream, warmup included — a stream continues past its replay, so only a batch run's final bar reads "last" from having no successor. Since R4 slice C an aggregated or realtime bar read every in-session bar as last. **What TradingView does:** it ends a session at the session DAY — the flag belongs to the last chart bar whose successor belongs to another session day, in session or not, and `session.isfirstbar` is its dual, the bar after a session's last one. The ordinal is `internal::session_trading_day_index` timeframe.hpp:145, the exchange-timezone day rolling at the symbol's day stamp (09:30 ET on NYSE RTH, midnight on 24x7, 17:00 ET on a 1700-1700 forex session), not local midnight; the unmerged form, since a fused holiday daily bar still holds two intraday sessions. The three `lab tv` tapes (`tests/fixtures/session_islastbar`) give that rule bar for bar with no disagreement, and the engine now holds 255/255 NYSE:F last bars, 255/255 first bars and 370/370 ETH last bars on the chart-timeframe AND the aggregated path (it held 1, 0 and 1 before). Early closes come with it: the three NYSE:F half days flag 12:45 ET without a holiday calendar, because the next bar is the next session day's. **Generic since R5 lane F5:** the kernel computes that reading for every host — `NativeDecisionContext::in_session` market_driver.hpp:161 and the facts after it (`opens_session_day`, `closes_session_day`, `closes_session_day_open_ended`), in C the session bytes of `pf_native_decision_v1` native_c_api.h:1465 — from the run's own calendar and input (`present_session_day` native_execution_consumer.cpp:7298), so a bare host reproduces the tapes flag for flag (`tests/test_native_session_day_facts.cpp`, kernel-only). The adapter computes no rule of its own: `scheduler_update_session_state` pine_strategy_host.cpp:1385 selects the kernel's facts into the three flags, which are `PineStrategyHost` members now (`session_ismarket_` pine_strategy_host.hpp:849; `BacktestEngine` holds no Pine session state), and only its live-probe tail reads the open-ended close at its batch's final bar (`tests/test_session_day_facts_adapter.cpp`). Selecting the kernel also ended a calc_on_order_fills defect of the adapter's lookahead, which read two bars ahead on a bar a fill recalculation had already published (a fill on a day's second-to-last bar flagged that bar last and the day's real last bar first). **Ruled, not open:** a bar with nothing held after it — a stream's bar, a live-tail bar — steps the calendar one width on, so it cannot see an early close the session string does not declare (ADR-0001, "Session-day facts at a bar with nothing held after it"). **Closed (F1):** a `calc_on_order_fills` recalculation reads the flags of the bar it runs on, not the previous bar's (the kernel presents the current bar's facts on that recalculation), and the close callback after it no longer looks one retained bar too far ahead (§3.8, AG3). | | OT8 | *(quirk)* Live-tail / probe-suppress harness flags (`realtime_tail_` pine_strategy_host.cpp:206-208) | n/a — **contained since N14** (the flags were `BacktestEngine` members until then; now `source::PineStrategyHost` state behind two kernel virtual seams, §2.ii q) | A | N14 | F:J7 S:C5 | parity-campaign tooling | --- @@ -185,7 +185,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa ### 2.i Kernel-only build target (L1) -- **Before L1:** one static library always compiled the source layer and installed every header, `include/pineforge/source/**` and `include/pineforge/compat/pine/**` included, and `src/compat/pine/market_admission.cpp` sat in the *main* list for historical reasons (added by `3b13183`, before the source-layer list existed — F). **Since L1:** the source layer is its own list (`PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91, `market_admission.cpp` at :94), appended to `pineforge` only when `PINEFORGE_BUILD_SOURCE_LAYER` is ON (`add_library(pineforge STATIC` CMakeLists.txt:164-167), beside the kernel-only archive (`pineforge_kernel` CMakeLists.txt:153), whose header install excludes `source` and `compat` (`PATTERN` CMakeLists.txt:416-421). +- **Before L1:** one static library always compiled the source layer and installed every header, `include/pineforge/source/**` and `include/pineforge/compat/pine/**` included, and `src/compat/pine/market_admission.cpp` sat in the *main* list for historical reasons (added by `3b13183`, before the source-layer list existed — F). **Since L1:** the source layer is its own list (`PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91, `market_admission.cpp` at :94), appended to `pineforge` only when `PINEFORGE_BUILD_SOURCE_LAYER` is ON (`add_library(pineforge STATIC` CMakeLists.txt:164-167), beside the kernel-only archive (`pineforge_kernel` CMakeLists.txt:153), whose header install excludes `source` and `compat` (`PATTERN` CMakeLists.txt:426-431). - **Link feasibility:** no kernel TU and no public kernel header includes `source/` or `compat/pine/` (F, O). The only code coupling is the forward-declared opaque `source::StrategyOverrides*` (§2.ii a), which needs no definition. - **Risk:** the archive member order of `libpineforge.a` changes. Harmless for static linking, but check `PINEFORGE_REQUIRE_ABI_RECEIPTS` (CMakeLists.txt:65); what the receipts pin is unverified (F). @@ -204,16 +204,16 @@ 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:82-91) and of the rich `BacktestEngine::run` overload (engine.hpp:1780-1785, 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:210-213 | 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) | F O S | | b | `pine_float_compare.hpp` | 1e-10 absolute equality band, used by kernel TA only: `float_band_le` ta_misc.cpp:29 (percentrank), `float_band_gt` ta_oscillators.cpp:462-463 (MFI), `plus_dm` ta_volatility_trend.cpp:356-359 (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:906-908 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1842-1845; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1875, `:8782`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2308-2309. 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:260-261. **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:906-908 (outside a run it answers the spec's `spec_tolerant_slot_labels` native_execution_consumer.cpp:1845-1848; the reads are `legacy_tolerant_slot_labels` native_execution_consumer.cpp:1878, `:8846`); set only by the adapter `slot_label_policy` pine_adapter.cpp:2142-2143. 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:260-261. **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:370-376) | 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:6434 | NEUTRAL | Reuse as the reference arithmetic of the price-grid and sizing lanes, delete the rest | O (single-source) | +| e | Dead helpers in the public kernel header | **Done.** All four are gone from `engine.hpp`: `round_to_mintick_directional` and `apply_slippage` were the pre-R5 spelling of the directional snap and the slippage step and the kernel now fills on `native_matching::grid_round_directional` / `native_matching::apply_slippage` src/native_matching.hpp:259 (the engine.hpp comment that named them went with R5 lane E10's residue sweep); `apply_limit_fill` and `reserve_percent_commission` are deleted outright, and only the adapter still spells the first two names, in its own comments. `round_to_mintick` engine.hpp:753 and `bar_fill_price` engine.hpp:771 stay, live through the call `bar_fill_price` native_execution_consumer.cpp:6445 | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:18697-18726 | 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:522, 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:66) 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:308-585) 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 | +| 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:19030-19059 | 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:528, 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:66) 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:308-585) 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:604. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:417; 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:1274 | 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) | +| 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:1239 | 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) | | m | `request.security` machinery | `register_security_eval` engine_security.cpp:27, `set_native_security_feed` engine_aux_security.cpp:76-127, `SecurityEvalState` engine.hpp:1156-1360 carry Pine semantics (lookahead, gaps, Heikin-Ashi, KI-55 range start) and are driven only by the source layer | HASH-VISIBLE / measured path | F planned to move both TUs and the state into the source host after native HTF. **R5-6 narrows this:** L6 reuses the feed store and routing, so those stay kernel; only the Pine-only evaluator semantics remain candidates. Do last. **Status (lane L12b): done** — the evaluator semantics are `source::PineSecurityEvalState` + `src/source/pine_security_eval.cpp`; the kernel keeps the registry, aggregator, feed store and one generic step | F S | | n | S-only proposals, not adopted | A generic `FillModel` interface with a TV model installed by the adapter (for the TradingView money / tick / path comment blocks in `engine.hpp` and the dead helpers of item e); "emit generic FIFO facts and let an adapter policy produce the KI-62 scratch" | — | Superseded by R5-3 (price-grid lane) and R5-1 (TV fill specifics stay behind `resolve_execution_terms`); the dead-helper part is item e; there is no scratch code left to re-lower (item d) | S | | o | `pineforge::admission` (market-admission journal) — **N14 addition** | was `include/pineforge/market_admission.hpp` (275 lines, installed by a kernel-only build) + `src/market_admission.cpp` (kernel TU) until N14 moved both under `source/` (`include/pineforge/source/market_admission.hpp`, `src/source/market_admission.cpp`): the observation journal of TradingView admission reviews; every `Configuration` field is a `strategy()` declaration parameter. No kernel TU consumed it (engine.hpp included it and used nothing; the hash helper in `src/broker_state_hash_internal.hpp` had one caller, `src/source/pine_state_hash.cpp`) | NEUTRAL (pure move; the fold is unchanged, so no hash domain moves) | **Status (N14): done** — moved whole to `include/pineforge/source/market_admission.hpp` + `src/source/market_admission.cpp` (`PINEFORGE_SOURCE_LAYER_SOURCES`); `hash_admission_field` moved verbatim into the source hash TU; `check_market_admission_schema.py` re-pointed; `MarketAdmissionDraft` / `MarketAdmissionJournal` spellings and the `market_admission_v2` inline namespace unchanged. ADR-0001 no longer calls it a generic stem | audit (Opus N14) | @@ -230,7 +230,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa 1.0 C surface still does not expose is the 1.0 C boundary table of `docs/pages/native-engine.md`. "Today" below is the tree L13 read.* -Today a C host can configure (`strategy_configure_native_v1` c_abi.cpp:861-926), stream (`strategy_stream_*` c_abi.cpp:589-698) and read results, but strategy logic is C++-only and the batch entry points (`strategy_create`, `run_backtest*`) are per-strategy symbols a native author hand-writes. Design retained from F (most complete), with O's event polling and S's hardening rules: +Today a C host can configure (`strategy_configure_native_v1` c_abi.cpp:850-915), stream (`strategy_stream_*` c_abi.cpp:578-687) and read results, but strategy logic is C++-only and the batch entry points (`strategy_create`, `run_backtest*`) are per-strategy symbols a native author hand-writes. Design retained from F (most complete), with O's event polling and S's hardening rules: ```c typedef struct pf_native_decision_v1 { uint64_t ordinal; int32_t interval_index; /* SCRIPT-bar index */ int64_t effective_time_ms, @@ -267,12 +267,12 @@ PF_API int pf_native_events_v1(pf_strategy_t, uint64_t after_ordinal, pf_native_ | Point | Decision | Src | |---|---|---| | Implementation | One new kernel TU (F `src/native_c_host.cpp`, O `c_native_host.cpp`, S `native_c_api.cpp`): `class CCallbackHost final : public NativeStrategyHost` forwarding each virtual to the table. Streaming reuses the existing `strategy_stream_*` symbols unchanged (they take any `BacktestEngine*`) | F O S | -| Errors | A non-zero callback return maps to `NativeFailureCode::CallbackException`; C callbacks must not unwind. The C++ path already latches exceptions as `CallbackException` native_execution_consumer.cpp:7068 | F O | -| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1406-1413): inside a callback, or between realtime inputs | F | +| Errors | A non-zero callback return maps to `NativeFailureCode::CallbackException`; C callbacks must not unwind. The C++ path already latches exceptions as `CallbackException` native_execution_consumer.cpp:7254 | F O | +| Legality | The existing rule (`commands_allowed` native_execution_consumer.cpp:1407-1414): inside a callback, or between realtime inputs | F | | Hardening | Tagged, size-prefixed PODs; `struct_size` + version; unknown-tag refusal; never cast the C request to a C++ variant; a C fixture compiles against the frozen previous header. S puts the API in a separate header `include/pineforge/native_c_api.h` | S | | SOP | Every symbol goes into `src/c_abi.cpp`, `pineforge.h`, `EXPECTED_RUNTIME` + both counts (check_c_abi_runtime.py:19-79), the README table and the Python harnesses, or the `source-guard-c-abi` stage fails, in preflight and every `ci_verify` profile (`CLAUDE.md`) | F O S | | Not in v1 of the C API | `resolve_execution_terms` / `validate_execution_precommit` are the adapter's seam; a C host that needs sizing uses `Sized` (F). O offers an optional `resolve_terms` callback: deferred. *History: R5 lane F4 routed both (`on_execution_terms`, `on_precommit`); the terms hook's units answer outside a host-sized close is still C++-only (the 1.0 C boundary table)* | F O | -| Open | Naming: F and O use `pf_native_*`; S uses `strategy_native_*`, which matches the existing runtime symbols. The v1 C spec `pf_native_run_spec_v1` pineforge.h:548 omitted `intrabar`, `path_order` and `abort_reporting`; the extension's policy tail carries all three now (`pf_native_abort_reporting_e` native_c_api.h:911) | F S | +| Open | Naming: F and O use `pf_native_*`; S uses `strategy_native_*`, which matches the existing runtime symbols. The v1 C spec `pf_native_run_spec_v1` pineforge.h:510 omitted `intrabar`, `path_order` and `abort_reporting`; the extension's policy tail carries all three now (`pf_native_abort_reporting_e` native_c_api.h:911) | F S | Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the epoch batch. @@ -281,8 +281,8 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the | | F (its §2.iv A) | O (its G10) | S (its P7) | Chosen | |---|---|---|---|---| | API shape | spec `subscriptions` + push callback `on_native_timeframe_bar` | `register_native_series` + pull `native_series_bar(id)` / `native_series_slot_is_new(id)` | spec `security_requests` + `configure_native_security_feed` + `native_security(id)` view | F's subscription in the spec + push callback, plus O's pull accessor | -| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:661-676, `contribute_input` native_execution_consumer.cpp:8030, `seal_script` native_execution_consumer.cpp:8007) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | -| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7116), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | +| Engine reuse | none: generalize the consumer's one script bucket (`ScriptBucket` native_execution_consumer.hpp:661-676, `contribute_input` native_execution_consumer.cpp:8092, `seal_script` native_execution_consumer.cpp:8057) to N; rejects exposing the `request.security` path as Pine-shaped end to end | promote the existing evaluator + feed machinery; edits `engine_security.cpp`, `engine_aux_security.cpp`, `engine_lower_tf.cpp` | same TUs + `native_calendar.cpp` | The existing feed machinery, **called, not edited**; `engine_security.cpp` untouched | +| Pump | consumer seal logic | accepted-input path (`invoke_input_callback` native_execution_consumer.cpp:7128), only for natively registered series | copy at Ready, evaluators created before the first input | O's registration-gated pump + S's copy-at-Ready | | Authoritative bars | `authoritative_bars` on the subscription | `Source::ExchangeFeed` through `set_native_security_feed` | `configure_native_security_feed(id, bars, tf)` | F's field, installed through the existing store | | Lookahead | none, by construction | `lookahead = false` default, available | `lookahead_on = false`, `gaps_on = true` | open → §4 U1 | @@ -290,15 +290,15 @@ Depends on L7 (working view), L3 (so `Sized` is in v1 of the C request) and the 2. **Wiring**, all inside the native consumer at Ready → begin and only when `subscriptions` is non-empty (zero work for the adapter): register one evaluator state per subscription through the existing `register_security_eval` (engine.hpp:1295-1297, *fresh*); install `authoritative_bars` into the existing store (`set_native_security_feed` engine.hpp:1730, engine_aux_security.cpp:78-129); call `prepare_native_security_feeds` (engine.hpp:1639, defined `BacktestEngine::prepare_native_security_feeds` engine_aux_security.cpp:128, *fresh*) — the "made native-callable" step: reachable from the native consumer, still refused from host code in-run; pump `feed_security_eval_state` (engine.hpp:1312-1314, *fresh*) from the accepted-input path; deliver each completed bucket through `on_native_timeframe_bar`. 3. **Untouched:** `engine_security.cpp`, the Pine scheduler sequence pine_strategy_host.cpp:1305-1389 and its call order. The per-run evaluator init is today a *source-layer* member (`PineStrategyHost::init_security_eval_states_for_run` pine_scheduler.cpp:9-39, declared `init_security_eval_states_for_run` pine_strategy_host.hpp:687, *fresh*), so L6 adds a kernel-side twin instead of moving it. 4. **Delivery rule (F):** a subscription bar is delivered on the input bar that completes it — its last contributing input bar, or, for a `LazyComplete` bucket, the next period's first input bar — immediately before that input's script calculation, never earlier — and never before an earlier script interval's lazily sealed calculation has run: the pump is ordered against the script interval, `seal(k) → deliver(i+1) → calc(i+1)` (L6d). -5. **Interim, documented in L0:** `on_native_input` + `TimeframeAggregator` (HT6). Doc notes (O): `set_native_security_feed` is public but inert without evaluators and throws in-run (`guard_native_mutation` engine_aux_security.cpp:78, which latches `UnsupportedSource` native_execution_consumer.cpp:1455); `set_aux_security_feed` returns false in the kernel engine_consumer.cpp:158-160. +5. **Interim, documented in L0:** `on_native_input` + `TimeframeAggregator` (HT6). Doc notes (O): `set_native_security_feed` is public but inert without evaluators and throws in-run (`guard_native_mutation` engine_aux_security.cpp:78, which latches `UnsupportedSource` native_execution_consumer.cpp:1456); `set_aux_security_feed` returns false in the kernel engine_consumer.cpp:158-160. 6. **Entanglement to put in the lane brief** (*fresh* read of `set_native_security_feed` `sha256:eca3dbea0baa461982ba89f13d0a5c43277854069fb6eb2dd2f5c5471313204d` engine_aux_security.cpp:46-78): the feed machinery is TV-calibrated. "W" / "M" buckets are built from the installed native daily bars, and the native stamps define the period partition (a holiday session folds into the next trade date's bar). A bare host that supplies `authoritative_bars` inherits those rules. Document them as the contract or gate them behind a partition policy — decide in the lane, not silently (§4 E8). 7. **HT3 / HT4:** lower-TF arrays come from the L5 sub-bar hook in v1. An auxiliary feed is a subscription whose bars come from the host; the Pine chart-slice mapping is not exposed. Whether v1 accepts a feed *finer* than the input is left to the lane brief (S wants it, F calls it a TV harness construct). **Status (audit lane N7): HT4 done for a bare host; the adapter's auxiliary path retained, measured.** `NativeRunSpec::auxiliary_feed` (`NativeAuxiliaryFeed{tf, bars}`, strictly finer than `input_tf`, opt-in and hashed only when present) plus `NativeTimeframeSubscription::source = NativeSeriesSource::AuxiliaryFeed` build a series from the host's finer bars through the existing evaluator step (`register_security_eval` against the feed's timeframe, `feed_security_eval_state` per feed bar — called, not edited); routing is by time alone (a feed bar rides on the first accepted input whose period it opened before), with `declare_auxiliary_feed` at begin, `append_auxiliary_bars` on a realtime stream and a C spelling (`PF_NATIVE_SPEC_EXT_AUXILIARY_FEED` tail, `strategy_native_append_auxiliary_bars_v1`). No epoch moved: `native_run_spec_v3` and `engine_script_run_v18` were open then (no release tag contained them, no frozen provider fixture of either existed), exactly as L6c's `gaps` and begin-time hook were added. `BacktestEngine::set_aux_security_feed` stays the source host's door and still answers a silent false on a bare host (the pre-begin setter pins, `tests/test_native_example_batch.cpp` among them, hold it to that). **Retained, why:** the adapter does not re-lower its auxiliary sites, on three measurements — 0 of 312 corpus probes install an auxiliary feed, so corpus byte-identity cannot witness the move; TradingView's chart slice leaves pre-range feed coverage inert where the kernel folds it by time (two hourly buckets against three over one feed, `tests/test_native_auxiliary_feed_twin.cpp` row B); and the adapter evaluates a chart bar's slice at its calculation, after that bar's matching pass, where the kernel delivers before it (position 1 against 0 at the same bucket, row C), which no predicate over an opaque generated `evaluate_security` can prove neutral. The congruent shape is the same series on both (row A) and can move once a population exercises it. Full contract: `docs/pages/native-engine.md`, "The auxiliary finer feed". ### 2.v Promote the examples (L10) -- Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:454-455, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). -- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:2969-3037) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). +- Move `runner/examples/native_market_strategy.cpp` and `native_selected_strategy.cpp` to a top-level `examples/native/`, built by `PINEFORGE_BUILD_EXAMPLES` (today "none yet" and guarding nothing CMakeLists.txt:37; the examples build only under `PINEFORGE_BUILD_LIVE_RUNNER` CMakeLists.txt:464-465, with `native_market_example` runner/CMakeLists.txt:45). Link the kernel target (S), or `PineForge::pineforge` during migration (O) — no SQLite / curl / OpenSSL (runner/CMakeLists.txt:1-4). +- Build (1) standalone executables with a `main()` that runs batch + stream on embedded bars (the guide already contains that `main` native-engine.md:3051-3119) and (2) the same MODULE targets the runner tests load; keep the C-ABI shims so the live runner can still `dlopen` them (F, O, S). - Update the three places that pin the paths: `native_market_example` runner/CMakeLists.txt:45, `NATIVE_EXAMPLES` check_native_include_independence.py:49, the tests at `test_native_example_batch` runner/CMakeLists.txt:128. - Add a minimal "hello, kernel" host (~60 lines, no C ABI — O) and one example per v1 feature lane as it lands; each doubles as that lane's twin host (F). - Add `include/pineforge/native_module.hpp` with `PINEFORGE_EXPORT_NATIVE_STRATEGY(Class)` to replace the ~70 hand-written `extern "C"` lines per example (F, single-source). @@ -323,9 +323,9 @@ Union of F §2.vi (9), O §2.vi (5 + 1) and S §2.6 (7), deduplicated. S had no | 3 | "Read results via `metrics.hpp`" (ADR L76-80) | Trade stats yes. Equity curve and equity metrics are empty for a bare host, silently, and an open final position is missing from the report | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | F O | | 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:1730, `guard_native_mutation` engine_aux_security.cpp:78 | 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). No kernel TU includes a source header | `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13 | 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:739 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:370-376 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2013-2024; `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) | 58 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 58 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:13933f70987f6bfe37d3015ae065a8d5ee5937c37431d48e2ecd1f17a4c29958` c_abi.cpp:289-929, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:19-79, c_abi.cpp:862, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:590-709, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:515-567, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:494-511, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:802-833, c_abi.cpp:929 | F O S | +| 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:370-376 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2024-2035; `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) | 58 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 58 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:13933f70987f6bfe37d3015ae065a8d5ee5937c37431d48e2ecd1f17a4c29958` c_abi.cpp:278-918, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:20-80, c_abi.cpp:851, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:579-698, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:504-556, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:483-500, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:791-822, c_abi.cpp:918 | F O S | | 9 | "No examples. `PINEFORGE_BUILD_EXAMPLES` is 'none yet'" (ADR L139) | The option is inert, but two Pine-free `NativeStrategyHost` examples exist, are compiled by the independence checker and tested through the live runner, only under `PINEFORGE_BUILD_LIVE_RUNNER`. Wording: "no top-level examples target" | at `73817c1` (overtaken; ADR-0001, "Current state vs. target") | F O S | | 10 | Follow-ups (1)-(4) (ADR L168-170) | Add native reporting, sizing, margin, HTF — and, per R5, calc-timing, price grid and order ergonomics. Without them "usable without Pine" is a build-system claim, not a feature claim | §3.5 | F | | 11 | Build layout | One `pineforge` library always appends the source list; a kernel-only target does not exist yet; `compat/pine/market_admission.cpp` sits in the main list, a boundary bug. **Overtaken:** `PINEFORGE_KERNEL_SOURCES` CMakeLists.txt:113 and `add_library` CMakeLists.txt:153 build `PineForge::kernel`, and the compat unit is in `PINEFORGE_SOURCE_LAYER_SOURCES` CMakeLists.txt:91 | `add_library` CMakeLists.txt:153 | S (F, O agree in §2.i) | @@ -341,7 +341,7 @@ the campaign's own anchor audit found that the first thing to rot: | the request-epoch sentence | `native_order_v4` | `native_order_v7` native_order.hpp:25 | | the two host-epoch sentences | host epoch v16 | `engine_script_run_v19` native_host.hpp:20 | | "What still requires Pine compatibility to build" | `BacktestEngine` still holds `CapAttachment` / `OrderPriority` / `IntradayCap` | `engine.hpp` has zero references to them; the cap type is `CapAttachment` intraday_cap.hpp:18 and the constructor is `PineStrategyHost`'s pine_strategy_host.hpp:244 | -| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9156-9196 shows no such refusal (still unpinned by a test; the page says so) | +| the rich-`run` paragraph | the rich `run` overload fails a native host | `run_rich` native_execution_consumer.cpp:9229-9269 shows no such refusal (still unpinned by a test; the page says so) | | added | — | the self-aggregation recipe beside the subscriptions it is an alternative to; the open-bar lookahead warning with `current_partial_bar` native_host.hpp:1073; and `set_native_security_feed` documented as the pre-run door for a series' authoritative bars, refused in-run | --- @@ -350,7 +350,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:2205-2338) 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:1654-1658). 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 Cloud Run campaign sweep on the final tree (fixed population, 4190 probes, zero tolerance on the hard band — native-refactor-progress.md:41-47; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:139-140). 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:2080-2213) 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:1665-1669). 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 Cloud Run campaign sweep on the final tree (fixed population, 4190 probes, zero tolerance on the hard band — native-refactor-progress.md:41-47; a fresh exact-HEAD `PASS` verdict is required, `sha256:59adeba8aabaefa740633d52e9216b49065b28b397b87f22c57ad5e19f2948c2` AGENTS.md:141-142). 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 drive the adapter and 14 the native host (O), 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. @@ -408,11 +408,11 @@ 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:2357, 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:2191, 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:5629-5633) and `drain_applied_notifications` (native_execution_consumer.cpp:6290-6303) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | -| L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:1304-1388 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | +| L5 | `NativeCalculationTrigger`, `on_native_recalculate(…, reason, cause)`, `on_native_sub_bar`, `current_partial_bar()`, `NativeOpenBarView` → A.4 | the adapter keeps `BarClose` + its own cascade; `born_on_remaining_path` (native_execution_consumer.cpp:5641-5645) and `drain_applied_notifications` (native_execution_consumer.cpp:6302-6315) are frozen; accessors are inert | COOF probe re-expressed natively: same fills where the TV waypoint rule is inactive (F); a host that flips on its own fill produces the adapter's cascade in shape (O); callback reason, cursor, newborn eligibility and event ordinals compared (S) | +| L6 | `NativeTimeframeSubscription`, `on_native_timeframe_bar`, `native_series_bar` → A.5, §2.iv | `subscriptions` empty for the adapter; registration-gated pump; `engine_security.cpp` and pine_strategy_host.cpp:927-1011 untouched; corpus diff before / after (O: highest parity risk of any lane) | HTF-filter probe on a 24x7 symbol: exact (F); `"D"` over a 15m feed equals a `TimeframeAggregator` baseline bar for bar (O); installed-feed and no-feed cases (S) | | L7 | `native_working_requests()`, `cancel_all()`, `cancel_where(label)`, `TriggerAnchor`, `TrailTicks`, zero offset, `ReplaceOptions`; toolkit `submit_bracket`, `OrderBook` → A.6 | new kinds unused by the adapter; anchor defaults to `Absolute`; the adapter's `tick * 0.5` sentinel keeps working | bracket + trailing probe through the builder: exact where TR5 / FP7 are inactive (on-grid levels) (F); the zero-offset trail test asserts the run no longer fails (O); parent-limit + stop sibling, parent rejection, replacement, parent-cycle revival (S) | | L8 | `NativePriceGrid { None, QuantizeFills, QuantizeFillsAndTriggers }`, `NativeGridRounding` → A.7 | `None` for the adapter, permanently — **native-only by ruling, audit lane P6: §3.6.2** | a host with `QuantizeFills` books on-grid prices; a second test pins that `QuantizeFillsAndTriggers` still differs from TV's half-tick threshold (FP5) (O) | | L9 | `NativeRiskLimits`, `NativeRiskEvent`, `MatchRejectReason::RiskLimit` → A.8 | `risk` unset for the adapter; its ledger (`SourceDayLedger` pine_adapter.hpp:1200) untouched — **retained after measurement, audit lane N12; native-only by ruling, audit lane P6: §3.6.1** | risk probes: same halt bar (F); each limit + the day-boundary basis (O); breach, forced close, cancellation, next-day reset (S) | @@ -444,7 +444,7 @@ Flagged: | Tier | Lanes | Why | |---|---|---| -| Day-one blockers (O) | L1, L2, L3, L10 | Without L2 `fill_report` silently returns an empty curve and degenerate equity metrics and drops an open final position. Without L3 every non-unit order needs a hand-written `resolve_execution_terms`, and a mistake is a run-terminating `TermsUnresolved` (native_execution_consumer.cpp:4976-4978). Without L1 "kernel" is a claim, not an artifact. L10 is the minimum buildable proof. | +| Day-one blockers (O) | L1, L2, L3, L10 | Without L2 `fill_report` silently returns an empty curve and degenerate equity metrics and drops an open final position. Without L3 every non-unit order needs a hand-written `resolve_execution_terms`, and a mistake is a run-terminating `TermsUnresolved` (native_execution_consumer.cpp:4987-4989). Without L1 "kernel" is a claim, not an artifact. L10 is the minimum buildable proof. | | Required by the promise (R5-11) | L0, L4, L5, L6, L7, L8 | A kernel that cannot liquidate is not a broker model (O); bracket / trailing corner cases are hit immediately (O); calc-timing and HTF are Pine features the user wants natively (R5-5, R5-6); the price grid is generic and missing (R5-3). | | Wanted with v1, not blocking | L11 a, d | removes the last `source::` name from kernel signatures and the orphaned TV commentary (F) | | In scope, 1.x | L9 | R5-9; a toolkit stand-in is acceptable meanwhile (F) | @@ -499,7 +499,7 @@ inside the per-bar body (corpus 442d497, `set_pine_risk_direction(1)` / `set_pine_risk_max_position_size(2)` / `set_pine_risk_max_intraday_filled_orders(3)`), so the adapter first sees a limit on script bar 0, from `PineStrategyHost::set_pine_risk_*` -(pine_strategy_host.cpp:952-974) — after `prepare_native_begin` has already +(pine_strategy_host.cpp:575-597) — after `prepare_native_begin` has already projected the spec (pine_strategy_host.cpp:163) and `configure_native` has folded it into the continuation digest. Measured: an env-gated probe printing the adapter's risk configuration inside `project()` on the corpus reads @@ -516,17 +516,17 @@ adapter's own rule acts. **Per rule (STEP 1), assuming the values were known at begin time.** Adapter line numbers are this tree's (683a82f); kernel semantics are -`NativeRiskLimits` native_run_spec.hpp:309 and `risk_evaluate` native_execution_consumer.cpp:3618 +`NativeRiskLimits` native_run_spec.hpp:310 and `risk_evaluate` native_execution_consumer.cpp:3692 (evaluated at the script bar's open, its close calculation and after each applied drain; fills counted at the corresponding adapter checkpoints; the block refuses `would_open`). | Pine rule | adapter mechanism | kernel counterpart | class | measured divergence (test scenario) | ruling | |---|---|---|---|---|---| -| `strategy.risk.allow_entry_in` | `risk_.direction`; `project()` already declares `spec.allowed_open_directions` (pine_adapter.cpp:2361); 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:12849-12850; 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:16391, `update_risk_state` pine_adapter.cpp:13484-13511): 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:12842), 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:1200-1208): +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:17724-17733); 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:15894); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16006), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14569-14578, `submit_intraday_loss_close` pine_adapter.cpp:14539-14567), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:16745); 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:5799, `:7457`, `:8181`, `:8309`, `:11292`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:17644-17647) | `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:13475-13477); 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:13381-13402, `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:2195); 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:13026-13027; 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:16685, `update_risk_state` pine_adapter.cpp:13661-13688): 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:13019), 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:1200-1208): +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:17858-17867); 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:16115); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16227), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14789-14798, `submit_intraday_loss_close` pine_adapter.cpp:14759-14787), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17039); 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:5891, `:7601`, `:8334`, `:8462`, `:11457`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:17938-17941) | `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:13652-13654); 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:13558-13579, `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, @@ -556,7 +556,7 @@ remain the five probes' baseline rows and the `SW` check above. **The P6 decision: native-only.** Re-verified on main `b8e7976e`: the ordering stands (`project()` at pine_strategy_host.cpp:242, `configure_native` at :243, -the `set_pine_risk_*` setters (`set_pine_risk_direction` pine_strategy_host.cpp:584-607), and corpus 442d497 +the `set_pine_risk_*` setters (`set_pine_risk_direction` pine_strategy_host.cpp:585-608), and corpus 442d497 `corpus/validation/cap-risk-gates-allow-max-intraday-01`'s generated strategy still emits the three calls inside `on_source_bar`); `rg "spec\.risk|NativeRiskLimits" src/source include/pineforge/source` is still empty; `test_adapter_risk_relower` still @@ -741,8 +741,8 @@ applies literally: the mask cannot be justified without the word "TradingView". And what the adapter keeps is not a duplicate of the kernel mechanism that a re-lowering would delete: `source_trigger_threshold` (pine_adapter.cpp:258), `source_level_on_price_grid` (:244) and the -`nearest_tick` (pine_adapter.cpp:257) / `source_bar_fill_tick` (:278) / -`directional_tick` (:302) spellings are a *different* rule — a half-tick threshold per order kind with +`nearest_tick` (pine_adapter.cpp:182) / `source_bar_fill_tick` (:203) / +`directional_tick` (:227) spellings are a *different* rule — a half-tick threshold per order kind with the fill booked elsewhere (E7) — which is why re-lowering moves outcomes instead of preserving them. @@ -848,7 +848,7 @@ so the gross open profit is 800. | **the equity re-mark** | `last.open_profit = 0; last.equity = initial_capital_ + net_profit_sum_ + range_end_pnl` | none — the kernel leaves the curve exactly as the run marked it | `RE1` (no fee): the adapter reports the curve's last point as `eq=100800 op=0`, the kernel as `eq=100800 op=800` — same equity, the open profit moved into the realized side. `RE2` (0.1 %): `eq=100758.8 op=0` against `eq=100800 op=800`; the adapter's equity is **41.2 lower**, exactly the round-trip commission of both lots (20.5 + 20.7), because the re-mark carries the NET row P&L where the run's own mark carried the GROSS open profit | retained: a report that restates the last equity point off net row P&L is TradingView's report shape. Re-marking the curve from the kernel would move a figure every bare host already reads | | **the extreme re-fold** | `max_equity_`/`min_equity_` reset to `initial_capital_`, `max_drawdown_`/`max_runup_` to 0, then `fold_equity_extreme` over the whole curve | none — the extremes stay as `update_equity_extremes` sampled them | `RE3` (a dip to 90 at bar 1, a peak of 108 at bar 3, 0.1 %): both sides agree on `maxeq=101400 mineq=99000 maxdd=2400`, and the run-up diverges — adapter `maxru=1758.8000000000029`, kernel `maxru=1800`. The re-fold is a no-op **except** where the re-marked last point differs, so the divergence is again the 41.2 round trip, modulo the binary fold order | retained: it is a consequence of the re-mark above, not a mechanism of its own; it cannot be re-lowered while the re-mark is adapter policy | | **the same-bar exit re-sort** | `sort_same_bar_exit_trades(trades_, adapter_)` — same-bar bracket exits ordered by script command sequence | none — the kernel never reorders `trades_` | not exercised by the four scenarios (it reorders CLOSED rows, not range-end rows); it is TradingView command-sequence ordering by construction (`git show ab9714be:src/source/pine_fills.cpp`, lines 664-670) | retained: script command sequence is a source-language fact | -| **the mark cadence** | three call sites, each clearing and rebuilding: the terminal sub-bar of the last source bar (`scheduler_record_range_end` pine_strategy_host.cpp:333), after every applied execution once terminal (`record_applied_range_end`, :649), and the scheduler's terminal-source-bar hook (`PineScheduler::applied` pine_scheduler_native.cpp:549, before the bar's broker hash) | once, after the last point of the run (`run_batch` ×3, `stream_end`) | structural, not a number: the adapter's rows are re-derived at each mark so the report reflects the book as of the last event on the terminal bar, and its re-marked equity is visible to that bar's broker hash. A one-shot run-end producer cannot occupy those three points | retained: three marks on a terminal bar is a TradingView report cadence, not a generic run-end fact | +| **the mark cadence** | three call sites, each clearing and rebuilding: the terminal sub-bar of the last source bar (`scheduler_record_range_end` pine_strategy_host.cpp:333), after every applied execution once terminal (`record_applied_range_end`, :502), and the scheduler's terminal-source-bar hook (`PineScheduler::applied` pine_scheduler_native.cpp:549, before the bar's broker hash) | once, after the last point of the run (`run_batch` ×3, `stream_end`) | structural, not a number: the adapter's rows are re-derived at each mark so the report reflects the book as of the last event on the terminal bar, and its re-marked equity is visible to that bar's broker hash. A one-shot run-end producer cannot occupy those three points | retained: three marks on a terminal bar is a TradingView report cadence, not a generic run-end fact | | **the excursion projection gate** | `excursion_range_end_projection_` suppressed the owner's exit-bar path fold for the projection | none — the generic builder folds what it folds | `RE1`: identical run-up/draw-down on both halves (`ru=500 dd=0`, `ru=300 dd=0`) | retired by R5 lane H-THIN: the Pine host owns no lot excursion since, so no owner fold is left to suppress; `RE1` still reads the same magnitudes, now the kernel's | | **the tail / warm-up suppression** | `if (stream_warmup_mode_ \|\| realtime_tail_ \|\| equity_curve_.empty()) return;` | none — the kernel emits at `stream_end` whatever the run left open | structural: the kernel has no warm-up or realtime-tail notion, and `run_corpus`'s probe-tail suppression (`suppress_probe_tail`) is a harness fact | retained: a tail is a source-layer replay concept | @@ -1009,9 +1009,9 @@ key, since replaced by the walk of the retained rows, `placement_` pine_state_hash.cpp:292) — one per accepted request, cleared only by `reset_for_run` pine_adapter.cpp:1942 — and reflected the whole admission journal (since replaced by a running digest of its events, `admission_journal` -pine_state_hash.cpp:573). `PineScheduler::hash_state` pine_state_hash.cpp:611 +pine_state_hash.cpp:580). `PineScheduler::hash_state` pine_state_hash.cpp:618 re-folded the consumed source prefix too (`consumed` -pine_state_hash.cpp:618-640). Measured with recording on and off, in two +pine_state_hash.cpp:625-647). Measured with recording on and off, in two shapes: - the audit's order-and-cancel probe (one `strategy_order` accepted and @@ -1054,10 +1054,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:8593). Full-table scans + (`has_full_entry_bracket` pine_adapter.cpp:8746). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1529, `revive_brackets_after_margin` pine_adapter.cpp:1751, - `preserved_by_close_all` pine_adapter.cpp:11323), and + `preserved_by_close_all` pine_adapter.cpp:11488), 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. @@ -1145,13 +1145,13 @@ retires every leg of each exit row it names and drops such a parked stop (`retire_cancelled_exits` pine_adapter.cpp:1677), 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:2763). Both shapes now cost ×1.89 to ×2.12 per doubling up +pine_adapter.cpp:2793). 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:3194); a lot opened +it off (`release_closed_cohort_origins` pine_adapter.cpp:3224); 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 @@ -1209,7 +1209,7 @@ engine.hpp:1666) is not. The other is a contract that the six folded fields of a booked row are never rewritten. In the tree they are not; the only writer after booking, the Pine host, rewrites `close_cause`, `exit_from_bracket`, `exit_comment` and `entry_incarnation` only; its -writes are `close_cause` pine_strategy_host.cpp:362, `exit_comment` :469, `close_cause` :483-488, `exit_from_bracket` :869 and `entry_incarnation` :1050-1058. +writes are `close_cause` pine_strategy_host.cpp:362, `exit_comment` :470, `close_cause` :484-489, `exit_from_bracket` :870 and `entry_incarnation` :1051-1059. But `trades_` engine.hpp:546 is a protected member any host can write, so a lane that takes the consumer-side digest has to make that contract explicit. Folding `(count, digest)` in place of the rows saves the 256-entry table but @@ -1520,14 +1520,14 @@ the rule gives 9.9 + 9.89). | # | Risk | Why it bites | Containment | Src | |---|---|---|---|---| -| E1 | **Spec hashing.** `hash_spec` folds every field unconditionally (native_execution_consumer.cpp:234-256) | 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:1620); 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:13922); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14462 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:2519, the COOF `at_fill` exception pine_adapter.cpp:6441) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5782) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5463 and `drain_applied_notifications` (native_execution_consumer.cpp:6290-6303); 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:15735-15764) | 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:1957) | 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:333) 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:46-78, *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:1620; 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 | +| E1 | **Spec hashing.** `hash_spec` folds every field unconditionally (native_execution_consumer.cpp:234-256) | 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:1665); 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:13979); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14519 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:2441, the COOF `at_fill` exception pine_adapter.cpp:6633) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5687) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5641 and `drain_applied_notifications` (native_execution_consumer.cpp:6302-6315); 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:15956-15985) | 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:1548) | 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:258) 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:46-78, *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:1213; full corpus diff; supplied-bar provenance preserved, never silently replaced by chart aggregation (S); differences on early-close sessions are recorded, not "fixed" (F). The Pine scheduler keeps one more `TimeframeAggregator` pass, before the run (`PineScheduler::run_begin`, audit M26): the script-bar count it yields (`terminal_source_bar`, the probe-tail suppression) and the per-input completion flags it hands `on_native_input` are needed before the first input arrives, while the kernel seals script intervals only as inputs arrive and offers no pre-run interval query (`NativeExecutionConsumer::script_interval_at` is consumer-private). Retained by R5 lane F7: replacing it needs that kernel capability | F O S | | E9 | **TA numerics are TV-calibrated** (1e-10 band in DMI / MFI / percentrank; conditional-call extremum ring `ExtremeRing` ta.hpp:134) | Non-Pine users may expect IEEE semantics; changing them breaks parity | Rename only (§2.ii b). For an every-bar caller the ring equals a positional window (the ring comment `sha256:fd1f92206558863ad8df33fd3ef3b8cc8b9bd6678a25fc98ca866168c1a81aca` ta.hpp:87-88). Document. → U6 | F O S | | E10 | **Dead-but-hashed lot flags** (§2.ii j) | Deleting them changes `broker_state_hash` | Separate, explicitly non-neutral lane (L12) | F O | | E11 | **OCA and ownership.** Pine's default-sized OCA and `from_entry` reservation rules are source policies | Source IDs, callsite tokens or Pine labels leaking into `WorkingRequestCore` | Opaque keys and receipts only (R5-4) | S | @@ -1540,11 +1540,11 @@ the rule gives 9.9 + 9.89). | P1 | **Epoch bumps** move the frozen C++ ABI fixtures and the settlement ABI checker `CLAUDE.md` names; a neutral-refactor worker may not edit frozen headers | Bump lanes are not neutral-refactor lanes; the brief lists the header extension (§3.1 c) | F O S | | P2 | **Stale fence.** `CLAUDE.md` names "measured sources" `engine_strategy_commands.cpp`, `engine_fills.cpp`, `engine_risk.cpp`, `engine_market_admission.cpp`; none exists in `src/` at `73817c1` (only `engine_orders.cpp` and `engine_run.cpp` do) | The supervisor restates the fence for the post-R4-C tree (likely `native_execution_consumer.cpp`, `native_order.cpp`, `engine_execution.cpp`, `src/source/*`) | F | | P3 | **Corpus gate state.** S's audit found `scripts/run_corpus.sh` stopping at the corpus strategy compile: committed `generated.cpp` files no longer match the engine (8/8 drifted from the pinned codegen commit under `VERIFY=1 ONLY=validation/analyzer scripts/regen_corpus_cpp.sh`) | §3.1 a relies on that script. Re-pin the engine / codegen corpus tuple before L1. Not re-verified in this merge (no builds were run) | S (single-source) | -| P4 | **Hash compatibility.** The source hash covers hundreds of adapter and scheduler fields (`PineExecutionAdapter::hash_state` pine_state_hash.cpp:237, `PineScheduler::hash_state` pine_state_hash.cpp:613); L4 / L9 add hashed state, L12 removes some | A hash mismatch is a diagnostic until the trade diff is known, never a reason to relax parity (S). Open: does the campaign gate compare broker-state hashes across releases, or only trades (O)? → Q3 | O S | +| P4 | **Hash compatibility.** The source hash covers hundreds of adapter and scheduler fields (`PineExecutionAdapter::hash_state` pine_state_hash.cpp:242, `PineScheduler::hash_state` pine_state_hash.cpp:618); L4 / L9 add hashed state, L12 removes some | A hash mismatch is a diagnostic until the trade diff is known, never a reason to relax parity (S). Open: does the campaign gate compare broker-state hashes across releases, or only trades (O)? → Q3 | O S | | P5 | **Test asymmetry:** 430 adapter test files vs 14 native | native-only tests in every lane (§3.1 b) | O | | P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, non-empty FX curves, security feeds and source setters (`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:298) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:585 → `set_risk_direction` pine_adapter.cpp:18133 (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:298) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:585 → `set_risk_direction` pine_adapter.cpp:18463 (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) @@ -1553,7 +1553,7 @@ the rule gives 9.9 + 9.89). | Q1 | Does `engine_script_run_v18` open at L4 and close after L5 ∥ L6, or does L4's sizing hook ship with that batch (§3.4 row 4)? | — (raised by this merge) | | Q2 | Is a hash-domain bump (needed by §2.ii j) acceptable before the R5 final audit (phase table, native-refactor-progress.md:23, *fresh*), or must L12 wait? | F asks; none | | Q3 | Does the 4190-probe gate compare broker-state hashes across releases, or only trades? If hashes, L4 and L12 need a hash-version plan | O asks; none | -| Q4 | `SizeTime::AtAcceptance` needs a current execution point (`current_execution_point` native_execution_consumer.cpp:2816); between realtime inputs there is none — reject or fall back to `AtMatch`? | F: reject with `InvalidQuantityBasis` | +| Q4 | `SizeTime::AtAcceptance` needs a current execution point (`current_execution_point` native_execution_consumer.cpp:2876); between realtime inputs there is none — reject or fall back to `AtMatch`? | F: reject with `InvalidQuantityBasis` | | Q5 | Liquidation check granularity: `PathAdverseExtreme` default; is a cheaper `CalculationOnly` worth shipping in v1? Pure spec value or injectable policy object (S)? | F: path-accurate default; merged A.3 = spec value + host sizing hook | | Q6 | Does a native series return the current value, a retained series object, or both? It decides C ABI ownership and hash size | S asks; A.5 offers push + current-bar pull | | Q7 | Is a close-reservation key stable across host restarts or only within one `RunIdentity`? | S: identity rules suggest run scope | @@ -1569,7 +1569,7 @@ the rule gives 9.9 + 9.89). | **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:15900-15916) 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:2478) 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:16121-16137) 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:2508) 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. | @@ -1591,11 +1591,11 @@ virtual void hash_host_extension(BrokerStateHashSink&) const {} // F (S: hash_ NativeMetricsView native_metrics() const; // S: net_profit, equity, closed_trades, open_lots; non-virtual ``` -- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` + `record_equity_point(script_open_ms)` (engine.hpp:1380, engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7210, native_execution_consumer.cpp:7210, native_execution_consumer.cpp:7210) 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:2357), with `report_open_position_at_end == false`. +- `KernelRecorded`: the consumer calls the existing `update_equity_extremes()` + `record_equity_point(script_open_ms)` (engine.hpp:1380, engine.hpp:1380) once per script calculation (after `invoke_callback` native_execution_consumer.cpp:7222, native_execution_consumer.cpp:7222, native_execution_consumer.cpp:7222) 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:2191), with `report_open_position_at_end == false`. - Publish read-only `closed_trade_*` accessors (RP6). - Alternative: F's virtual `owns_equity_recording()` (the A48 ownership pattern of `owns_lot_excursions` native_host.hpp:1023). Same semantics, but it costs a host-epoch bump, so the spec form was chosen; the virtual was never added, and `NativeReportPolicy` is what landed (§3.4 row 3). -- TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5073-5115), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. +- TUs: `native_execution_consumer.cpp` (seal path native_execution_consumer.cpp:5085-5127), `engine_run.cpp`, `engine_report.cpp`, `engine_state_hash.cpp`, `native_run_spec.{hpp,cpp}`. ### A.2 L3 — sizing, fractional reduce, sibling reservation (F P1, P7 + O G2 + S P2, P3) @@ -1618,7 +1618,7 @@ struct ScopeFraction { double fraction = 1.0; ScopeClaim claim = ScopeClaim::Gro using ReductionSize = std::variant; // F ``` -- Resolution: `units = cash / (price * point_value * fx)`, `cash` = the value or `fraction * marked_equity(price)`, `price` = the candidate's `default_resolved_price`. It plugs in where `HostSized` units resolve today (native_execution_consumer.cpp:693; terms resolution `resolve_execution_terms` native_execution_consumer.cpp:4816). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:877); validation reuses `RequestRejectReason::InvalidQuantityBasis` (native_order.hpp:827). +- Resolution: `units = cash / (price * point_value * fx)`, `cash` = the value or `fraction * marked_equity(price)`, `price` = the candidate's `default_resolved_price`. It plugs in where `HostSized` units resolve today (native_execution_consumer.cpp:693; terms resolution `resolve_execution_terms` native_execution_consumer.cpp:4958). A host override of `resolve_execution_terms` keeps the last word. Non-representable → `MatchRejectReason::TermsUnresolved` (native_order.hpp:877); validation reuses `RequestRejectReason::InvalidQuantityBasis` (native_order.hpp:827). - `NetOfSiblings`: a fraction-sized reduce claims the bound scope minus the units already claimed by live sibling reduces on it; keys are opaque, never source IDs (S). - Not adopted: S's spec-level `default_sizing` + virtual `resolve_native_sizing` (duplicates the per-request intent, costs an epoch); S's `PercentFreeMargin` (needs L4's free-margin definition) and `EveryCalculation` timing; O's spec-level `size_net_of_fees` (per-request flag, 2 of 3); O's `PercentOfScope` basis (expressed as `ScopeFraction`); S's `ReplaceByKey` / `Fifo` modes (callsite semantics → adapter, R5-4). - TUs: `native_order.{hpp,cpp}`, `native_execution_consumer.cpp`, `native_run_spec.cpp`, the definition hash. @@ -1645,9 +1645,9 @@ virtual std::optional resolve_margin_call_units(const NativeMarginCallVi virtual void on_native_margin_call(const native_order::MarginCallEvent&) {} // O, S ``` -- Mechanism (F): a **kernel-originated request**. At each script-bar open and after each applied fill the kernel computes the liquidation level and rests a `Reduce` / `Flatten` with `Stop{level}` bound to the live openings — the shape the adapter builds host-side today (pine_adapter.cpp:13223-13244). 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:2575), 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:13269-13290). 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:2622), 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:4675-4688 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). +- TUs: `native_run_spec.{hpp,cpp}`, `native_execution_consumer.cpp` (`admit_opening_inspect` native_execution_consumer.cpp:4684-4697 per side; scheduling next to `invoke_bar_open_callback` / `drain_after_applied`), `native_order.*`, `engine_state_hash.cpp` + the coverage checker, `c_abi.cpp` + `pineforge.h` (`pf_native_run_spec_v2` or an extension struct; `CLAUDE.md` SOP). ### A.4 L5 — calculation timing (F P4 + O G8, G9 + S P6) @@ -1667,10 +1667,10 @@ std::optional current_partial_bar() const; Callback chronology contract (the normative part of the lane; F P4.1 + O G8): -1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1697-1698) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6995-7007) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). -2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5463). `born_on_remaining_path` (native_execution_consumer.cpp:5629-5633) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1660-1667) is calibrated against it. -3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1083-1090, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6848-6855). `on_native_tick` stays the observation hook (S). -4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2553); valid in bar-open, applied and tick callbacks. +1. At one point: match and settle → `on_native_applied` per applied event, FIFO (rule already at `on_native_applied` native-engine.md:1718-1719) → with `BarCloseAndFills`, one recalculation at the fill cursor (reason `OrderFill`), driven from the existing notification drain (native_execution_consumer.cpp:6995-7007) under its re-entrancy guard, bounded by `max_recalculations_per_point`. S would run it *before* the drain; not chosen, the drain order is frozen (E4). +2. Requests born in any of these callbacks follow the existing birth rule: eligible on the unconsumed rest of the bar (`born_on_remaining_path` native_execution_consumer.cpp:5641). `born_on_remaining_path` (native_execution_consumer.cpp:5641-5645) is not touched — the adapter's COOF deferral (`pending_coof_requests_`, element type `PendingCoofRequest` pine_adapter.hpp:1641-1648) is calibrated against it. +3. `EveryModeledPoint`: a calculation at each magnifier sample / observed print, in batch as well as stream (every delivery loop asks the per-point guard native_execution_consumer.hpp:1083-1090, whose body is `recalculate_at_modeled_point` native_execution_consumer.cpp:6857-6864). `on_native_tick` stays the observation hook (S). +4. `current_partial_bar()` derives from the driver points already recorded (`record_driver` native_execution_consumer.cpp:2566); valid in bar-open, applied and tick callbacks. 5. The kernel never attempts language-state rollback; that stays Pine's (`snapshot / restore / commit_coof_script_state`, pine_scheduler_native.cpp:52-104) (O). ### A.5 L6 — native HTF subscriptions (F §2.iv + O G10 + S P7; design in §2.iv) @@ -1708,7 +1708,7 @@ struct FromOwnerFill { double offset = 0.0; bool ticks = false; }; // O: signe using TriggerAnchor = std::variant; // Request::anchor = Absolute{} struct TrailTicks { double ticks; }; // O: offset spelling resolved against price_tick struct ReplaceOptions { bool retain_trigger_state = false; }; // O: keeps `TrailTrack::best` (native_order.hpp:651) -// Trail::offset == 0.0 is accepted: "exit on the first adverse move past the best" (now accepted: `valid_trail_offset` native_order.cpp:479) +// Trail::offset == 0.0 is accepted: "exit on the first adverse move past the best" (now accepted: `valid_trail_offset` native_order.cpp:496) // include/pineforge/native_toolkit.hpp — header-only, Pine-free, covered by the include-independence checker (F; shapes from S P1) struct BracketSpec { native_order::RequestHandle parent; std::optional take_profit, stop_loss, trail; @@ -1736,7 +1736,7 @@ NativeGridRounding grid_rounding = NativeGridRounding::HalfUp; ``` - `QuantizeFills`: the kernel rounds `resolved_price` onto `price_tick` **before** slippage (`HalfUp`) or in the direction that favours the resting order (`Directional`). `QuantizeFillsAndTriggers`: the matcher additionally compares the quantized path against the raw level. -- TUs: `native_run_spec.cpp` (validation), `native_execution_consumer.cpp` (resolve path native_execution_consumer.cpp:3962-3984, `execute_current` path native_execution_consumer.cpp:3782-3792), `native_matching.hpp` (threshold form). The dead helpers were the reference arithmetic and are deleted (§2.ii e); the kernel's own step is `native_matching::apply_slippage` native_matching.hpp:259. +- TUs: `native_run_spec.cpp` (validation), `native_execution_consumer.cpp` (resolve path native_execution_consumer.cpp:3962-3984, `execute_current` path native_execution_consumer.cpp:3791-3801), `native_matching.hpp` (threshold form). The dead helpers were the reference arithmetic and are deleted (§2.ii e); the kernel's own step is `native_matching::apply_slippage` native_matching.hpp:259. ### A.8 L9 — risk limits (F P3 + O G7 + S P5) @@ -1753,10 +1753,10 @@ struct NativeRiskLimits { 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:1329) (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:373), not a chart clock. +- 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:13313-13405); 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:4675-4688 and the per-point walk), `native_order.hpp`, the state hash. +- Interim (F): a C++ host can build all four today from `native_marked_equity` + `on_native_applied`, which is what the adapter does (pine_adapter.cpp:13490-13582); 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:4684-4697 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 cda94bac..e757f5eb 100644 --- a/docs/native-refactor-progress.md +++ b/docs/native-refactor-progress.md @@ -173,18 +173,18 @@ repository and were never pushed. | 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:263`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:5175`), which still runs about three times a bar | PERF-D2, spark 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 | Branch `perf-d2/proto-throwaway` (`92e85571`); `$EV/exec/PERF-D2-scratch/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:268`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:5290`), which still runs about three times a bar | PERF-D2, spark 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 | Branch `perf-d2/proto-throwaway` (`92e85571`); `$EV/exec/PERF-D2-scratch/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, spark 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)" | Branch `perf-d3/proto-throwaway` (`a820b7df`, five commits on `b33ea659`); `$EV/exec/PERF-D3-scratch/patches/`; spark `~/perf-d3/` | 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:1029`) 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" | `$EV/exec/PERF-D3-scratch/patches/hold-ub-measurement-only.py` | As S1a. Size L, parity risk high: TradingView's margin money rules | | S2 and S2e, the codegen feature manifest | Codegen states which per-bar hooks and publications a strategy can reach, and the engine skips the others; no manifest means every feature. A median 39 of 52 are statically unreachable, over the 312 corpus strategies and the 100 slots alike. S2e adds two gates that stop the Pine risk ledgers of a strategy with no risk rule | PERF-D3, on top of S1: floor −14 % / −15 % in time (−6.4 % in instructions), 14 probes 0.943 / 0.934, 100 slots 0.942 / 0.945. The nine S2 gates are byte-identical. S2e adds about 2–3 % and moves the adapter hash domain `pineforge-source-adapter/v4` to its next version | As S1a; the supervisor's cap: "any structural S1/S2 redesign is a post-release epoch" | In `a820b7df` (a regex-based manifest scanner); `$EV/exec/PERF-D3-scratch/patches/` | A codegen and engine co-change with a corpus re-emission; S2e also needs an owner decision on the hash-domain step | | The kernel quiet-run loop and the Pine quiet calculation | A loop for consecutive quiet bars of a pumped batch at the chart's own timeframe (label and gap checks in the batch preflight, precomputed intervals and session facts, one calculation frame); and skipping a flat bar's post-script hooks when its script issued nothing | Estimated, not prototyped (PERF-D3's report, section 3.7): about 1,000 instructions a bar for the loop and 800 for the quiet calculation, taking the floor from 3,726 instructions (S1, S2 and D2-A) to about 1,900, or 180–200 ns a bar | As S1a (PERF-D3's follow-up row) | The PERF-D3 report only | After S1a and S1b | | PGO | Profile-guided optimisation of the library objects: an instrumented build, a training run over the bench strategies, a `-fprofile-use` rebuild and the identity gates, per compiler and target, with a staleness gate on the profile | LAYOUT1, spark A725, GCC 13.3, `perf stat`, trained on 86 slots and timed on the 14 held-out probes: 14.5 % fewer cycles, CPU time 0.865, 7 % fewer instructions, 26 % fewer L1i refills, 36 % fewer front-end stalls, layout spread 9.0 % → 5.1 %, fingerprints identical | "R5 LAYOUT1 DONE + ACCEPTED 2026-09-24 21:45 Taipei": "PGO APPROVED as a lane (PGO1) scheduled LAST in the perf wave after the code lanes (profile staleness)"; the supervisor's cap: "PGO folds into the release lane"; the owner: "release with PGO in the release build"; TRIAGE1 §D | `$EV/exec/LAYOUT1-scratch/`; spark `~/layout1/results/`. The tree carries only LAYOUT1's `-falign-functions=64` | The release lane. BENCH4 timed the standard Release build without PGO, and neither BENCH4's nor REL10-ENGINE's brief names it | -| 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:1651`) hands out the whole vector and the core holds `history_` (`include/pineforge/native_order.hpp:2126`) inside `native_order_v7`, so both are a layout and API change | L3, spark 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" | `r5/l3` (`b33ea659`, merged by INT22); `$EV/exec/L3-scratch/`; spark `~/l3/prof` | A `native_order` layout epoch | -| A checked-values handoff for executions (L3) | `execution_values` runs twice, in `check_execution` (`src/native_order.cpp:4764`) and again in `apply_execution` (`src/native_order.cpp:4778`); 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 | +| 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:2197`) inside `native_order_v7`, so both are a layout and API change | L3, spark 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" | `r5/l3` (`b33ea659`, merged by INT22); `$EV/exec/L3-scratch/`; spark `~/l3/prof` | A `native_order` layout epoch | +| A checked-values handoff for executions (L3) | `execution_values` runs twice, in `check_execution` (`src/native_order.cpp:4791`) and again in `apply_execution` (`src/native_order.cpp:4805`); handing the checked values across needs a public type, an API addition with no layout change | L3, as above: 2.21 % of bracket for the two together, about 1.1 % recoverable | L3 ruling 2: "a public checked-values handoff for executions (~1.1% bracket) -> DEFERRED" | As above | An owner decision on the API addition | | A chunked `trades_` store (L3) | Closed trades in chunks rather than one vector; `trades_` (`include/pineforge/engine.hpp:546`) is protected `BacktestEngine` state, so script ABI | L3, as above: on bracket, vector growth 0.64 %, `Trade` moves 0.74 % and `record_close_trade` 0.43 % (at most about 1.4 %); at most 0.35 % on the 14 probes | L3 ruling 4: "chunked trades_ store (ABI) -> DEFERRED" | As above | An `engine_script_run` epoch that changes the engine's protected layout | -| A zoned chart-day memo (D2-D finding 4) | On a chart with a timezone, `libc_chart_day_key` (`src/source/pine_adapter.cpp:13512`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:15893`) reads `chart_day_key` (`src/source/pine_adapter.cpp:16048`) unconditionally. The non-UTC monthly Sharpe/Sortino walk, `month_key_local` (`src/engine_metrics.cpp:32`), pays one `localtime_r` per equity point. UTC charts use D2-D's memo, `civil_chart_day_key` (`src/source/pine_adapter.cpp:13489`) | Not measured on a zoned chart. The UTC path cost 134 instructions a bar before D2-D's memo and 68 after (D2-D, spark, 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:438`). 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:563`), 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:258`) reads two thread-locals per call on a zoned clock, and the prepared order storage's `free_blocks` (`src/native_order.cpp:588`) 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 (spark 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 | `$EV/exec/AUDIT4-opus-scratch/perf/tls/` | 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 | +| A zoned chart-day memo (D2-D finding 4) | On a chart with a timezone, `libc_chart_day_key` (`src/source/pine_adapter.cpp:13531`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:15956`) reads `chart_day_key` (`src/source/pine_adapter.cpp:16112`) 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:13515`) | Not measured on a zoned chart. The UTC path cost 134 instructions a bar before D2-D's memo and 68 after (D2-D, spark, 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:443`). 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:559`), 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:248`) reads two thread-locals per call on a zoned clock, and the prepared order storage's `free_blocks` (`src/native_order.cpp:605`) one per prepared mutation. `ema_na_warmup_flag` is read once per EMA, at its first compute | One thread-local access per reader call. AUDIT4, macOS arm64, a dlopen'd tutorial-MACD probe, counted by `_tlv_get_addr` breakpoints and by descriptor patching: 1.000 a bar for one `ta::Highest` and for one default `ta::VWAP`, 1.999 for one `pine_hour` on a New York clock, 0.000 on the no-order floor and with orders. On Linux a TLS-descriptor call costs about 20 instructions: D2-C's 13 → 4 calls a bar was 260 → 80 instructions (spark 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 | `$EV/exec/AUDIT4-opus-scratch/perf/tls/` | A byte-identical lane that hands these readers the pump's block without a thread-local read. `ta::bar_context()` is script ABI, so its public declaration stays as it is | | "audit-off mode", "LTO", "data-oriented layout" | Named in "OWNER DECISION 2026-09-25 00:20 Taipei" among the post-release structural items: "S1 bulk quiet bars, S2 per-strategy specialization, audit-off mode, LTO+PGO, data-oriented layout". No engine or campaign document defines, designs or measures the three | None | The owner decision | None | Struck from this inventory until a design note defines one of them | ### Recorded witness drift @@ -194,12 +194,12 @@ repository and were never pushed. K4, the batch log pre-size. Since V19-B's step 6 (`08e71bae`) a run under the default `NativeEventRetention::Window` keeps no driver point and retires its journal at every script-bar boundary, so K4 has nothing to size. -`reserve_driver_log` (`src/native_execution_consumer.cpp:2538-2543`) returns +`reserve_driver_log` (`src/native_execution_consumer.cpp:2585-2590`) returns unless the retention is Full, and `presize_logs` -(`src/native_execution_consumer.cpp:8837-8848`) sizes no journal under Window. -`NativeRunSpec::event_retention` (`include/pineforge/native_run_spec.hpp:639`) +(`src/native_execution_consumer.cpp:9103-9114`) sizes no journal under Window. +`NativeRunSpec::event_retention` (`include/pineforge/native_run_spec.hpp:641`) defaults to Window, and the Pine adapter declares -`NativeEventRetention::Window` (`src/source/pine_adapter.cpp:2429`). K4 still +`NativeEventRetention::Window` (`src/source/pine_adapter.cpp:2322`). 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 spark) 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:5175`) returns at its receipt gate when no +(`src/source/pine_adapter.cpp:5290`) returns at its receipt gate when no command lies above its cursor. `every_gate_is_taken_both_ways` (`tests/test_adapter_quiet_bar.cpp:1036`) 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:1917-1918`), with their type + (`include/pineforge/source/pine_adapter.hpp:1920-1921`), 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/contributing-llm.md b/docs/pages/contributing-llm.md index 127fb047..afd2142f 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -93,7 +93,7 @@ finding to report, not a step to take. `static_assert`s in `src/c_abi.cpp` pin the layouts — if one fails on your branch, find what changed in the C++ representation. Adding a runtime `PF_API` export means the implementation, the declaration, - `EXPECTED_RUNTIME` check_c_abi_runtime.py:28, the ctypes harnesses and the + `EXPECTED_RUNTIME` check_c_abi_runtime.py:29, the ctypes harnesses and the README table, all in one commit. *Enforced:* `scripts/check_c_abi_runtime.py`. @@ -230,12 +230,12 @@ decision, and removing one is a regression: | Looks duplicated | Why both exist | Ruling of record | |---|---|---| -| the kernel's price grid (`NativeRunSpec::price_grid`) and the adapter's own tick rules | TradingView quantizes per *order kind* — stop and limit legs on the quantized bar, the trail stop and the `calc_on_order_fills` cursors raw — and the kernel grid is one rule for the run. A per-kind mask would spell that inconsistency into the kernel. | ADR 0001 ruling table, row `price_grid`; design `native-feature-parity.md:527` | -| the kernel's risk limits (`NativeRunSpec::risk`) and the adapter's `strategy.risk.*` | structurally, Pine's risk calls are per-bar statements that arrive after the spec has been digested; substantively, four measured divergences in the latch, the streak, the close price and the day key. | ADR 0001 ruling table, row `risk`; design `native-feature-parity.md:438` | +| the kernel's price grid (`NativeRunSpec::price_grid`) and the adapter's own tick rules | TradingView quantizes per *order kind* — stop and limit legs on the quantized bar, the trail stop and the `calc_on_order_fills` cursors raw — and the kernel grid is one rule for the run. A per-kind mask would spell that inconsistency into the kernel. | ADR 0001 ruling table, row `price_grid`; design `native-feature-parity.md:532` | +| the kernel's risk limits (`NativeRunSpec::risk`) and the adapter's `strategy.risk.*` | structurally, Pine's risk calls are per-bar statements that arrive after the spec has been digested; substantively, four measured divergences in the latch, the streak, the close price and the day key. | ADR 0001 ruling table, row `risk`; design `native-feature-parity.md:443` | | the kernel's `max_abs_units` and the adapter's `max_position_size` | the kernel caps the *resulting* book, TradingView gates the *live* book before the fill. | ADR 0001 ruling table, row `max_abs_units` | | the kernel's `max_open_lots` and Pine's `pyramiding` | the adapter counts *entries per cycle*, the kernel counts *physical lots*. Measured against TradingView the kernel's count is the closer one (14 of 15 tape scenarios against the adapter's 8, `tests/test_pyramiding_count_differential.cpp`): TradingView checks an entry once, at its first eligible point, against the trades then open. Lowered onto the cap, four corpus probes move away from TradingView, for want of that rule, so the adapter keeps its count. | ADR 0001 ruling table, row `max_open_lots` | | the kernel's margin model and the adapter's money admission | the adapter answers TradingView's ten-significant-digit admission itself and declares a *maintenance-only* model, because a positive initial requirement would decline openings TradingView takes and admit adds it refuses (measured both ways, `tests/test_adapter_margin_schedule_differential.cpp`). | ADR 0001 ruling table, row `initial_margin_fraction` | -| `NativeRunSpec::report_open_position_at_end` and the adapter's range-end rows | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it: report *shape*, not a mark-to-market row. | ADR 0001 ruling table, row `report_open_position_at_end`; design `native-feature-parity.md:624` | +| `NativeRunSpec::report_open_position_at_end` and the adapter's range-end rows | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it: report *shape*, not a mark-to-market row. | ADR 0001 ruling table, row `report_open_position_at_end`; design `native-feature-parity.md:629` | | `subscriptions` in the spec and the adapter's begin-time declaration | the adapter declares the same kernel subscriptions through a hook instead of the field, so a plain `request.security` site really is a kernel subscription. | ADR 0001 ruling table, row `subscriptions` | | the kernel's auxiliary feed and the adapter's auxiliary drive | the adapter's chart slice leaves pre-range coverage inert where the kernel folds by time, and evaluates after the bar's matching pass where the kernel delivers before it. | ADR 0001 ruling table, row `auxiliary_feed` | | `FeedTolerant` / `LegacyTolerant`, `NativeFeedTolerance` / `NativeLegacyTolerance` | deprecated spellings kept as exact aliases so existing hosts and the adapter compile unchanged; same value, same hash. | native_run_spec.hpp:311 and native_run_spec.hpp:353 | @@ -252,7 +252,7 @@ policy layer that *uses* kernel features. Where every TradingView rule lives. **front door** — one of the three ways in: PineScript through codegen, C++ through `NativeStrategyHost` native_host.hpp:845, or C through the -`strategy_native_*` surface (`strategy_native_host_create_v1` native_c_api.h:2598). +`strategy_native_*` surface (`strategy_native_host_create_v1` native_c_api.h:2658). **twin** — a test unit compiled twice, once against a frozen historical header closure and once against the current one, so a behaviour change has to be diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index df0ac551..d2e0efff 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -463,7 +463,7 @@ spec fields. The rich `run(bars, n, input_tf, script_tf, inputs, syminfo, overrides, …)` overload (`engine.hpp:1623-1634`) is **not** refused as a source mutation: it reaches `NativeExecutionConsumer::run_rich` -(`native_execution_consumer.cpp:9156-9196`), which admits the begin, checks the +(`native_execution_consumer.cpp:9229-9269`), which admits the begin, checks the timeframe arguments against the spec, preflights and pumps the batch exactly like the plain overload. `inputs` / `syminfo` / `overrides` are carried only as `NativeBeginArgs` fields to `prepare_native_begin` — the overrides as the @@ -491,7 +491,7 @@ Serialized external C++ calls may command only **between realtime inputs**, never reentrantly during input processing. A host written in C issues the same five commands through `strategy_native_submit_v1` / `_replace_v1` / `_cancel_v1` / `_cancel_all_v1` / `_cancel_where_v1` -(`native_c_api.h:2730-2780`), under the same legality rule; see *Driving the +(`native_c_api.h:2749-2799`), under the same legality rule; see *Driving the kernel from C* below. `native_order::Request` values belong to `native_order_v7` @@ -631,13 +631,13 @@ a host reacts to its own execution and may submit again. A request born there, mid-bar on a continuous segment, is eligible on the **remaining path suffix** of that segment — the birth is admitted at the current cursor and the geometric search then sees only the unconsumed suffix (`born_on_remaining_path`, -`native_execution_consumer.cpp:5629-5633`). Requests accepted before the +`native_execution_consumer.cpp:5641-5645`). Requests accepted before the segment, and discrete points, keep the ordinary birth gate above. `on_native_bar_open` fires at the modeled opening, before that point's matching -pass (`native_execution_consumer.cpp:7084-7086`). **Lookahead warning:** the +pass (`native_execution_consumer.cpp:7096-7098`). **Lookahead warning:** the `Bar` it receives is the *complete* script bar — the consumer has already set -`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6971`), the +`engine.current_bar_ = open_view` (`native_execution_consumer.cpp:6983`), the complete bar unless the spec asks for `NativeOpenBarView::OpenOnly` — so its high, low and close are the finished bar's, not what is known at the open. A host that must decide on open-only information reads @@ -1484,7 +1484,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:13686-13710`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:13868-13901`), 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 @@ -1935,8 +1935,8 @@ there lists what it leaves out. | `current_partial_bar()` | the lookahead-free bar so far, through the last path point consumed | `strategy_native_partial_bar_v1` | | `native_series_bar(i)` | the latest delivered bucket of subscription `i` | `strategy_native_series_bar_v1` | | `native_recalculation_count()` / `native_recalculations_skipped()` | the calculations the cadence drove and the ones its per-point bound dropped | `strategy_native_recalculations_v1` | -| `native_decision_floor()` (`native_host.hpp:1343`) | the run's monotonic decision floor in epoch ms — the same value `NativeStateView::decision_floor_ms` carries, and the lower bound every request's birth is compared against | `pf_native_state_v1::decision_floor_ms` | -| `native_consumed_high_water()` (`native_host.hpp:1348`) | the highest `run_number` this host has consumed. It lives **outside** per-run reset, so the next configure on the same host needs a strictly larger number; a fresh host reads 0 | `pf_native_state_v1::consumed_high_water` | +| `native_decision_floor()` (`native_host.hpp:1361`) | the run's monotonic decision floor in epoch ms — the same value `NativeStateView::decision_floor_ms` carries, and the lower bound every request's birth is compared against | `pf_native_state_v1::decision_floor_ms` | +| `native_consumed_high_water()` (`native_host.hpp:1366`) | the highest `run_number` this host has consumed. It lives **outside** per-run reset, so the next configure on the same host needs a strictly larger number; a fresh host reads 0 | `pf_native_state_v1::consumed_high_water` | | `native_continuation_hash()` | the consumer's continuation identity: a fold of its live state (*What the continuation and the broker-state hash fold*), the timezone folded by its content, so the same spec over the same bars and zone rules answers the same value on every host | `strategy_native_continuation_hash_v1` | | `native_sized_units(sized, price, equity, fx)` | the kernel's own `Sized` resolution as a pure query | `strategy_native_sized_units_v1` | | `inspect_current_execution(cmd)` | `NativeCurrentExecutionPreview`, with a `NativeCurrentRefusal` `native_host.hpp:697` when the command cannot be consumed here | none in 1.0 (scheduled for 1.1.0): `strategy_native_execute_current_v1` applies the command and is no preview | @@ -1978,8 +1978,8 @@ bar instead, for a host whose last input is still forming; a D/W/M bar holds whole days, so all four are true. Every callback of one script bar carries the same four, fills and fill recalculations included, and a C host reads the first three from `pf_native_decision_v1`'s session bytes -(`native_c_api.h:1454`). The kernel resolves each session day once through -`native_calendar::session_day_at` (`native_calendar.hpp:407`), which a host +(`native_c_api.h:1465`). The kernel resolves each session day once through +`native_calendar::session_day_at` (`native_calendar.hpp:455`), which a host may call too. `tests/test_native_session_day_facts.cpp` replays the TradingView session tapes through a bare host. It is a presentation snapshot copied onto the callback stack: writing to it cannot move the floor, the @@ -2099,7 +2099,7 @@ default, set while no run is active — because each row is a full the live state, not the run's length: the closed rows enter through a running digest). With the switch on, one row follows each point, after the extremes that point just folded -(`record_script_report_point`, `native_execution_consumer.cpp:7833`), so +(`record_script_report_point`, `native_execution_consumer.cpp:7881`), so ```text broker_state_hash_len == equity_curve_len == script_bars_processed @@ -2225,13 +2225,13 @@ under the ticket the model or the run named. A host running its own forced close states the cause on the row it produced. `closed_trade_close_cause(i)` (`engine.hpp:1833`) is the C++ read and -`strategy_closed_trade_close_cause` (`pineforge.h:1274`) the C one, with the +`strategy_closed_trade_close_cause` (`pineforge.h:1239`) the C one, with the same numbering: `-1` for a bad index or a NULL handle, `0` UNKNOWN, `1` SCRIPT, `2` BRACKET, `3` MARGIN_CALL, `4` INTRADAY_LOSS_CAP, `5` INTRADAY_FILL_CAP, `6` RANGE_END. A row closed at the end of the run (`open_at_end`) always answers `6`, ahead of every other cause. The ticket a row was booked under is `strategy_closed_trade_entry_id` / -`_exit_id` / `_exit_comment` (`pineforge.h:1197-1227`), which index exactly the +`_exit_id` / `_exit_comment` (`pineforge.h:1162-1192`), which index exactly the rows of `fill_report`'s trade array and take any handle this engine produces — including a `pf_strategy_t` from `strategy_native_host_create_v1`, which is how a C host reads back the ticket its own margin model declared. @@ -2910,7 +2910,7 @@ Only completed buckets are published, so this recipe has no lookahead by construction. It is the same class the kernel's own subscription evaluator aggregates with, and the one the kernel's `script_bucket_completions` query feeds when the Pine scheduler asks how its input span buckets -(`TimeframeAggregator` `native_execution_consumer.cpp:7913`). What it does +(`TimeframeAggregator` `native_execution_consumer.cpp:7963`). What it does **not** give you is what a declared subscription does: an `authoritative_bars` feed, the `gaps` and `lookahead` delivery rules, the lazy-seal chronology, a C spelling, and the @@ -2994,9 +2994,9 @@ These are existing refusals, not implied future features: confirmed bars carry those series A C host has the same stream and the same commands. Streaming needs no new -symbol — `strategy_stream_begin` and its family (`pineforge.h:748`) take +symbol — `strategy_stream_begin` and its family (`pineforge.h:729`) take a `pf_strategy_t` from `strategy_native_host_create_v1` unchanged — and -`strategy_native_submit_v1` (`native_c_api.h:2680`) obeys the one legality +`strategy_native_submit_v1` (`native_c_api.h:2699`) obeys the one legality rule its C++ spelling does. Rebuild strategy libraries against this engine. An ABI-v4 module without the diff --git a/docs/pages/pine-to-native.md b/docs/pages/pine-to-native.md index 81ad3f7c..72e99818 100644 --- a/docs/pages/pine-to-native.md +++ b/docs/pages/pine-to-native.md @@ -70,9 +70,9 @@ gain: (native_order.hpp:1001), `ExecutionAppliedEvent` (native_order.hpp:1146), `MatchRejectedEvent` (native_order.hpp:1108), `CancelledEvent` (native_order.hpp:1043) — readable in order from `native_events` - (native_host.hpp:1317). + (native_host.hpp:1335). - **Any language.** The same kernel drives from C through a callback table - (`pf_native_callbacks_v1` native_c_api.h:2463), so a host in Rust, Go, Python or Zig needs no C++ + (`pf_native_callbacks_v1` native_c_api.h:2475), so a host in Rust, Go, Python or Zig needs no C++ unless it needs a capability the 1.0 C surface does not spell (the 1.0 C boundary table, @ref native_engine). - **One binary, no transpiler.** Link `libpineforge`, ship an executable or a @@ -100,7 +100,7 @@ A `none` in the C++ column is a **ruling**, never a gap: it names why the kernel does not have the concept and where the behaviour lives. The two normative ruling tables are ADR-0001's "Kernel capabilities the Pine adapter does not declare" and the design record's §3.6 -(`native-feature-parity.md:412`); both are held against the tree by +(`native-feature-parity.md:417`); both are held against the tree by `scripts/check_native_feature_rulings.py`. Every example named here is a CTest row: `ctest --test-dir build -R example_` @@ -114,33 +114,33 @@ A Pine script declares its broker once, in the `strategy()` call, and inherits the rest from the chart. A native host declares *everything* in one `NativeRunSpec` (native_run_spec.hpp:554) applied by `configure_native` (native_host.hpp:1196); a C host sends `pf_native_run_spec_v1` -(pineforge.h:548) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2267) -through `strategy_configure_native_ext_v1` (native_c_api.h:3113). Setup is +(pineforge.h:510) plus `pf_native_run_spec_ext_v1` (native_c_api.h:2286) +through `strategy_configure_native_ext_v1` (native_c_api.h:3132). Setup is atomic: `Ready` → begin → `Completed`, and an incomplete value is refused rather than defaulted. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy()` | `NativeRunSpec` native_run_spec.hpp:554 | `pf_native_run_spec_v1` pineforge.h:548 | `hello_kernel.cpp` / `hello_kernel_c.c` | One atomic setup value. `validate_native_run_spec` native_run_spec.hpp:820 names the offending field before a run begins. | -| `initial_capital` | `initial_capital` native_run_spec.hpp:580 | `initial_capital` pineforge.h:539 | `hello_kernel.cpp` | Required, positive. | -| `currency` | `currency` native_run_spec.hpp:571 | `currency` pineforge.h:537 | `hello_kernel.cpp` | With `ticker` native_run_spec.hpp:568, `tickerid` native_run_spec.hpp:569, `type` native_run_spec.hpp:570, `basecurrency` native_run_spec.hpp:572, `description` native_run_spec.hpp:573 and `volumetype` native_run_spec.hpp:574 this is Pine's whole `syminfo` block, named rather than inherited from a chart. | -| `commission_type` | `NativeFeeKind` native_run_spec.hpp:19 | `fee_kind` pineforge.h:540 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | -| `commission_value` | `fee_value` native_run_spec.hpp:590 | `fee_value` pineforge.h:540 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | -| `slippage` | `slippage_ticks` native_run_spec.hpp:584 | `slippage_ticks` pineforge.h:540 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3971-3972. TradingView's round-then-slip order is adapter-only. | -| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:541 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7540-7541, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | -| `pyramiding` | `max_open_lots` native_run_spec.hpp:596 | `max_open_lots` pineforge.h:543 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:2914). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). Lowered onto the cap, four corpus probes move away from TradingView, for want of its first-eligible-point rule, so the adapter keeps its count. | -| `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:348 with `size_basis` native_c_api.h:2104 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | -| `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2108 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | -| `calc_on_order_fills` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2278 | `native_calc_on_fills_strategy.cpp` | `BarCloseAndFills` recalculates once at the cursor of each applied execution, bounded by `max_recalculations_per_point` native_run_spec.hpp:627. The generic cadence only: TradingView's waypoint-only refill, its two-fills-at-open rule and its script-state rollback stay in the adapter. | -| `calc_on_every_tick` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2278 | `native_calc_on_fills_strategy.cpp` | `EveryModeledPoint` additionally recalculates at every modeled point of the delivered path and at every observed print. In a stream, `on_native_tick` native_host.hpp:878 is one call per accepted realtime print whatever the trigger is. | -| `margin_long` | `NativeMarginModel::initial_long` native_run_spec.hpp:241 | `margin_initial_long` native_c_api.h:2288 | `native_margin_strategy.cpp` | A fraction, not a percent. `0.0` is the maintenance-only spelling for that side: the kernel enforces no opening requirement there and the host owns opening admission. | -| `margin_short` | `NativeMarginModel::initial_short` native_run_spec.hpp:242 | `margin_initial_short` native_c_api.h:2292 | `native_margin_strategy.cpp` | Per side, unlike the one-scalar `initial_margin_fraction` native_run_spec.hpp:598 the model replaces. `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 add the liquidation half Pine has no word for. | +| `strategy()` | `NativeRunSpec` native_run_spec.hpp:554 | `pf_native_run_spec_v1` pineforge.h:510 | `hello_kernel.cpp` / `hello_kernel_c.c` | One atomic setup value. `validate_native_run_spec` native_run_spec.hpp:820 names the offending field before a run begins. | +| `initial_capital` | `initial_capital` native_run_spec.hpp:580 | `initial_capital` pineforge.h:501 | `hello_kernel.cpp` | Required, positive. | +| `currency` | `currency` native_run_spec.hpp:571 | `currency` pineforge.h:499 | `hello_kernel.cpp` | With `ticker` native_run_spec.hpp:568, `tickerid` native_run_spec.hpp:569, `type` native_run_spec.hpp:570, `basecurrency` native_run_spec.hpp:572, `description` native_run_spec.hpp:573 and `volumetype` native_run_spec.hpp:574 this is Pine's whole `syminfo` block, named rather than inherited from a chart. | +| `commission_type` | `NativeFeeKind` native_run_spec.hpp:19 | `fee_kind` pineforge.h:502 | `native_open_lots_strategy.cpp` | `Percent`, `CashPerUnit`, `CashPerExecution` — the same three TradingView offers. | +| `commission_value` | `fee_value` native_run_spec.hpp:590 | `fee_value` pineforge.h:502 | `native_open_lots_strategy.cpp` | Quoted per execution by `quote_execution_commissions` engine_execution.cpp:1588. A cash-per-execution ticket is split by units over every slice of that execution. | +| `slippage` | `slippage_ticks` native_run_spec.hpp:584 | `slippage_ticks` pineforge.h:502 | `native_price_grid_strategy.cpp` | Raw `ticks * price_tick`, applied once native_execution_consumer.cpp:3980-3981. TradingView's round-then-slip order is adapter-only. | +| `process_orders_on_close` | `NativeCloseExecution` native_run_spec.hpp:33 | `close_execution` pineforge.h:503 | `native_open_lots_strategy.cpp` | `AfterCalculation` emits the post-calculation close point native_execution_consumer.cpp:7588-7589, so a request submitted in bar *k*'s calculation fills at bar *k*'s own close. | +| `pyramiding` | `max_open_lots` native_run_spec.hpp:596 | `max_open_lots` pineforge.h:505 | `native_open_lots_strategy.cpp` | Caps surviving+new physical lots (`resulting_lot_count` native_execution_consumer.cpp:2974). The Pine adapter keeps its own per-cycle *entry count*, ruled **adapter-policy** in ADR-0001's table row `max_open_lots`; measured against TradingView it is not TradingView's rule, and this cap is the closer one (14 of 15 tape scenarios against 8, `tests/test_pyramiding_count_differential.cpp`). Lowered onto the cap, four corpus probes move away from TradingView, for want of its first-eligible-point rule, so the adapter keeps its count. | +| `default_qty_type` | `Sized` native_order.hpp:164 with `CashValue` native_order.hpp:91 or `EquityFraction` native_order.hpp:97 | `PF_NATIVE_INTENT_SIZED` native_c_api.h:348 with `size_basis` native_c_api.h:2123 | `native_sized_report_strategy.cpp` | The size is a property of the *request*, not of the run: a native host may size one order by cash and the next by equity fraction. `Transact` native_order.hpp:41 is the fixed-units form. Pine itself has no per-call `qty_type` — TradingView's compiler refuses one — so a script's `strategy.entry(qty = ...)` is always units; the source layer's typed per-call entry, which only generated or C++ callers reach, converts its money exactly as the declared default does. | +| `default_qty_value` | the basis's own scalar: `CashValue::cash` native_order.hpp:92, `EquityFraction::fraction` native_order.hpp:98 | `intent_value` native_c_api.h:2127 | `native_sized_report_strategy.cpp` | `SizeTime` native_order.hpp:106 chooses *when* the basis resolves and `SizePrice` native_order.hpp:146 *which* price it converts at; both compose. | +| `calc_on_order_fills` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2297 | `native_calc_on_fills_strategy.cpp` | `BarCloseAndFills` recalculates once at the cursor of each applied execution, bounded by `max_recalculations_per_point` native_run_spec.hpp:627. The generic cadence only: TradingView's waypoint-only refill, its two-fills-at-open rule and its script-state rollback stay in the adapter. | +| `calc_on_every_tick` | `NativeCalculationTrigger` native_run_spec.hpp:94 | `calculation` native_c_api.h:2297 | `native_calc_on_fills_strategy.cpp` | `EveryModeledPoint` additionally recalculates at every modeled point of the delivered path and at every observed print. In a stream, `on_native_tick` native_host.hpp:878 is one call per accepted realtime print whatever the trigger is. | +| `margin_long` | `NativeMarginModel::initial_long` native_run_spec.hpp:241 | `margin_initial_long` native_c_api.h:2307 | `native_margin_strategy.cpp` | A fraction, not a percent. `0.0` is the maintenance-only spelling for that side: the kernel enforces no opening requirement there and the host owns opening admission. | +| `margin_short` | `NativeMarginModel::initial_short` native_run_spec.hpp:242 | `margin_initial_short` native_c_api.h:2311 | `native_margin_strategy.cpp` | Per side, unlike the one-scalar `initial_margin_fraction` native_run_spec.hpp:598 the model replaces. `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 add the liquidation half Pine has no word for. | | `close_entries_rule` | none — a native close names the lots it closes | — | `native_selected_strategy.cpp` | Pine picks FIFO or ANY for the whole script. A native reduce is explicit instead: `BindOpening` native_order.hpp:445 closes one opening's lots, `BindOpenings` native_order.hpp:454 a chosen set, a bare `Reduce` native_order.hpp:220 the book in FIFO order. The adapter keeps the run-level rule (`close_entries_rule` pine_adapter.hpp:88). | | `backtest_fill_limits_assumption` | none — a generic limit fills when the level is reached | — | `tests/test_native_order_core.cpp` | TradingView's assumption is "the price must exceed the limit by *n* ticks". The generic knob beside it is `Limit::fill_through` native_order.hpp:239, which makes a limit market-if-touched; the *n*-tick assumption itself is a TradingView rule and stays in the adapter. | -| `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:378 | `intrabar_kind` native_c_api.h:2331 | `tests/test_native_calc_timing.cpp` | The host owns the lower bars, as `lower_tf` native_run_spec.hpp:394 or `synthesized` native_run_spec.hpp:408. The C magnifier arguments of `run_backtest_full` pineforge.h:648 configure a *compiled Pine strategy*, never a native host. | +| `use_bar_magnifier` | `IntrabarPath` native_run_spec.hpp:378 | `intrabar_kind` native_c_api.h:2350 | `tests/test_native_calc_timing.cpp` | The host owns the lower bars, as `lower_tf` native_run_spec.hpp:394 or `synthesized` native_run_spec.hpp:408. The C magnifier arguments of `run_backtest_full` pineforge.h:610 configure a *compiled Pine strategy*, never a native host. | | `fill_orders_on_standard_ohlc` | none — the kernel matches the bars you feed it | — | — | TradingView's switch exists because a Heikin-Ashi chart's candles are not the traded prices. A native host feeds the standard OHLC it wants matched, and derives any transformed series itself. | | `max_bars_back` | none — a native host owns its own history | — | `hello_kernel.cpp` | Pine infers a buffer length for every series. `pineforge::Series` series.hpp:94 is a fixed-capacity ring you size yourself, so nothing is inferred and nothing silently truncates. | -| `risk_free_rate` | none — the ratio is a report field with TradingView's fixed rate | `sharpe_monthly` pineforge.h:276 | `native_sized_report_strategy.cpp` | `pf_metrics_t::equity.sharpe_monthly` and `pf_metrics_t::equity.sortino_monthly` (`sortino_monthly` pineforge.h:282) use a fixed 2 %/yr, the TradingView default; nothing in the kernel takes a declared rate. Their pre-1.0 spellings `sharpe_tv` / `sortino_tv` were removed for 1.0; the serialized report keys stay `sharpe_tv` / `sortino_tv`. A host that needs another rate computes it from `pf_report_t::equity_curve`. | +| `risk_free_rate` | none — the ratio is a report field with TradingView's fixed rate | `sharpe_monthly` pineforge.h:290 | `native_sized_report_strategy.cpp` | `pf_metrics_t::equity.sharpe_monthly` and `pf_metrics_t::equity.sortino_monthly` (`sortino_monthly` pineforge.h:296) use a fixed 2 %/yr, the TradingView default; nothing in the kernel takes a declared rate. Their pre-1.0 spellings `sharpe_tv` / `sortino_tv` were removed for 1.0; the serialized report keys stay `sharpe_tv` / `sortino_tv`. A host that needs another rate computes it from `pf_report_t::equity_curve`. | ### The feed-shape and presentation policies Pine has no word for @@ -263,7 +263,7 @@ source-only features state why a native counterpart does not exist. | `strategy.percent_of_equity` | `EquityFraction` native_order.hpp:97 | `PF_NATIVE_SIZE_BASIS_EQUITY_FRACTION` native_c_api.h:373 | `native_sized_report_strategy.cpp` | A fraction of the marked equity at the sizing point: `0.10` is ten percent, never `10`. | | `strategy.long` | `Side::Long` native_order.hpp:65 | `PF_NATIVE_SIDE_LONG` native_c_api.h:366 | `native_selected_strategy.cpp` | For `Transact` it is the sign of `signed_units` order_action.hpp:10; `Sized::side` native_order.hpp:165 and `HostSized::side` native_order.hpp:75 name it explicitly. | | `strategy.short` | `Side::Short` native_order.hpp:65 | `PF_NATIVE_SIDE_SHORT` native_c_api.h:367 | `native_selected_strategy.cpp` | Same. | -| `strategy.default_entry_qty()` | none — the kernel has no declaration-level default quantity | — | `native_sized_report_strategy.cpp` | Every native request carries its own size, so there is no run-level default to read back. The adapter keeps Pine's (`default_qty_value` pine_adapter.hpp:67), and `strategy_set_override` pineforge.h:690 overrides it for a compiled Pine strategy. | +| `strategy.default_entry_qty()` | none — the kernel has no declaration-level default quantity | — | `native_sized_report_strategy.cpp` | Every native request carries its own size, so there is no run-level default to read back. The adapter keeps Pine's (`default_qty_value` pine_adapter.hpp:67), and `strategy_set_override` pineforge.h:652 overrides it for a compiled Pine strategy. | `HostSized` native_order.hpp:73 is the sixth intent and the escape hatch: the host answers the units itself from `resolve_execution_terms` @@ -271,7 +271,7 @@ host answers the units itself from `resolve_execution_terms` `RemainingUnits` native_order.hpp:550 and may return its own. A C host answers it only for the two host-sized closes the C surface admits, a cohort close and a Book-scoped `WAIT_FOR_APPLIED` child, through `on_close_units` -native_c_api.h:2555; a host-sized opening, and replacing the kernel's own `Sized` +native_c_api.h:2567; a host-sized opening, and replacing the kernel's own `Sized` or `ScopeFraction` units, are C++-only in 1.0. ## Orders {#pine_to_native_map_orders} @@ -280,27 +280,27 @@ Everything here is one call: `submit` native_host.hpp:1213, or `submit_market` native_host.hpp:1227 for the market-only shorthand, which *refuses* non-market extras rather than dropping them. `submit` returns a `RequestHandle` native_order_identity.hpp:135; keep it, it is the order id. In C the same call -is `strategy_native_submit_v1` native_c_api.h:2680 over a -`pf_native_request_v1` native_c_api.h:2095. +is `strategy_native_submit_v1` native_c_api.h:2699 over a +`pf_native_request_v1` native_c_api.h:2114. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2680 | `native_market_strategy.cpp` | Pine's entry *reverses* an opposite position; `Transact` does not. Spell the reversal explicitly with the row below. | -| `strategy.order()` | the same `Request` native_order.hpp:483 | `strategy_native_submit_v1` native_c_api.h:2680 | `native_market_strategy.cpp` | `strategy.order` is the non-reversing entry, so it maps 1:1 with no caveat. | -| `strategy.exit()` | `submit_bracket` native_toolkit.hpp:160 over a `BracketSpec` native_toolkit.hpp:37 | `strategy_native_submit_v1` native_c_api.h:2680 per leg | `native_bracket_strategy.cpp` | The full bracket is described below in [Brackets](@ref pine_to_native_brackets). | +| `strategy.entry()` | `Request` native_order.hpp:483 carrying `Transact` native_order.hpp:41 | `strategy_native_submit_v1` native_c_api.h:2699 | `native_market_strategy.cpp` | Pine's entry *reverses* an opposite position; `Transact` does not. Spell the reversal explicitly with the row below. | +| `strategy.order()` | the same `Request` native_order.hpp:483 | `strategy_native_submit_v1` native_c_api.h:2699 | `native_market_strategy.cpp` | `strategy.order` is the non-reversing entry, so it maps 1:1 with no caveat. | +| `strategy.exit()` | `submit_bracket` native_toolkit.hpp:160 over a `BracketSpec` native_toolkit.hpp:37 | `strategy_native_submit_v1` native_c_api.h:2699 per leg | `native_bracket_strategy.cpp` | The full bracket is described below in [Brackets](@ref pine_to_native_brackets). | | `strategy.close()` | `Reduce` native_order.hpp:220 with `OwnerOpenedUnits` native_order.hpp:191 and `BindOpening` native_order.hpp:445 | `PF_NATIVE_REDUCE_OWNER_OPENED` native_c_api.h:354 with `PF_NATIVE_OWNER_BIND_OPENING` native_c_api.h:459 | `native_selected_strategy.cpp` | Closes the lots one opening produced. The cycle comes from `ExecutionAppliedEvent::cycle_after` native_order.hpp:1165. A fractional close is `ScopeFraction` native_order.hpp:207 — `qty_percent = 50` is `fraction = 0.5`, with `ScopeClaim` native_order.hpp:195 and `ScopeBasis` native_order.hpp:202 saying whether siblings are netted and when the scope is measured. On a `quantity_grid` a fraction below 1 is floored onto the grid, and `qty_percent = 100` (`fraction = 1`) of the gross scope, settled in one fill, closes the whole scope unfloored. | | `strategy.close_all()` | `Flatten` native_order.hpp:36 | `PF_NATIVE_INTENT_FLATTEN` native_c_api.h:337 | `hello_kernel.cpp` / `hello_kernel_c.c` | The whole book, in one settlement cycle. | -| `strategy.cancel()` | `cancel_where` native_host.hpp:1278 with `NativeRequestField::Label` native_host.hpp:691 | `strategy_native_cancel_where_v1` native_c_api.h:2767 | `native_trail_risk_strategy.cpp` | Withdraws exactly the live requests whose `Request::label` native_order.hpp:485 is that id, and answers how many. `cancel` native_host.hpp:1249 is the single-request form for a host that kept the handle. | -| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1267 | `strategy_native_cancel_all_v1` native_c_api.h:2746 | `native_trail_risk_strategy.cpp` | Neither field is indexed: every form walks the live book once. A `PendingUntilArmed` native_order.hpp:394 child is absent from `native_working_requests` native_host.hpp:1263 before its arm, and all three cancels still address it. | +| `strategy.cancel()` | `cancel_where` native_host.hpp:1278 with `NativeRequestField::Label` native_host.hpp:691 | `strategy_native_cancel_where_v1` native_c_api.h:2786 | `native_trail_risk_strategy.cpp` | Withdraws exactly the live requests whose `Request::label` native_order.hpp:485 is that id, and answers how many. `cancel` native_host.hpp:1249 is the single-request form for a host that kept the handle. | +| `strategy.cancel_all()` | `cancel_all` native_host.hpp:1267 | `strategy_native_cancel_all_v1` native_c_api.h:2765 | `native_trail_risk_strategy.cpp` | Neither field is indexed: every form walks the live book once. A `PendingUntilArmed` native_order.hpp:394 child is absent from `native_working_requests` native_host.hpp:1263 before its arm, and all three cancels still address it. | | `strategy.oca.cancel` | `GroupEffect::Cancel` native_order.hpp:466 | `PF_NATIVE_GROUP_CANCEL` native_c_api.h:478 | `native_bracket_strategy.cpp` | Membership is `Member` native_order.hpp:468 inside `Group` native_order.hpp:473. | | `strategy.oca.reduce` | `GroupEffect::Reduce` native_order.hpp:466 | `PF_NATIVE_GROUP_REDUCE` native_c_api.h:479 | `tests/test_native_resting_group_contract.cpp` | The sibling's quantity is reduced instead of cancelled. | | `strategy.oca.none` | `NoGroup` native_order.hpp:467 | `PF_NATIVE_GROUP_NONE` native_c_api.h:472 | `hello_kernel.cpp` | The default: no group, no sibling effect. | -| `strategy.direction.all` | `NativeOpenDirections::Both` native_run_spec.hpp:76 | `allowed_open_directions` pineforge.h:541 | `native_risk_limits_strategy.cpp` | The default. | -| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:541 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2947. | -| `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:541 | `tests/test_native_run_spec.cpp` | Same, the other way. | -| `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:540 | `native_sized_report_strategy.cpp` | `fee_value` is percent ÷ 100 of the absolute account notional. | -| `strategy.commission.cash_per_contract` | `NativeFeeKind::CashPerUnit` native_run_spec.hpp:21 | `fee_kind` pineforge.h:540 | `tests/test_native_selected_requests.cpp` | Account currency per unit. | -| `strategy.commission.cash_per_order` | `NativeFeeKind::CashPerExecution` native_run_spec.hpp:22 | `fee_kind` pineforge.h:540 | `native_open_lots_strategy.cpp` | One ticket per execution, split by units over every slice that execution touches — the closed rows and, for a reversal, the lot it opens. | +| `strategy.direction.all` | `NativeOpenDirections::Both` native_run_spec.hpp:76 | `allowed_open_directions` pineforge.h:503 | `native_risk_limits_strategy.cpp` | The default. | +| `strategy.direction.long` | `NativeOpenDirections::Long` native_run_spec.hpp:74 | `allowed_open_directions` pineforge.h:503 | `tests/test_native_run_spec.cpp` | An opening the direction forbids is refused with `OpeningDirection` native_execution_consumer.cpp:2963. | +| `strategy.direction.short` | `NativeOpenDirections::Short` native_run_spec.hpp:75 | `allowed_open_directions` pineforge.h:503 | `tests/test_native_run_spec.cpp` | Same, the other way. | +| `strategy.commission.percent` | `NativeFeeKind::Percent` native_run_spec.hpp:20 | `fee_kind` pineforge.h:502 | `native_sized_report_strategy.cpp` | `fee_value` is percent ÷ 100 of the absolute account notional. | +| `strategy.commission.cash_per_contract` | `NativeFeeKind::CashPerUnit` native_run_spec.hpp:21 | `fee_kind` pineforge.h:502 | `tests/test_native_selected_requests.cpp` | Account currency per unit. | +| `strategy.commission.cash_per_order` | `NativeFeeKind::CashPerExecution` native_run_spec.hpp:22 | `fee_kind` pineforge.h:502 | `native_open_lots_strategy.cpp` | One ticket per execution, split by units over every slice that execution touches — the closed rows and, for a reversal, the lot it opens. | Two more order concepts Pine has no word for, both generic: `ReverseTo` native_order.hpp:60 is one exact signed target reached in one @@ -320,9 +320,9 @@ when a rejection's `RequestRejectReason` native_order.hpp:821 matters. | --- | --- | --- | --- | --- | | `limit=` | `Limit` native_order.hpp:237 | `PF_NATIVE_TRIGGER_LIMIT` native_c_api.h:391 | `native_calc_on_fills_strategy.cpp` | `fill_through` native_order.hpp:239 makes it market-if-touched: the level gates when the request becomes executable, and the fill is no longer bounded by it. | | `stop=` | `Stop` native_order.hpp:246 | `PF_NATIVE_TRIGGER_STOP` native_c_api.h:392 | `native_price_grid_strategy.cpp` | Stop-limit is `StopLimit` native_order.hpp:252. | -| `trail_points` | `FromOwnerFill{+ticks, true}` native_order.hpp:331 | `anchor = PF_NATIVE_ANCHOR_FROM_OWNER_FILL`, `anchor_offset = ticks`, `anchor_offset_in_ticks = 1` native_c_api.h:2112-2119 | `native_bracket_strategy.cpp` | Pine points are the arm distance from the entry, in ticks. The adapter lowers that anchor distance separately from the ride offset; it is not `TrailTicks`. | -| `trail_offset` | `TrailTicks` native_order.hpp:293 (resolved into `Trail::offset` native_order.hpp:291) | `p1 = ticks`, `trail_offset_in_ticks = 1` native_c_api.h:2113-2117 | `native_trail_risk_strategy.cpp` | Pine offset is the ride distance behind the running best, also expressed in ticks. Zero is legal and means "ride the best"; the first adverse move past it exits. | -| `trail_price` | `Trail::arm_price` native_order.hpp:292 | `trail_has_arm_price` native_c_api.h:2118 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1092 (C: `strategy_native_trail_state_v1` native_c_api.h:2992). The kernel compares `arm_price` with the **raw** path. TradingView tests the activation on the **tick-quantized** bar, with or without an offset and the placement close included (R5 lanes E5 and E9's tapes), so the Pine adapter arms at the activation's half-tick threshold — a placement close whose tick reaches the activation arms there — and books from a running best that starts at the activation; a one-shot (`trail_offset` 0) books the tick its resting threshold stands for, rounded away from the position. Since R5 lane E14 that running best is the **kernel's** own: the trailing leg names the activation — or the favourable one of the activation and the placement print when that print armed it — in the generic `Trail::best_seed` native_order.hpp:294, so the ride no longer starts half a tick short of the activation. | +| `trail_points` | `FromOwnerFill{+ticks, true}` native_order.hpp:331 | `anchor = PF_NATIVE_ANCHOR_FROM_OWNER_FILL`, `anchor_offset = ticks`, `anchor_offset_in_ticks = 1` native_c_api.h:2131-2138 | `native_bracket_strategy.cpp` | Pine points are the arm distance from the entry, in ticks. The adapter lowers that anchor distance separately from the ride offset; it is not `TrailTicks`. | +| `trail_offset` | `TrailTicks` native_order.hpp:293 (resolved into `Trail::offset` native_order.hpp:291) | `p1 = ticks`, `trail_offset_in_ticks = 1` native_c_api.h:2132-2136 | `native_trail_risk_strategy.cpp` | Pine offset is the ride distance behind the running best, also expressed in ticks. Zero is legal and means "ride the best"; the first adverse move past it exits. | +| `trail_price` | `Trail::arm_price` native_order.hpp:292 | `trail_has_arm_price` native_c_api.h:2137 | `native_trail_risk_strategy.cpp` | The absolute level the trail arms at. Read the live projection with `trail_state` native_host.hpp:1092 (C: `strategy_native_trail_state_v1` native_c_api.h:3011). The kernel compares `arm_price` with the **raw** path. TradingView tests the activation on the **tick-quantized** bar, with or without an offset and the placement close included (R5 lanes E5 and E9's tapes), so the Pine adapter arms at the activation's half-tick threshold — a placement close whose tick reaches the activation arms there — and books from a running best that starts at the activation; a one-shot (`trail_offset` 0) books the tick its resting threshold stands for, rounded away from the position. Since R5 lane E14 that running best is the **kernel's** own: the trailing leg names the activation — or the favourable one of the activation and the placement print when that print armed it — in the generic `Trail::best_seed` native_order.hpp:294, so the ride no longer starts half a tick short of the activation. | `limit=` and `stop=` are listed once here rather than once per command, because a native trigger is a property of the `Request` native_order.hpp:483 @@ -377,10 +377,10 @@ of a predicate, and it reports the kernel verbatim too: `ReplaceResult` and `SubmitResult` of every command that actually reached the kernel, and `cancel_outcome` says whether a cancel was commanded at all. Pine's `strategy.cancel` tells a script neither. In C -each leg is its own `strategy_native_submit_v1` native_c_api.h:2680 with +each leg is its own `strategy_native_submit_v1` native_c_api.h:2699 with `PF_NATIVE_OWNER_WAIT_FOR_APPLIED` native_c_api.h:458, and its `first_match` and `scope` are the request's `arm_first_match` / `arm_scope` -native_c_api.h:2178-2179, so a C bracket child arms after its owner's print or over +native_c_api.h:2197-2198, so a C bracket child arms after its owner's print or over the whole book exactly as a C++ one does. The Pine adapter lowers its own queued relative legs exactly this way — a @@ -405,12 +405,12 @@ block is the `PF_NATIVE_SPEC_EXT_RISK` native_c_api.h:739 tail. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:597 | `allowed_open_directions` pineforge.h:541 | `native_risk_limits_strategy.cpp` | The one `strategy.risk.*` rule that is already the kernel's, per ADR-0001's `risk` ruling row. Rejects with `OpeningDirection` native_execution_consumer.cpp:2947. | -| `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:595 | `max_abs_units` pineforge.h:542 | `tests/test_native_run_spec.cpp` | Tests the *resulting* book. TradingView gates the *live* book before the fill, so an entry is refused only once the book already holds the limit — ruled **adapter-policy** in ADR-0001's `max_abs_units` row, with lane N12's measurement (the adapter ends at 4 where the kernel cap ends at 2). | -| `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2314 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:259 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:261 is set. Blocks openings to the end of the run. | -| `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:312 | `risk_max_intraday_loss` native_c_api.h:2317 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | -| `strategy.risk.max_cons_loss_days()` | `NativeRiskLimits::max_consecutive_loss_days` native_run_spec.hpp:313 | `risk_max_consecutive_loss_days` native_c_api.h:2319 | `native_risk_limits_strategy.cpp` | Counts *days* that close with a realized loss; TradingView's streak counts trades. `NativeRiskDay` native_run_spec.hpp:270 chooses the session day or the civil date in the spec timezone. | -| `strategy.risk.max_intraday_filled_orders()` | `NativeRiskLimits::max_fills_per_day` native_run_spec.hpp:314 | `risk_max_fills_per_day` native_c_api.h:2321 | `native_trail_risk_strategy.cpp` | Applied fills counted as they settle, so the fills of one matching point all settle and the limit blocks the next admission. Read the ledger back with `native_risk_state` native_host.hpp:1311. | +| `strategy.risk.allow_entry_in()` | `allowed_open_directions` native_run_spec.hpp:597 | `allowed_open_directions` pineforge.h:503 | `native_risk_limits_strategy.cpp` | The one `strategy.risk.*` rule that is already the kernel's, per ADR-0001's `risk` ruling row. Rejects with `OpeningDirection` native_execution_consumer.cpp:2963. | +| `strategy.risk.max_position_size()` | `max_abs_units` native_run_spec.hpp:595 | `max_abs_units` pineforge.h:504 | `tests/test_native_run_spec.cpp` | Tests the *resulting* book. TradingView gates the *live* book before the fill, so an entry is refused only once the book already holds the limit — ruled **adapter-policy** in ADR-0001's `max_abs_units` row, with lane N12's measurement (the adapter ends at 4 where the kernel cap ends at 2). | +| `strategy.risk.max_drawdown()` | `NativeRiskLimits::max_drawdown` native_run_spec.hpp:311 | `risk_max_drawdown` native_c_api.h:2333 | `native_risk_limits_strategy.cpp` | `NativeLossLimit` native_run_spec.hpp:259 is account currency, or a percent of the running peak when `percent` native_run_spec.hpp:261 is set. Blocks openings to the end of the run. | +| `strategy.risk.max_intraday_loss()` | `NativeRiskLimits::max_intraday_loss` native_run_spec.hpp:312 | `risk_max_intraday_loss` native_c_api.h:2336 | `native_risk_limits_strategy.cpp` | Percent is of the day's opening equity. Blocks to the end of its own day. | +| `strategy.risk.max_cons_loss_days()` | `NativeRiskLimits::max_consecutive_loss_days` native_run_spec.hpp:313 | `risk_max_consecutive_loss_days` native_c_api.h:2338 | `native_risk_limits_strategy.cpp` | Counts *days* that close with a realized loss; TradingView's streak counts trades. `NativeRiskDay` native_run_spec.hpp:270 chooses the session day or the civil date in the spec timezone. | +| `strategy.risk.max_intraday_filled_orders()` | `NativeRiskLimits::max_fills_per_day` native_run_spec.hpp:314 | `risk_max_fills_per_day` native_c_api.h:2340 | `native_trail_risk_strategy.cpp` | Applied fills counted as they settle, so the fills of one matching point all settle and the limit blocks the next admission. Read the ledger back with `native_risk_state` native_host.hpp:1329. | The whole block is **native-only by ruling**: no adapter run declares it, and lane N12 measured why — TradingView's drawdown latch samples at the close and @@ -429,20 +429,20 @@ liquidation level, and the ticket the forced close is booked under. | concept | C++ | C | Runs in | | --- | --- | --- | --- | -| opening requirement per side | `initial_long` native_run_spec.hpp:241 / `initial_short` native_run_spec.hpp:242 | `margin_initial_long` native_c_api.h:2288 | `native_margin_strategy.cpp` | -| maintenance requirement per side | `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 | `margin_maintenance_long` native_c_api.h:2293 | `native_margin_strategy.cpp` | -| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:140 | `margin_sizing` native_c_api.h:2283 | `native_margin_strategy.cpp` | -| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:158 | `margin_check` native_c_api.h:2284 | `native_margin_strategy.cpp` | -| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2351 | `tests/test_native_margin_model.cpp` | -| which base the level is solved from | `NativeLiquidationLevelBase` native_run_spec.hpp:182 | `margin_level_base` native_c_api.h:2352 | `tests/test_native_margin_model.cpp` | -| the forced close's ticket | `liquidation_label` native_run_spec.hpp:251 | `margin_liquidation_label` native_c_api.h:2353 | `native_margin_strategy.cpp` | -| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:960 | — C hosts gate through `on_precommit` native_c_api.h:2573 | `native_margin_strategy.cpp` | -| the host's own requirement | `resolve_margin_requirement` native_host.hpp:976 | `on_margin_requirement` native_c_api.h:2510 | `native_margin_strategy.cpp` | -| which check points run at all | `margin_check_allowed` native_host.hpp:988 | `on_margin_check` native_c_api.h:2519 | `native_margin_strategy.cpp` | +| opening requirement per side | `initial_long` native_run_spec.hpp:241 / `initial_short` native_run_spec.hpp:242 | `margin_initial_long` native_c_api.h:2307 | `native_margin_strategy.cpp` | +| maintenance requirement per side | `maintenance_long` native_run_spec.hpp:243 / `maintenance_short` native_run_spec.hpp:244 | `margin_maintenance_long` native_c_api.h:2312 | `native_margin_strategy.cpp` | +| how much a liquidation closes | `NativeLiquidationSizing` native_run_spec.hpp:140 | `margin_sizing` native_c_api.h:2302 | `native_margin_strategy.cpp` | +| when the requirement is tested | `NativeLiquidationCheck` native_run_spec.hpp:158 | `margin_check` native_c_api.h:2303 | `native_margin_strategy.cpp` | +| which equity it is tested against | `NativeMarginEquityBasis` native_run_spec.hpp:172 | `margin_equity_basis` native_c_api.h:2370 | `tests/test_native_margin_model.cpp` | +| which base the level is solved from | `NativeLiquidationLevelBase` native_run_spec.hpp:182 | `margin_level_base` native_c_api.h:2371 | `tests/test_native_margin_model.cpp` | +| the forced close's ticket | `liquidation_label` native_run_spec.hpp:251 | `margin_liquidation_label` native_c_api.h:2372 | `native_margin_strategy.cpp` | +| the host's own admission verdict | `validate_execution_precommit` native_host.hpp:960 | — C hosts gate through `on_precommit` native_c_api.h:2592 | `native_margin_strategy.cpp` | +| the host's own requirement | `resolve_margin_requirement` native_host.hpp:976 | `on_margin_requirement` native_c_api.h:2529 | `native_margin_strategy.cpp` | +| which check points run at all | `margin_check_allowed` native_host.hpp:988 | `on_margin_check` native_c_api.h:2538 | `native_margin_strategy.cpp` | | a step of the account FX curve, checked though no price moved | `NativeMarginCheckKind::FxRoll` native_host.hpp:553 | `PF_NATIVE_MARGIN_CHECK_FX_ROLL` native_c_api.h:935 | `native_fx_roll_strategy.cpp` | -| a magnified bar's later samples, each checked where the path is (TradingView's bar magnifier) | `NativeMarginCheckKind::IntrabarSample` native_host.hpp:554 | `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE` native_c_api.h:939 | `tests/test_native_margin_intrabar_samples.cpp` | -| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:995 | `on_margin_call_units` native_c_api.h:2526 | `native_margin_strategy.cpp` | -| the liquidation event | `MarginCallEvent` native_order.hpp:1262 delivered to `on_native_margin_call` native_host.hpp:1001 | `on_margin_call` native_c_api.h:2474 | `native_margin_strategy.cpp` | +| a magnified bar's later samples, each checked where the path is (TradingView's bar magnifier) | `NativeMarginCheckKind::IntrabarSample` native_host.hpp:554 | `PF_NATIVE_MARGIN_CHECK_INTRABAR_SAMPLE` native_c_api.h:943 | `tests/test_native_margin_intrabar_samples.cpp` | +| the host's own liquidation size | `resolve_margin_call_units` native_host.hpp:995 | `on_margin_call_units` native_c_api.h:2545 | `native_margin_strategy.cpp` | +| the liquidation event | `MarginCallEvent` native_order.hpp:1262 delivered to `on_native_margin_call` native_host.hpp:1001 | `on_margin_call` native_c_api.h:2493 | `native_margin_strategy.cpp` | The one-scalar `initial_margin_fraction` native_run_spec.hpp:598 predates the model and remains the simple spelling; the two are mutually exclusive. It is @@ -457,20 +457,20 @@ part both ways (`tests/test_adapter_margin_schedule_differential.cpp`, M11). | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| script body at bar close | `on_native_bar` native_host.hpp:893 | `on_bar` native_c_api.h:2464 | `hello_kernel.cpp` / `hello_kernel_c.c` | The one pure virtual. Called once per closed script bar — and, with the default cadence, at every calculation there is: the kernel routes all of them through `on_native_recalculate` native_host.hpp:913, whose default forwards here. | -| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:913 with `NativeCalculationReason` native_host.hpp:825 | `on_recalculate` native_c_api.h:2492 | `native_calc_on_fills_strategy.cpp` | **Installing it REPLACES `on_native_bar` for every calculation** — in C++ you override the forwarding away, and in C a non-NULL `on_recalculate` is what the kernel calls instead of `on_bar`. A host that wants both calls the other itself from here; a C host that installs only `on_recalculate` and still expects `on_bar` silently trades nothing. | -| `barstate.isconfirmed` | `NativeCalculationReason::BarClose` native_host.hpp:826 | `on_recalculate` native_c_api.h:2492 | `native_calc_on_fills_strategy.cpp` | The script bar's own calculation is delivered for every run, whatever the cadence is. | -| `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1756 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | -| bar-open pre-pass | `on_native_bar_open` native_host.hpp:889 | `on_bar_open` native_c_api.h:2463 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | -| the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1073 | `strategy_native_partial_bar_v1` native_c_api.h:2977 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | -| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2466 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5387. | -| raw input before aggregation | `on_native_input` native_host.hpp:875 | `on_input` native_c_api.h:2461 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | -| a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:928 | `on_sub_bar` native_c_api.h:2502 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | -| `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1454 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. | -| `session.ismarket` | `in_session` market_driver.hpp:161 | `in_session` native_c_api.h:1472 | `tests/test_native_session_day_facts.cpp` | The script bar's label is in session on the run's own calendar (`NativeRunSpec::session` / `timezone`); a D/W/M bar holds whole session days, so it is always true there. The calendar applies a session's day mask to the session day's trading date: Sunday 17:00 CT of a CME-style `1700-1600:23456` is Monday's session, in session. Pine's own `session.ismarket` is still generated as the time-of-day predicate `pine_session_ismarket` session_time.hpp:247, whose mask reads each instant's weekday, so on such a masked overnight session the two differ: on six `lab tv` tapes TradingView flags every bar in market, the calendar's fact matches it on every bar, and the predicate misses every Sunday open under a weekday mask (`1700-1600:23456`) and every bar of `0000-2400`, which it cannot parse as a whole day (`tests/test_session_ismarket_tape.cpp`). Unmasked sessions (`1700-1600`, `1700-1700`, `24x7`, `0930-1600`) agree on all three. In C, read the three session bytes only when `session_facts` native_c_api.h:1471 is 1. | -| `session.isfirstbar` | `opens_session_day` market_driver.hpp:162 | `opens_session_day` native_c_api.h:1473 | `tests/test_native_session_day_facts.cpp` | In session, and the bar before it is not, or is on another session day. The session day rolls at the session's first window start and is keyed to its trading date, so an overnight session is one day across local midnight (Tokyo `2230-0500`: its days open at 22:30). "The bar before" is the one the run holds — the batch input, the stream warmup — else the calendar's previous eligible input slot across declared breaks; a run's first bar opens its day. | -| `session.islastbar` | `closes_session_day` market_driver.hpp:163, `closes_session_day_open_ended` market_driver.hpp:164 | `closes_session_day` native_c_api.h:1474 | `tests/test_native_session_day_facts.cpp` | In session, and the bar after it is not, or is on another session day, "the bar after" read the same way. A batch's final bar closes its day (a batch is complete input); `closes_session_day_open_ended` reads the calendar there instead, for a host recomputing a batch whose last input is still forming (C++ only: a C host's live edge is a stream). A stream's bars read the calendar, so a stream cannot see an early close its session string does not declare: the NYSE half day's 12:45 closes the day in a batch (the next bar held is the next day's) but not in a stream (ADR-0001, ruling "Session-day facts at a bar with nothing held after it"). | -| the price at the current point | `current_execution_point` native_host.hpp:1087 | `price` native_c_api.h:1533 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1099 is an observation, never an apply token; `execute_current` native_host.hpp:1104 is the apply. | +| script body at bar close | `on_native_bar` native_host.hpp:893 | `on_bar` native_c_api.h:2483 | `hello_kernel.cpp` / `hello_kernel_c.c` | The one pure virtual. Called once per closed script bar — and, with the default cadence, at every calculation there is: the kernel routes all of them through `on_native_recalculate` native_host.hpp:913, whose default forwards here. | +| every calculation, tagged with its reason | `on_native_recalculate` native_host.hpp:913 with `NativeCalculationReason` native_host.hpp:825 | `on_recalculate` native_c_api.h:2511 | `native_calc_on_fills_strategy.cpp` | **Installing it REPLACES `on_native_bar` for every calculation** — in C++ you override the forwarding away, and in C a non-NULL `on_recalculate` is what the kernel calls instead of `on_bar`. A host that wants both calls the other itself from here; a C host that installs only `on_recalculate` and still expects `on_bar` silently trades nothing. | +| `barstate.isconfirmed` | `NativeCalculationReason::BarClose` native_host.hpp:826 | `on_recalculate` native_c_api.h:2511 | `native_calc_on_fills_strategy.cpp` | The script bar's own calculation is delivered for every run, whatever the cadence is. | +| `barstate.isrealtime` | `NativeStateView::phase` native_host.hpp:285 with `NativeRunPhase` native_host.hpp:41 | `pf_native_state_v1` native_c_api.h:1767 | `native_market_strategy.cpp` | `Batch`, `Warmup`, `Realtime`. | +| bar-open pre-pass | `on_native_bar_open` native_host.hpp:889 | `on_bar_open` native_c_api.h:2482 | `native_calc_on_fills_strategy.cpp` | Receives the whole script bar by default; `NativeOpenBarView::OpenOnly` native_run_spec.hpp:109 masks that lookahead (H = L = C = open, volume 0) for a host that must decide at the open with open-only information. | +| the bar so far, mid-bar | `current_partial_bar` native_host.hpp:1073 | `strategy_native_partial_bar_v1` native_c_api.h:2996 | `native_calc_on_fills_strategy.cpp` | Never runs ahead of the cursor: at a fill inside a leg it holds the points already reached and never the later high. | +| a fill, as it happens | `on_native_applied` native_host.hpp:938 | `on_applied` native_c_api.h:2485 | `native_calc_on_fills_strategy.cpp` | Fires mid-path after each fill; a request born there is eligible on the unconsumed rest of the bar native_execution_consumer.cpp:5399. | +| raw input before aggregation | `on_native_input` native_host.hpp:875 | `on_input` native_c_api.h:2480 | `native_auxiliary_feed_strategy.cpp` | Every accepted confirmed input bar, before aggregation or matching. | +| a lower-timeframe sub-bar | `on_native_sub_bar` native_host.hpp:928 | `on_sub_bar` native_c_api.h:2521 | `tests/test_native_calc_timing.cpp` | `NativeCalculationReason::SubBar` is reserved and never delivered to the recalculate hook — a sub-bar has its own callback. | +| `time`, `bar_index`, the bar's calendar interval, session facts | `NativeDecisionContext` market_driver.hpp:120 with `NativeCoordinate` market_driver.hpp:72 | `pf_native_decision_v1` native_c_api.h:1465 | `hello_kernel.cpp` | `NativeCoordinate::interval_index` is the sequential SCRIPT-bar index on every run, including aggregation; `input_interval_index` names the input slot for a host that needs input cadence. Both are copied onto the callback stack, and mutating them changes nothing. In C++ `script_interval` market_driver.hpp:124 and `input_interval` market_driver.hpp:123 add the calendar facts (eligible open, last traded close, next period and next input opens); the C decision carries the script interval's five instants and the session days of its open and of the next input in its tail, presented to a callback table at `PF_NATIVE_CALLBACKS_V1_POLICY_SIZE` or later, and no input interval. The C `on_input` callback receives the input index separately. The session-day facts are the three rows below. | +| `session.ismarket` | `in_session` market_driver.hpp:161 | `in_session` native_c_api.h:1483 | `tests/test_native_session_day_facts.cpp` | The script bar's label is in session on the run's own calendar (`NativeRunSpec::session` / `timezone`); a D/W/M bar holds whole session days, so it is always true there. The calendar applies a session's day mask to the session day's trading date: Sunday 17:00 CT of a CME-style `1700-1600:23456` is Monday's session, in session. Pine's own `session.ismarket` is still generated as the time-of-day predicate `pine_session_ismarket` session_time.hpp:247, whose mask reads each instant's weekday, so on such a masked overnight session the two differ: on six `lab tv` tapes TradingView flags every bar in market, the calendar's fact matches it on every bar, and the predicate misses every Sunday open under a weekday mask (`1700-1600:23456`) and every bar of `0000-2400`, which it cannot parse as a whole day (`tests/test_session_ismarket_tape.cpp`). Unmasked sessions (`1700-1600`, `1700-1700`, `24x7`, `0930-1600`) agree on all three. In C, read the three session bytes only when `session_facts` native_c_api.h:1482 is 1. | +| `session.isfirstbar` | `opens_session_day` market_driver.hpp:162 | `opens_session_day` native_c_api.h:1484 | `tests/test_native_session_day_facts.cpp` | In session, and the bar before it is not, or is on another session day. The session day rolls at the session's first window start and is keyed to its trading date, so an overnight session is one day across local midnight (Tokyo `2230-0500`: its days open at 22:30). "The bar before" is the one the run holds — the batch input, the stream warmup — else the calendar's previous eligible input slot across declared breaks; a run's first bar opens its day. | +| `session.islastbar` | `closes_session_day` market_driver.hpp:163, `closes_session_day_open_ended` market_driver.hpp:164 | `closes_session_day` native_c_api.h:1485 | `tests/test_native_session_day_facts.cpp` | In session, and the bar after it is not, or is on another session day, "the bar after" read the same way. A batch's final bar closes its day (a batch is complete input); `closes_session_day_open_ended` reads the calendar there instead, for a host recomputing a batch whose last input is still forming (C++ only: a C host's live edge is a stream). A stream's bars read the calendar, so a stream cannot see an early close its session string does not declare: the NYSE half day's 12:45 closes the day in a batch (the next bar held is the next day's) but not in a stream (ADR-0001, ruling "Session-day facts at a bar with nothing held after it"). | +| the price at the current point | `current_execution_point` native_host.hpp:1087 | `price` native_c_api.h:1544 | `native_selected_strategy.cpp` | The readiness preview `inspect_current_execution` native_host.hpp:1099 is an observation, never an apply token; `execute_current` native_host.hpp:1104 is the apply. | ## Position, account and report {#pine_to_native_map_report} @@ -488,7 +488,7 @@ migration decision: reachable from inside your host exactly as they are from inside a generated Pine strategy, and `native_open_lots_strategy.cpp` reads every one of them. 3. **The report, after the run** — `fill_report` engine.hpp:1843 into a - `pf_report_t` pineforge.h:469, which is what a C host reads. + `pf_report_t` pineforge.h:431, which is what a C host reads. One rule governs the whole table, in two scopes. The equity **series** is recorded at the kernel's own report point, which exists **only** under @@ -508,7 +508,7 @@ For event readback, C++ `NativeRunSpec::event_retention` defaults to `Window`: the kernel retires events at script-bar boundaries, so a host that inspects `native_events(0)` only after a run must request `Full`. A host that consumes events during the run can keep `Window` and acknowledge its cursor with -`native_acknowledge_events` native_host.hpp:1332. A C caller that omits the +`native_acknowledge_events` native_host.hpp:1350. A C caller that omits the extension's retention word keeps `Full` for ABI compatibility. Pine's `strategy.openprofit` is the **gross** mark-to-market move of open @@ -528,39 +528,39 @@ range-end reporting is off. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.account_currency` | `currency` native_run_spec.hpp:571 | `currency` pineforge.h:537 | `hello_kernel.cpp` / `native_fx_roll_strategy.cpp` | Declared, never inferred. `account_fx` native_run_spec.hpp:582 is the one positive scalar that converts quote to account; a timestamped curve is `configure_native_fx_curve` native_host.hpp:1202. The FX-roll example runs a JPY account on a USD-quoted stock through such a curve, and a step of it is a margin check point of its own. | -| `strategy.initial_capital` | `initial_capital` native_run_spec.hpp:580 | `initial_capital` pineforge.h:539 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | -| `strategy.equity` | `current_equity() + open_profit(mark)` engine.hpp:970-1024; or `native_marked_equity(mark)` native_host.hpp:1301 plus open entry commissions | `strategy_native_marked_equity_v1` native_c_api.h:3017 plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine marks at the current `close`. The native marked call takes the mark explicitly and returns balance plus open lots' **net** `unrealized_pnl` native_host.hpp:341; add their remaining entry commissions to reproduce Pine's gross-open-profit equity. | -| `strategy.netprofit` | `net_profit` engine.hpp:967 | `net_profit` pineforge.h:412 | `native_open_lots_strategy.cpp` | Realized only. | -| `strategy.netprofit_percent` | derive: `net_profit` engine.hpp:967 over `initial_capital` native_run_spec.hpp:580 | `pf_metrics_t` pineforge.h:358 | `native_sized_report_strategy.cpp` | The kernel keeps the facts, not the ratio. | -| `strategy.grossprofit` | `gross_profit` engine.hpp:968 | `pf_trade_stats_t` pineforge.h:291 | `native_open_lots_strategy.cpp` | | -| `strategy.grossprofit_percent` | `grossprofit_percent` engine.hpp:975 | `pf_metrics_t` pineforge.h:358 | `native_sized_report_strategy.cpp` | Of initial capital. | -| `strategy.grossloss` | `gross_loss` engine.hpp:969 | `pf_trade_stats_t::gross_loss` pineforge.h:246 | `native_open_lots_strategy.cpp` | For booked closed trades the protected C++ accessor is signed (negative for losses), while the C report stores a positive loss magnitude. With range-end reporting off, `-C.gross_loss == gross_loss()` and `netprofit == grossprofit - C.gross_loss`. With it on, C trade statistics include the report-only range-end rows; filter `open_at_end == 0` before comparing to Pine's closed-trade statistics. | -| `strategy.grossloss_percent` | `grossloss_percent` engine.hpp:978 | `pf_metrics_t` pineforge.h:358 | `native_sized_report_strategy.cpp` | | -| `strategy.openprofit` | `open_profit` engine.hpp:1025 | `strategy_native_marked_equity_v1` native_c_api.h:3024 less realized balance, plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine's open profit is gross of remaining entry fees. The native marked equity and open-lot `unrealized_pnl` are net of them; use the same explicit mark for every term. | +| `strategy.account_currency` | `currency` native_run_spec.hpp:571 | `currency` pineforge.h:499 | `hello_kernel.cpp` / `native_fx_roll_strategy.cpp` | Declared, never inferred. `account_fx` native_run_spec.hpp:582 is the one positive scalar that converts quote to account; a timestamped curve is `configure_native_fx_curve` native_host.hpp:1202. The FX-roll example runs a JPY account on a USD-quoted stock through such a curve, and a step of it is a margin check point of its own. | +| `strategy.initial_capital` | `initial_capital` native_run_spec.hpp:580 | `initial_capital` pineforge.h:501 | `hello_kernel.cpp` | The value you declared, unchanged by the run. | +| `strategy.equity` | `current_equity() + open_profit(mark)` engine.hpp:970-1024; or `native_marked_equity(mark)` native_host.hpp:1301 plus open entry commissions | `strategy_native_marked_equity_v1` native_c_api.h:3036 plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine marks at the current `close`. The native marked call takes the mark explicitly and returns balance plus open lots' **net** `unrealized_pnl` native_host.hpp:341; add their remaining entry commissions to reproduce Pine's gross-open-profit equity. | +| `strategy.netprofit` | `net_profit` engine.hpp:967 | `net_profit` pineforge.h:374 | `native_open_lots_strategy.cpp` | Realized only. | +| `strategy.netprofit_percent` | derive: `net_profit` engine.hpp:967 over `initial_capital` native_run_spec.hpp:580 | `pf_metrics_t` pineforge.h:320 | `native_sized_report_strategy.cpp` | The kernel keeps the facts, not the ratio. | +| `strategy.grossprofit` | `gross_profit` engine.hpp:968 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | +| `strategy.grossprofit_percent` | `grossprofit_percent` engine.hpp:975 | `pf_metrics_t` pineforge.h:320 | `native_sized_report_strategy.cpp` | Of initial capital. | +| `strategy.grossloss` | `gross_loss` engine.hpp:969 | `pf_trade_stats_t::gross_loss` pineforge.h:224 | `native_open_lots_strategy.cpp` | For booked closed trades the protected C++ accessor is signed (negative for losses), while the C report stores a positive loss magnitude. With range-end reporting off, `-C.gross_loss == gross_loss()` and `netprofit == grossprofit - C.gross_loss`. With it on, C trade statistics include the report-only range-end rows; filter `open_at_end == 0` before comparing to Pine's closed-trade statistics. | +| `strategy.grossloss_percent` | `grossloss_percent` engine.hpp:978 | `pf_metrics_t` pineforge.h:320 | `native_sized_report_strategy.cpp` | | +| `strategy.openprofit` | `open_profit` engine.hpp:1025 | `strategy_native_marked_equity_v1` native_c_api.h:3036 less realized balance, plus open lots' `entry_commission` | `native_open_lots_strategy.cpp` | Pine's open profit is gross of remaining entry fees. The native marked equity and open-lot `unrealized_pnl` are net of them; use the same explicit mark for every term. | | `strategy.openprofit_percent` | derive from `open_profit` engine.hpp:1025 | — | `native_open_lots_strategy.cpp` | Pine's denominator is the realized equity. | -| `strategy.max_drawdown` | `max_drawdown_` engine.hpp:581 | `max_equity_drawdown` pineforge.h:296 | `native_open_lots_strategy.cpp` | Folded under **every** report policy — see the rule above. The scalar is the run's own; the `pf_equity_stats_t` figure derived from the recorded curve still needs `KernelRecorded` native_run_spec.hpp:63. | -| `strategy.max_drawdown_percent` | `max_drawdown_percent` engine.hpp:1507 | `max_equity_drawdown_pct` pineforge.h:296 | `native_sized_report_strategy.cpp` | Same rule. | -| `strategy.max_runup` | `max_runup_` engine.hpp:582 | `max_equity_runup` pineforge.h:300 | `native_open_lots_strategy.cpp` | Same rule. | -| `strategy.max_runup_percent` | `max_runup_percent` engine.hpp:972 | `max_equity_runup_pct` pineforge.h:300 | `native_sized_report_strategy.cpp` | Same rule. | -| `strategy.avg_trade` | `avg_trade` engine.hpp:981 | `pf_trade_stats_t` pineforge.h:291 | `native_open_lots_strategy.cpp` | | -| `strategy.avg_trade_percent` | `avg_trade_percent` engine.hpp:985 | `pf_trade_stats_t` pineforge.h:291 | `native_sized_report_strategy.cpp` | | -| `strategy.avg_winning_trade` | `avg_winning_trade` engine.hpp:992 | `pf_trade_stats_t` pineforge.h:291 | `native_sized_report_strategy.cpp` | | -| `strategy.avg_winning_trade_percent` | `avg_winning_trade_percent` engine.hpp:998 | `pf_trade_stats_t` pineforge.h:291 | `native_sized_report_strategy.cpp` | | -| `strategy.avg_losing_trade` | `avg_losing_trade` engine.hpp:995 | `pf_trade_stats_t` pineforge.h:291 | `native_sized_report_strategy.cpp` | | -| `strategy.avg_losing_trade_percent` | `avg_losing_trade_percent` engine.hpp:1007 | `pf_trade_stats_t` pineforge.h:291 | `native_sized_report_strategy.cpp` | | -| `strategy.wintrades` | `count_wintrades` engine.hpp:1042 | `pf_trade_stats_t` pineforge.h:291 | `native_open_lots_strategy.cpp` | | -| `strategy.losstrades` | `count_losstrades` engine.hpp:1043 | `pf_trade_stats_t` pineforge.h:291 | `native_open_lots_strategy.cpp` | | -| `strategy.eventrades` | `eventrades` engine.hpp:1841 | `pf_trade_stats_t` pineforge.h:291 | `native_open_lots_strategy.cpp` | Public, not protected: a zero-profit closed row. Win + loss + even is the closed-row count, which the example asserts. | +| `strategy.max_drawdown` | `max_drawdown_` engine.hpp:581 | `max_equity_drawdown` pineforge.h:274 | `native_open_lots_strategy.cpp` | Folded under **every** report policy — see the rule above. The scalar is the run's own; the `pf_equity_stats_t` figure derived from the recorded curve still needs `KernelRecorded` native_run_spec.hpp:63. | +| `strategy.max_drawdown_percent` | `max_drawdown_percent` engine.hpp:1507 | `max_equity_drawdown_pct` pineforge.h:274 | `native_sized_report_strategy.cpp` | Same rule. | +| `strategy.max_runup` | `max_runup_` engine.hpp:582 | `max_equity_runup` pineforge.h:278 | `native_open_lots_strategy.cpp` | Same rule. | +| `strategy.max_runup_percent` | `max_runup_percent` engine.hpp:972 | `max_equity_runup_pct` pineforge.h:278 | `native_sized_report_strategy.cpp` | Same rule. | +| `strategy.avg_trade` | `avg_trade` engine.hpp:981 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | +| `strategy.avg_trade_percent` | `avg_trade_percent` engine.hpp:985 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | +| `strategy.avg_winning_trade` | `avg_winning_trade` engine.hpp:992 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | +| `strategy.avg_winning_trade_percent` | `avg_winning_trade_percent` engine.hpp:998 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | +| `strategy.avg_losing_trade` | `avg_losing_trade` engine.hpp:995 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | +| `strategy.avg_losing_trade_percent` | `avg_losing_trade_percent` engine.hpp:1007 | `pf_trade_stats_t` pineforge.h:269 | `native_sized_report_strategy.cpp` | | +| `strategy.wintrades` | `count_wintrades` engine.hpp:1042 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | +| `strategy.losstrades` | `count_losstrades` engine.hpp:1043 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | | +| `strategy.eventrades` | `eventrades` engine.hpp:1841 | `pf_trade_stats_t` pineforge.h:269 | `native_open_lots_strategy.cpp` | Public, not protected: a zero-profit closed row. Win + loss + even is the closed-row count, which the example asserts. | | `strategy.max_contracts_held_all` | `max_contracts_held_all` engine.hpp:1836 | — | `native_open_lots_strategy.cpp` | `pf_trade_stats_t` has no max-contracts-held field. A C host must derive the peak from position/open-lot observations; there is no direct C report counterpart. | | `strategy.max_contracts_held_long` | `max_contracts_held_long` engine.hpp:1837 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the long-side peak from position/open-lot observations. | | `strategy.max_contracts_held_short` | `max_contracts_held_short` engine.hpp:1838 | — | `tests/test_native_report_truth.cpp` | No direct C report counterpart; derive the short-side peak from position/open-lot observations. | -| `strategy.position_size` | `NativePhysicalPosition::signed_units` native_host.hpp:297 | `strategy_native_position_v1` native_c_api.h:2793 | `hello_kernel.cpp` | Signed: no separate direction field. | -| `strategy.position_avg_price` | `NativePhysicalPosition::average_price` native_host.hpp:298 | `strategy_native_position_v1` native_c_api.h:2793 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | -| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1297 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2834 | `native_open_lots_strategy.cpp` | Pine reports the newest entry's id; the native book lets you read any lot's. | -| `strategy.margin_liquidation_price` | `native_liquidation_price` native_host.hpp:1320 | `strategy_native_liquidation_price_v1` native_c_api.h:3025 | `native_margin_strategy.cpp` | Solved from the declared maintenance requirement, so it answers `nullopt` when the model declares none or the slope is degenerate (a long at full maintenance has no finite level). | -| `strategy.convert_to_account()` | none — the kernel converts with the FX you declare | `strategy_configure_native_fx_curve_v1` pineforge.h:584 | `native_fx_roll_strategy.cpp` / `tests/test_native_fx_curve.cpp` | Pine's converter needs TradingView's own FX series. Natively, `account_fx` native_run_spec.hpp:582 is one scalar and `configure_native_fx_curve` native_host.hpp:1202 stages an immutable timestamped curve; codegen emits the Pine call as the identity, because no fixed table reproduces TradingView's series. | -| `strategy.convert_to_symbol()` | none — same reason, the other direction | `strategy_configure_native_fx_curve_v1` pineforge.h:584 | `tests/test_native_fx_curve.cpp` | Same ruling. | +| `strategy.position_size` | `NativePhysicalPosition::signed_units` native_host.hpp:297 | `strategy_native_position_v1` native_c_api.h:2812 | `hello_kernel.cpp` | Signed: no separate direction field. | +| `strategy.position_avg_price` | `NativePhysicalPosition::average_price` native_host.hpp:298 | `strategy_native_position_v1` native_c_api.h:2812 | `native_selected_strategy.cpp` | Volume-weighted over the open lots. | +| `strategy.position_entry_name` | `native_open_lots` native_host.hpp:1297 then the last row's `entry_label` native_host.hpp:333 | `strategy_native_open_lot_get_v1` native_c_api.h:2853 | `native_open_lots_strategy.cpp` | Pine reports the newest entry's id; the native book lets you read any lot's. | +| `strategy.margin_liquidation_price` | `native_liquidation_price` native_host.hpp:1320 | `strategy_native_liquidation_price_v1` native_c_api.h:3044 | `native_margin_strategy.cpp` | Solved from the declared maintenance requirement, so it answers `nullopt` when the model declares none or the slope is degenerate (a long at full maintenance has no finite level). | +| `strategy.convert_to_account()` | none — the kernel converts with the FX you declare | `strategy_configure_native_fx_curve_v1` pineforge.h:546 | `native_fx_roll_strategy.cpp` / `tests/test_native_fx_curve.cpp` | Pine's converter needs TradingView's own FX series. Natively, `account_fx` native_run_spec.hpp:582 is one scalar and `configure_native_fx_curve` native_host.hpp:1202 stages an immutable timestamped curve; codegen emits the Pine call as the identity, because no fixed table reproduces TradingView's series. | +| `strategy.convert_to_symbol()` | none — same reason, the other direction | `strategy_configure_native_fx_curve_v1` pineforge.h:546 | `tests/test_native_fx_curve.cpp` | Same ruling. | @anchor pine_to_native_map_trades ## Open trades {#pine_to_native_map_open_trades} @@ -568,9 +568,9 @@ range-end reporting is off. `native_open_lots(mark)` native_host.hpp:1297 answers one `NativeOpenLot` native_host.hpp:328 per open physical lot, in book order (oldest first, `NativePhysicalPosition::lot_count` native_host.hpp:299 rows). The C twin is -`strategy_native_open_lot_count_v1` native_c_api.h:2826 then -`strategy_native_open_lot_get_v1` native_c_api.h:2834 into a -`pf_native_open_lot_v1` native_c_api.h:1732, whose two strings borrow the +`strategy_native_open_lot_count_v1` native_c_api.h:2845 then +`strategy_native_open_lot_get_v1` native_c_api.h:2853 into a +`pf_native_open_lot_v1` native_c_api.h:1743, whose two strings borrow the snapshot until the next count call. `mark` is the price the three marked fields are computed at; Pine's builtins mark at the current `close`. Reading it moves no fill, no hash and no row. `native_open_lots_strategy.cpp` is the runnable @@ -583,19 +583,19 @@ native_order.hpp:445 names. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.opentrades` | `native_open_lots` native_host.hpp:1297 | `strategy_native_open_lot_count_v1` native_c_api.h:2826 | `native_open_lots_strategy.cpp` | The row count, which equals `lot_count` native_host.hpp:299. | -| `strategy.opentrades.entry_id()` | `entry_label` native_host.hpp:333 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The opening request's `label` native_order.hpp:485. | -| `strategy.opentrades.entry_comment()` | `entry_comment` native_host.hpp:334 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The opening request's `comment` native_order.hpp:486. | -| `strategy.opentrades.entry_bar_index()` | `entry_bar_index` native_host.hpp:336 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Script-bar index of the opening fill. | -| `strategy.opentrades.entry_time()` | `entry_time_ms` native_host.hpp:335 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Effective time of the opening fill, Unix ms. | -| `strategy.opentrades.entry_price()` | `entry_price` native_host.hpp:337 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The booked price: slippage and the price grid are already applied. | -| `strategy.opentrades.size()` | `signed_units` native_host.hpp:338 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Pine's is unsigned with a separate direction; here `> 0` is long, `< 0` short, and `side` native_host.hpp:332 says the same. | -| `strategy.opentrades.commission()` | `entry_commission` native_host.hpp:339 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The entry fee still on the lot, account currency. A partial close takes its share onto the closed row: the example's FIFO reduce leaves 1.5 units carrying 1.5 of a 2.00 ticket. | -| `strategy.opentrades.profit()` | `unrealized_pnl` native_host.hpp:341 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | The move from `entry_price` to `mark` in account currency, **net of `entry_commission`** — the lot's own term of the marked equity. Gross is `unrealized_pnl + entry_commission`. | +| `strategy.opentrades` | `native_open_lots` native_host.hpp:1297 | `strategy_native_open_lot_count_v1` native_c_api.h:2845 | `native_open_lots_strategy.cpp` | The row count, which equals `lot_count` native_host.hpp:299. | +| `strategy.opentrades.entry_id()` | `entry_label` native_host.hpp:333 | `pf_native_open_lot_v1` native_c_api.h:1743 | `native_open_lots_strategy.cpp` | The opening request's `label` native_order.hpp:485. | +| `strategy.opentrades.entry_comment()` | `entry_comment` native_host.hpp:334 | `pf_native_open_lot_v1` native_c_api.h:1743 | `native_open_lots_strategy.cpp` | The opening request's `comment` native_order.hpp:486. | +| `strategy.opentrades.entry_bar_index()` | `entry_bar_index` native_host.hpp:336 | `pf_native_open_lot_v1` native_c_api.h:1743 | `native_open_lots_strategy.cpp` | Script-bar index of the opening fill. | +| `strategy.opentrades.entry_time()` | `entry_time_ms` native_host.hpp:335 | `pf_native_open_lot_v1` native_c_api.h:1743 | `native_open_lots_strategy.cpp` | Effective time of the opening fill, Unix ms. | +| `strategy.opentrades.entry_price()` | `entry_price` native_host.hpp:337 | `pf_native_open_lot_v1` native_c_api.h:1743 | `native_open_lots_strategy.cpp` | The booked price: slippage and the price grid are already applied. | +| `strategy.opentrades.size()` | `signed_units` native_host.hpp:338 | `pf_native_open_lot_v1` native_c_api.h:1743 | `native_open_lots_strategy.cpp` | Pine's is unsigned with a separate direction; here `> 0` is long, `< 0` short, and `side` native_host.hpp:332 says the same. | +| `strategy.opentrades.commission()` | `entry_commission` native_host.hpp:339 | `pf_native_open_lot_v1` native_c_api.h:1743 | `native_open_lots_strategy.cpp` | The entry fee still on the lot, account currency. A partial close takes its share onto the closed row: the example's FIFO reduce leaves 1.5 units carrying 1.5 of a 2.00 ticket. | +| `strategy.opentrades.profit()` | `unrealized_pnl` native_host.hpp:341 | `pf_native_open_lot_v1` native_c_api.h:1743 | `native_open_lots_strategy.cpp` | The move from `entry_price` to `mark` in account currency, **net of `entry_commission`** — the lot's own term of the marked equity. Gross is `unrealized_pnl + entry_commission`. | | `strategy.opentrades.profit_percent()` | derive: `unrealized_pnl` native_host.hpp:341 over entry cost | — | `native_open_lots_strategy.cpp` | Entry cost is `entry_price * abs(signed_units) * point_value * account_fx`. | -| `strategy.opentrades.max_runup()` | `favorable_excursion` native_host.hpp:342 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Largest move for the lot the kernel has sampled along the delivered path, account currency, gross of fees, with `mark` folded in. | +| `strategy.opentrades.max_runup()` | `favorable_excursion` native_host.hpp:342 | `pf_native_open_lot_v1` native_c_api.h:1743 | `native_open_lots_strategy.cpp` | Largest move for the lot the kernel has sampled along the delivered path, account currency, gross of fees, with `mark` folded in. | | `strategy.opentrades.max_runup_percent()` | derive from `favorable_excursion` native_host.hpp:342 | — | `native_open_lots_strategy.cpp` | The snapshot carries the facts, not the ratio. | -| `strategy.opentrades.max_drawdown()` | `adverse_excursion` native_host.hpp:343 | `pf_native_open_lot_v1` native_c_api.h:1732 | `native_open_lots_strategy.cpp` | Largest move against the lot, likewise; both excursions are magnitudes `>= 0`. | +| `strategy.opentrades.max_drawdown()` | `adverse_excursion` native_host.hpp:343 | `pf_native_open_lot_v1` native_c_api.h:1743 | `native_open_lots_strategy.cpp` | Largest move against the lot, likewise; both excursions are magnitudes `>= 0`. | | `strategy.opentrades.max_drawdown_percent()` | derive from `adverse_excursion` native_host.hpp:343 | — | `native_open_lots_strategy.cpp` | | | `strategy.opentrades.capital_held` | `open_trades_capital_held` engine.hpp:1016 | — | `native_open_lots_strategy.cpp` | `pf_trade_stats_t` carries no capital-held field. Derive it from the open lots and declared price/value/FX terms, or read the protected C++ accessor. | @@ -612,39 +612,39 @@ the `Trade` engine.hpp:172 rows this run booked, in booking order; `report_trade_count` engine.hpp:1815 and `get_report_trade` engine.hpp:1818 span the same rows followed by the range-end rows `report_open_position_at_end` native_run_spec.hpp:615 adds. In C the report's -`pf_report_t::trades` pineforge.h:409 carries the numeric fields, and the +`pf_report_t::trades` pineforge.h:371 carries the numeric fields, and the strings and the cause come from `strategy_closed_trade_entry_id` -pineforge.h:1192, `strategy_closed_trade_exit_id` pineforge.h:1205, -`strategy_closed_trade_exit_comment` pineforge.h:1207 and -`strategy_closed_trade_close_cause` pineforge.h:1274, which index exactly the +pineforge.h:1157, `strategy_closed_trade_exit_id` pineforge.h:1170, +`strategy_closed_trade_exit_comment` pineforge.h:1172 and +`strategy_closed_trade_close_cause` pineforge.h:1239, which index exactly the rows of that array. | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `strategy.closedtrades` | `closed_trade_count` engine.hpp:1807 | `total_trades` pineforge.h:406 | `native_open_lots_strategy.cpp` | `closed_trade_count` counts booked closed rows. The C report's `total_trades` also includes range-end rows when `report_open_position_at_end` is enabled; filter `pf_trade_t::open_at_end == 0` to match Pine `strategy.closedtrades`. | +| `strategy.closedtrades` | `closed_trade_count` engine.hpp:1807 | `total_trades` pineforge.h:368 | `native_open_lots_strategy.cpp` | `closed_trade_count` counts booked closed rows. The C report's `total_trades` also includes range-end rows when `report_open_position_at_end` is enabled; filter `pf_trade_t::open_at_end == 0` to match Pine `strategy.closedtrades`. | | `strategy.closedtrades.first_index` | none — the kernel keeps every row | — | `native_open_lots_strategy.cpp` | TradingView drops old rows past a 9000-trade cap and advances `first_index` when it does. The kernel caps nothing, so the first index is always 0 and codegen emits the literal. | -| `strategy.closedtrades.entry_id()` | `entry_id` engine.hpp:183 | `strategy_closed_trade_entry_id` pineforge.h:1192 | `native_open_lots_strategy.cpp` | The same string the lot carried as `entry_label` native_host.hpp:333. | -| `strategy.closedtrades.entry_comment()` | `entry_comment` engine.hpp:184 | — | `native_open_lots_strategy.cpp` | No C accessor: `strategy_closed_trade_entry_id` pineforge.h:1192 answers the id, not the comment, and `pf_trade_t` carries no strings. A C host that needs it reads the open lot's `entry_comment` (`pf_native_open_lot_v1` native_c_api.h:1732) before the close. | -| `strategy.closedtrades.entry_bar_index()` | `entry_bar_index` engine.hpp:181 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | Copied from the lot. | -| `strategy.closedtrades.entry_time()` | `entry_time` engine.hpp:173 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | Unix ms. | -| `strategy.closedtrades.entry_price()` | `entry_price` engine.hpp:175 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.exit_id()` | `exit_id` engine.hpp:186 | `strategy_closed_trade_exit_id` pineforge.h:1205 | `native_margin_strategy.cpp` | For a kernel liquidation this is the model's own `liquidation_label` native_run_spec.hpp:251, which is how a reporting layer classifies the row. | -| `strategy.closedtrades.exit_comment()` | `exit_comment` engine.hpp:185 | `strategy_closed_trade_exit_comment` pineforge.h:1207 | `native_margin_strategy.cpp` | | -| `strategy.closedtrades.exit_bar_index()` | `exit_bar_index` engine.hpp:182 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.exit_time()` | `exit_time` engine.hpp:174 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.exit_price()` | `exit_price` engine.hpp:176 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | | -| `strategy.closedtrades.size()` | `qty` engine.hpp:177 with `is_long` engine.hpp:180 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | Unsigned, with the direction beside it. A partial close books a row for the closed slice only. | -| `strategy.closedtrades.profit()` | `pnl` engine.hpp:178 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | Net of `commission` engine.hpp:201. `pnl + commission` is the gross move, which equals the lot's `unrealized_pnl + entry_commission` at a mark equal to the exit price, scaled to the closed slice. | -| `strategy.closedtrades.profit_percent()` | `pnl_pct` engine.hpp:179 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | Net return on entry cost. | -| `strategy.closedtrades.commission()` | `commission` engine.hpp:201 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | Entry share plus exit share. One cash-per-execution ticket is split by units over every slice of that execution. | -| `strategy.closedtrades.max_runup()` | `max_runup` engine.hpp:199 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | TradingView's net-open-profit basis: the lot's gross favorable excursion less the entry share, floored at zero. The open-lot field is the gross value it is derived from. | +| `strategy.closedtrades.entry_id()` | `entry_id` engine.hpp:183 | `strategy_closed_trade_entry_id` pineforge.h:1157 | `native_open_lots_strategy.cpp` | The same string the lot carried as `entry_label` native_host.hpp:333. | +| `strategy.closedtrades.entry_comment()` | `entry_comment` engine.hpp:184 | — | `native_open_lots_strategy.cpp` | No C accessor: `strategy_closed_trade_entry_id` pineforge.h:1157 answers the id, not the comment, and `pf_trade_t` carries no strings. A C host that needs it reads the open lot's `entry_comment` (`pf_native_open_lot_v1` native_c_api.h:1743) before the close. | +| `strategy.closedtrades.entry_bar_index()` | `entry_bar_index` engine.hpp:181 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Copied from the lot. | +| `strategy.closedtrades.entry_time()` | `entry_time` engine.hpp:173 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Unix ms. | +| `strategy.closedtrades.entry_price()` | `entry_price` engine.hpp:175 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | | +| `strategy.closedtrades.exit_id()` | `exit_id` engine.hpp:186 | `strategy_closed_trade_exit_id` pineforge.h:1170 | `native_margin_strategy.cpp` | For a kernel liquidation this is the model's own `liquidation_label` native_run_spec.hpp:251, which is how a reporting layer classifies the row. | +| `strategy.closedtrades.exit_comment()` | `exit_comment` engine.hpp:185 | `strategy_closed_trade_exit_comment` pineforge.h:1172 | `native_margin_strategy.cpp` | | +| `strategy.closedtrades.exit_bar_index()` | `exit_bar_index` engine.hpp:182 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | | +| `strategy.closedtrades.exit_time()` | `exit_time` engine.hpp:174 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | | +| `strategy.closedtrades.exit_price()` | `exit_price` engine.hpp:176 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | | +| `strategy.closedtrades.size()` | `qty` engine.hpp:177 with `is_long` engine.hpp:180 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Unsigned, with the direction beside it. A partial close books a row for the closed slice only. | +| `strategy.closedtrades.profit()` | `pnl` engine.hpp:178 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Net of `commission` engine.hpp:201. `pnl + commission` is the gross move, which equals the lot's `unrealized_pnl + entry_commission` at a mark equal to the exit price, scaled to the closed slice. | +| `strategy.closedtrades.profit_percent()` | `pnl_pct` engine.hpp:179 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Net return on entry cost. | +| `strategy.closedtrades.commission()` | `commission` engine.hpp:201 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | Entry share plus exit share. One cash-per-execution ticket is split by units over every slice of that execution. | +| `strategy.closedtrades.max_runup()` | `max_runup` engine.hpp:199 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | TradingView's net-open-profit basis: the lot's gross favorable excursion less the entry share, floored at zero. The open-lot field is the gross value it is derived from. | | `strategy.closedtrades.max_runup_percent()` | derive from `max_runup` engine.hpp:199 | — | `native_open_lots_strategy.cpp` | Over `entry_price * qty * point_value`. | -| `strategy.closedtrades.max_drawdown()` | `max_drawdown` engine.hpp:200 | `pf_trade_t` pineforge.h:225 | `native_open_lots_strategy.cpp` | The adverse excursion plus the entry share. | +| `strategy.closedtrades.max_drawdown()` | `max_drawdown` engine.hpp:200 | `pf_trade_t` pineforge.h:203 | `native_open_lots_strategy.cpp` | The adverse excursion plus the entry share. | | `strategy.closedtrades.max_drawdown_percent()` | derive from `max_drawdown` engine.hpp:200 | — | `native_open_lots_strategy.cpp` | | Pine has no word for *why* a row closed. The native row does: `closed_trade_close_cause` engine.hpp:1833 (C: -`strategy_closed_trade_close_cause` pineforge.h:1274) distinguishes a script +`strategy_closed_trade_close_cause` pineforge.h:1239) distinguishes a script close, a liquidation, a risk flatten and the range end — the kernel states the last three itself. The bracket-leg cause (`2`) is the kernel's too for a close its owner's fill armed — a `WaitForApplied` owner relation with an intent that @@ -658,13 +658,13 @@ classifies its own rows by order family instead. Which leg closed a row is its | Pine | C++ | C | Runs in | Notes | | --- | --- | --- | --- | --- | -| `request.security()` | `NativeTimeframeSubscription` native_run_spec.hpp:486 in `subscriptions` native_run_spec.hpp:633, or `declare_timeframe_subscriptions` native_host.hpp:1136 inside `on_native_run_begin` native_host.hpp:872 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2898 | `native_htf_strategy.cpp` | A subscription is a series instance: several may share one timeframe. Completed buckets arrive at `on_native_timeframe_bar` native_host.hpp:885 and the latest is pulled with `native_series_bar` native_host.hpp:1119. `authoritative_bars` native_run_spec.hpp:488 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:799 and `pf_native_gaps_e` native_c_api.h:810 (lane E7). The Pine adapter runs its own plain sites through these same subscriptions, ruled **adapter-hook** in ADR-0001's `subscriptions` row. | -| `request.security_lower_tf()` | `NativeAuxiliaryFeed` native_run_spec.hpp:515 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:483 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3175 | `native_auxiliary_feed_strategy.cpp` | Not the same shape: Pine returns an intrabar *array* per bar, the kernel gives you a finer *series* routed by time. A host that wants the raw sub-bars puts them in `IntrabarPath` native_run_spec.hpp:378 and reads `on_native_sub_bar` native_host.hpp:928 instead. | +| `request.security()` | `NativeTimeframeSubscription` native_run_spec.hpp:486 in `subscriptions` native_run_spec.hpp:633, or `declare_timeframe_subscriptions` native_host.hpp:1136 inside `on_native_run_begin` native_host.hpp:872 | `strategy_native_declare_subscriptions_v1` native_c_api.h:2917 | `native_htf_strategy.cpp` | A subscription is a series instance: several may share one timeframe. Completed buckets arrive at `on_native_timeframe_bar` native_host.hpp:885 and the latest is pulled with `native_series_bar` native_host.hpp:1119. `authoritative_bars` native_run_spec.hpp:488 replace a completed bucket's OHLCV. The row's two delivery words are typed in C: `pf_native_lookahead_e` native_c_api.h:799 and `pf_native_gaps_e` native_c_api.h:810 (lane E7). The Pine adapter runs its own plain sites through these same subscriptions, ruled **adapter-hook** in ADR-0001's `subscriptions` row. | +| `request.security_lower_tf()` | `NativeAuxiliaryFeed` native_run_spec.hpp:515 with `NativeSeriesSource::AuxiliaryFeed` native_run_spec.hpp:483 | `strategy_native_append_auxiliary_bars_v1` native_c_api.h:3194 | `native_auxiliary_feed_strategy.cpp` | Not the same shape: Pine returns an intrabar *array* per bar, the kernel gives you a finer *series* routed by time. A host that wants the raw sub-bars puts them in `IntrabarPath` native_run_spec.hpp:378 and reads `on_native_sub_bar` native_host.hpp:928 instead. | | `barmerge.gaps_off` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:490 set false | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_HOLD` native_c_api.h:811 | `native_htf_strategy.cpp` | The delivered bucket stands until the next delivery replaces it. | | `barmerge.gaps_on` | `NativeTimeframeSubscription::gaps` native_run_spec.hpp:490 set true | `pf_native_subscription_v1::gaps` = `PF_NATIVE_GAPS_CLEAR` native_c_api.h:813 | `native_htf_strategy.cpp` | The series is cleared on every input bar it delivers nothing on, so the pull answers empty — the native spelling of `na`. | | `barmerge.lookahead_off` | `NativeTimeframeSubscription::lookahead` native_run_spec.hpp:489 set false | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_COMPLETION` native_c_api.h:800 | `native_htf_strategy.cpp` | The default and the honest one: a bucket is delivered when it completes. | | `barmerge.lookahead_on` | `NativeTimeframeSubscription::lookahead` native_run_spec.hpp:489 set true | `pf_native_subscription_v1::lookahead` = `PF_NATIVE_LOOKAHEAD_AT_FIRST_INPUT` native_c_api.h:802 | `tests/test_native_htf_subscriptions.cpp` | Delivers a bucket's final values on the input that held its first bar. It is lookahead: use it to reproduce a chart, never to trade. | -| `request.currency_rate()` | none — no FX data feed | `strategy_configure_native_fx_curve_v1` pineforge.h:584 | `native_fx_roll_strategy.cpp` / `tests/test_native_fx_curve.cpp` | The kernel converts with the curve you stage; it fetches nothing. | +| `request.currency_rate()` | none — no FX data feed | `strategy_configure_native_fx_curve_v1` pineforge.h:546 | `native_fx_roll_strategy.cpp` / `tests/test_native_fx_curve.cpp` | The kernel converts with the curve you stage; it fetches nothing. | | `request.dividends()` | none — no fundamentals feed | — | — | Rejected at transpile for a Pine script; a native host fetches and models it itself. | | `request.earnings()` | none — no fundamentals feed | — | — | Same. | | `request.financial()` | none — no fundamentals feed | — | — | Same. | @@ -744,21 +744,21 @@ static int on_bar(void* user, const pf_bar_t* bar, const pf_native_decision_v1* } ``` -The host is a `pf_native_callbacks_v1` native_c_api.h:2456 table handed to -`strategy_native_host_create_v1` native_c_api.h:2639; the run is -`pf_native_run_spec_v1` pineforge.h:548 plus `pf_native_run_spec_ext_v1` -native_c_api.h:2267 through `strategy_configure_native_ext_v1` -native_c_api.h:3113, and `strategy_native_run_v1` native_c_api.h:2652 drives -the bars into a `pf_report_t` pineforge.h:469. +The host is a `pf_native_callbacks_v1` native_c_api.h:2475 table handed to +`strategy_native_host_create_v1` native_c_api.h:2658; the run is +`pf_native_run_spec_v1` pineforge.h:510 plus `pf_native_run_spec_ext_v1` +native_c_api.h:2286 through `strategy_configure_native_ext_v1` +native_c_api.h:3132, and `strategy_native_run_v1` native_c_api.h:2671 drives +the bars into a `pf_report_t` pineforge.h:431. Three rules govern the C door: 1. **Every struct is tagged and size-prefixed.** Set `struct_size` - native_c_api.h:2096 and `version` native_c_api.h:2097; a mismatch is refused + native_c_api.h:2115 and `version` native_c_api.h:2116; a mismatch is refused with a documented negative status and mutates nothing. 2. **A callback must not unwind, and must return 0.** A non-zero return latches `NativeFailureCode::CallbackException` native_host.hpp:69 and ends the run - `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2879. + `Failed`, readable with `strategy_native_state_v1` native_c_api.h:2898. 3. **Installing `on_recalculate` replaces `on_bar`.** A C host that installs only the recalculate hook and still expects the bar hook silently trades nothing. @@ -978,7 +978,7 @@ closes the stop at `101 - 20 × 0.25 = 96`. *accepted* on bar *k* and *filled* at the next eligible point — the open of bar *k+1* under the default `NativeCloseExecution::NextEligiblePoint` native_run_spec.hpp:34. Acceptance and fill are separate rows in - `native_events` native_host.hpp:1317. + `native_events` native_host.hpp:1335. 3. **`strategy.entry` reverses, `Transact` does not.** If the Pine strategy relies on an entry flipping a short into a long, spell it `ReverseTo` native_order.hpp:60. @@ -1036,13 +1036,13 @@ PINEFORGE_EXPORT_NATIVE_STRATEGY(MyHost); ``` `PINEFORGE_EXPORT_NATIVE_STRATEGY` native_module.hpp:271 defines -`strategy_create` pineforge.h:597, `strategy_free`, `strategy_set_input` -pineforge.h:680, `strategy_set_override` pineforge.h:690, -`strategy_set_magnifier_volume_weighted` pineforge.h:698, `run_backtest` -pineforge.h:631, `run_backtest_full` pineforge.h:648 and `report_free` -pineforge.h:662. It adds **no** new C symbol: every remaining runtime export — -`strategy_configure_native_v1` pineforge.h:548, the `strategy_stream_*` family -c_abi.cpp:589-708, `strategy_execution_contract` pineforge.h:498 — already +`strategy_create` pineforge.h:578, `strategy_free`, `strategy_set_input` +pineforge.h:642, `strategy_set_override` pineforge.h:652, +`strategy_set_magnifier_volume_weighted` pineforge.h:660, `run_backtest` +pineforge.h:593, `run_backtest_full` pineforge.h:610 and `report_free` +pineforge.h:624. It adds **no** new C symbol: every remaining runtime export — +`strategy_configure_native_v1` pineforge.h:510, the `strategy_stream_*` family +c_abi.cpp:578-697, `strategy_execution_contract` pineforge.h:460 — already lives in the engine, and the generated `strategy_create` references `pf_abi_version()` so a static link keeps that object. @@ -1059,7 +1059,7 @@ target `native_market_example` runner/CMakeLists.txt:45 — from the same source A native host is *not* a TradingView-parity claim. When you port a strategy that already exists in Pine and want to know exactly where the two differ, run -the twin harness of `native-feature-parity.md:331`: +the twin harness of `native-feature-parity.md:336`: 1. **Same inputs.** One probe, one bar feed, one `NativeRunSpec`. Run it through codegen + the adapter, and through your `NativeStrategyHost`. @@ -1068,8 +1068,8 @@ the twin harness of `native-feature-parity.md:331`: number shifts the moment one row is added or dropped, which turns a one-trade difference into a whole-list difference. 3. **Then diff the detail.** Event order and kind from `native_events` - native_host.hpp:1317, ownership (which opening a close consumed), and the - continuation hash `native_continuation_hash` native_host.hpp:1358. + native_host.hpp:1335, ownership (which opening a close consumed), and the + continuation hash `native_continuation_hash` native_host.hpp:1376. 4. **Explain every difference against a named row.** §1 of `native-feature-parity.md:42` marks each TradingView-only rule — money rounding, half-tick trigger thresholds, trail conventions, margin-call From 10f386f85ebdb94bb7d1bb04812454da9a245912 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Sat, 26 Sep 2026 17:34:18 +0800 Subject: [PATCH 83/83] Adapter: a terminal receipt read at the next open cancels only the siblings its leg had (R5 lane INT26, round 2: the gate sweep's leaver) INT26's gate sweep (exp-r5-int26-20260926 against rebaseline e69aa1f7) refused the merge on one leaver, scrapper:data-BINANCE-BTCUSDT/standard/vasudevshenoy-manoj-betrayed-me (BTCUSDT 15m, process_orders_on_close, pyramiding 0; stop entries 'Buy'/'Sell' with strategy.exit stop/limit brackets, a pending entry cancelled when the next bar does not trigger it): 821 of TradingView's 821 trades on main 0d76a099, 820 on ebe67484 (canonical 100 -> 99.9). Bisect (the probe's own feed, spark, one build per pick boundary): a6e6a005 and 2922ade0 book 821, e1d7a984 -- R5 lane PAR-ORDERS, H-MEASURE Finding 6b, a process_orders_on_close stop entry its placing close already reached fills at that close -- books 820, and so does every later pick. Mechanism, on 2025-08-03 10:00 UTC. The 09:45 close cancels its pending 'Buy', places it again (stop 113689.29) and re-issues 'Buy Exit' (limit 113771.41); the re-issued legs keep their older book slot, so on the 10:00 bar the pre-script drain settles the limit on its parent's touch bar -- after that bar's receipt read. The 10:00 bar closes at its high 113771.47; its body cancels 'Buy' again and places it at that high with a fresh 'Buy Exit' (limit 114004.53), and since e1d7a984 the close pass fills that re-entry at the close, as TradingView does (#274), so the fresh legs are working when 10:15 opens. The first limit's terminal receipt is read only then, and observe_terminal_receipts' sibling cancel withdrew every working leg of the same exit id, from_entry and origin -- the fresh bracket's included, both legs at origin 0. The re-entry ran on to the 15:15 session close and swallowed TradingView's #275 (11:45). Before e1d7a984 the re-entry rested to 10:15, its legs still staged, so the late read found no sibling to take. The pick is right; the sibling cancel was not. Fix, adapter only: cancel_bracket_siblings takes the ordinal the terminal receipt executed at when the observer reads it, and skips a matching leg of a later strategy.exit call (another command_sequence) that the kernel accepted after that ordinal (its Birth::acceptance_ordinal, read through native_working_requests() only when such a leg is among the matches): that leg is the same id's later order. A leg of the filled leg's own call is its sibling however late it was accepted -- the drain submits a bracket's legs one at a time, executing each, so a stop can be accepted after its limit filled -- and goes as before (a review of the first form, which skipped every leg accepted after the fill, found that case). The close passes that call it synchronously pass nothing and are unchanged. No state is added. The probe (spark, generated.cpp of codegen a4259656, the lane feed 6b54c44a): 821 trades, canonical rubric excellent, countAbsDelta 0 (820 / strong before). TradingView's #273-#275 exact: 10:00 113689.29 -> 10:00 113771.41; 10:00 113771.47 -> 11:00 114004.53; 11:45 114005.84 -> 15:15 113844.70. 819 of 821 trades exact to the minute and cent on both legs (main: 771; the two others, #1 and #3, differ on main too). Witness: tests/test_pooc_stop_reentry_bracket_tapes.cpp replays three `lab tv --no-note` tapes of synthetic scripts of the same shape on NYSE:F 15m (tests/fixtures/pooc_stop_reentry_bracket; tv_trades e0447b53 / b7e89ce1 / efbd25e3): three flat signal closes that each cancel and place again a stop entry with its bracket, 10c beyond the close, then 1c (the next bar fills the entry and then the limit and closes flat), then at that close (filled there), long on bars 39-41 and short on bars 44-46; TradingView books the same four rows on the chart and under the magnifier. Every row is compared field by field, with the end book and its working bracket legs. - int26-reentry-bracket-chart: all four rows, flat; the short is the leaver's shape. - int26-reentry-bracket-mag records a divergence the fix does not touch (the same before and after): there the drain leaves the short's retained limit to the flush below the body, whose book still holds the short, so the re-entry is never placed -- rows 1-3 exact, both books flat. - int26-reentry-noexit-chart, the control: the short's re-entry places no strategy.exit (TradingView's row 4 still open at the end); the short bracket's stop, accepted after its limit filled, must still be withdrawn. Fail-before: against ebe67484 2 of 27 checks fail (the chart tape: 3 rows against 4, short 100 against flat); against the first form of the rule 1 of 28 (the control's stop left working); after, 28 of 28. Floors: one source-bound row. RELEASE_MIN_TESTS 702 -> 703, EXCLUDED_REGISTERED_MIN 683/683/692 -> 684/684/693 (and test_ci_verify's expectation, docs/ci.md). KERNEL_MIN_TESTS unchanged. docs/ci.md's sentence "The PR registration floors at INT26 are 683 for Debug and sanitizers and 692 for native" is rewritten with the new counts. Docs: the design page's PAR-ORDERS paragraph records the rule and the magnified divergence; the CHANGELOG 1.0.0 entry's process_orders_on_close item adds it for a user. Every doc anchor carried onto this tree by the exact line map from ebe67484 (135 moved); the three citations of ci_verify.py's RELEASE_MIN_TESTS line (CONTRIBUTING.md, contributing-llm.md twice) set by hand, 390 -> 393. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 5 +- CONTRIBUTING.md | 2 +- docs/adr/0001-kernel-adapter-boundary.md | 28 +- docs/ci.md | 9 +- docs/design/native-feature-parity.md | 128 ++++--- docs/native-refactor-progress.md | 8 +- docs/pages/contributing-llm.md | 4 +- docs/pages/native-engine.md | 2 +- include/pineforge/source/pine_adapter.hpp | 5 +- scripts/ci_verify.py | 10 +- scripts/test_ci_verify.py | 2 +- src/source/pine_adapter.cpp | 34 +- tests/CMakeLists.txt | 11 + .../pooc_stop_reentry_bracket/README.md | 34 ++ .../int26-reentry-bracket-chart/meta.json | 16 + .../int26-reentry-bracket-chart/metrics.json | 40 ++ .../int26-reentry-bracket-chart/strategy.pine | 39 ++ .../int26-reentry-bracket-chart/tv_trades.csv | 9 + .../int26-reentry-bracket-mag/meta.json | 16 + .../int26-reentry-bracket-mag/metrics.json | 40 ++ .../int26-reentry-bracket-mag/strategy.pine | 39 ++ .../int26-reentry-bracket-mag/tv_trades.csv | 9 + .../int26-reentry-noexit-chart/meta.json | 16 + .../int26-reentry-noexit-chart/metrics.json | 40 ++ .../int26-reentry-noexit-chart/strategy.pine | 42 +++ .../int26-reentry-noexit-chart/tv_trades.csv | 9 + .../test_pooc_stop_reentry_bracket_tapes.cpp | 346 ++++++++++++++++++ 27 files changed, 853 insertions(+), 90 deletions(-) create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/README.md create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/meta.json create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/metrics.json create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/strategy.pine create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/tv_trades.csv create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/meta.json create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/metrics.json create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/strategy.pine create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/tv_trades.csv create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/meta.json create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/metrics.json create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/strategy.pine create mode 100644 tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/tv_trades.csv create mode 100644 tests/test_pooc_stop_reentry_bracket_tapes.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a408534..64a61009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -207,7 +207,10 @@ TradingView behaviour it matches. close (slippage applied), on the chart, an aggregated chart and under the bar magnifier, and so does one placed while the position still holds the other side, once a `strategy.close` on the same bar has closed it; each - filled at the next open. + filled at the next open. Such an entry placed again at the close of the + bar whose `strategy.exit` limit closed the previous one keeps the fresh + bracket it was placed with; the previous bracket's fill withdrew it at the + next open, and the position ran on without an exit. An add can no longer exceed `pyramiding` because an opposite entry was resting from an earlier bar. The corpus probe `order-deferred-flip-pooc-cross-bar-01` now books all 792 of its diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a38f3d70..eadd79f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -153,7 +153,7 @@ What each gate refuses: `ci_verify.py` counts the CTest rows that actually **ran** and fails below a floor — `KERNEL_MIN_TESTS` ci_verify.py:260 and `RELEASE_MIN_TESTS` -ci_verify.py:390. +ci_verify.py:393. A deleted or silently skipped row is a failure, not a quieter run. If your change adds rows, raise the floor in the same commit and say by how much; if it legitimately removes one, lower it deliberately and say why. `--min-tests` diff --git a/docs/adr/0001-kernel-adapter-boundary.md b/docs/adr/0001-kernel-adapter-boundary.md index d9c3b90c..5e5fe549 100644 --- a/docs/adr/0001-kernel-adapter-boundary.md +++ b/docs/adr/0001-kernel-adapter-boundary.md @@ -209,8 +209,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:529`) for TV sizing and fill spelling; `validate_execution_precommit` (`pine_strategy_host.cpp:554`), whose - `validate_precommit` (`pine_adapter.cpp:12907`) returns `AdmitWithHostMargin` - (`pine_adapter.cpp:13079`) to own the opening margin decision; `on_native_tick` + `validate_precommit` (`pine_adapter.cpp:12937`) returns `AdmitWithHostMargin` + (`pine_adapter.cpp:13109`) to own the opening margin decision; `on_native_tick` (`pine_strategy_host.cpp:299`) / `on_native_applied` (`pine_strategy_host.cpp:350`) 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 @@ -225,7 +225,7 @@ kernel and own these quirks, each at its site: `NativeCloseExecution::AfterCalculation` in the projected spec (`pine_adapter.cpp:2152-2153`); calc cadence and language publication are `PineScheduler`'s (`pine_scheduler.hpp:20`). - **Batching and open-order priority.** Same-bar command batching and its deferred queues - (`PendingSameBarCommand` pine_adapter.hpp:1584, `pending_bracket_legs_` pine_adapter.hpp:1996-2014); the retained parent-before-child ordering of live handles — an + (`PendingSameBarCommand` pine_adapter.hpp:1584, `pending_bracket_legs_` pine_adapter.hpp:1999-2017); 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:975-1045`). Kernel priority is acceptance/incarnation order. @@ -233,9 +233,9 @@ kernel and own these quirks, each at its site: (`select_exit_activation` src/compat/pine/exit_activation.cpp:42, `select_exit_suspension` src/compat/pine/exit_lifecycle.cpp:10, `select_replacement_revival_definition` src/compat/pine/exit_lifecycle.cpp:46) 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:9588-9593`); the half-tick + extremes while the raw running best is retained (`has_trail_request` `pine_adapter.cpp:9618-9623`); the half-tick trigger threshold (`source_trigger_threshold` pine_adapter.cpp:258) and raw-vs-booked fill spelling, behind the - adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11679`). + adapter's terms seam (`resolve_terms` `pine_adapter.cpp:11709`). - **Money arithmetic.** Ten-significant-digit half-up money (`source_money_round` `pine_adapter.cpp:370-376`, 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 @@ -252,12 +252,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:2024-2035`); the POOC reservation-growth population predicate + ledger (`close_logical_units_` `pine_adapter.hpp:2027-2038`); 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:13868`) for the scheduling, - `resolve_margin_requirement` (`pine_adapter.cpp:13937`) for the ten-significant-digit money, + with TradingView's: `margin_check_allowed` (`pine_adapter.cpp:13898`) for the scheduling, + `resolve_margin_requirement` (`pine_adapter.cpp:13967`) 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 @@ -268,7 +268,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:16715`) and + (`non_pooc_commissioned_short` `pine_adapter.cpp:16745`) and the 1x-long money call — plus the TV admission scopes (`explicit_pair_scope` `src/compat/pine/market_admission.cpp:33-46`) and the review fold they feed (`awaits_pair_review` `:67-72`, `fold_admission_history` `:79-136`). @@ -317,9 +317,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:8272`), the first-open execution chain and its + (`next_source_path_waypoint` `pine_adapter.cpp:8302`), the first-open execution chain and its own loop guard (`kFirstOpenLoopGuard` `pine_scheduler_native.cpp:663`), and the two fills Pine refuses to - recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5552`). + recalculate on (`suppress_grouped_stop_recalc` `pine_adapter.cpp:5582`). - **Pine language state and harness flags.** Series, the tick-level barstate flags and position-view freezing (`PineLanguageState` `pine_language_state.hpp:12`); the three session flags, `PineStrategyHost` members since R5 lane F5 (`session_ismarket_` @@ -372,7 +372,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:370-376`). `strategy.close` batching and the entry-id ledger - are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2024-2035`) and + are **gone** from the kernel — the ledger is adapter state (`close_logical_units_` `pine_adapter.hpp:2027-2038`) 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:417`); what enables the generic model is the presence of `NativeRunSpec::margin`. So is the string-sentinel decoding of adapter-written comments: @@ -681,8 +681,8 @@ 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:258`), `source_level_on_price_grid` (`:244`), `nearest_tick` (`:182`) / `source_bar_fill_tick` (`:203`) / `directional_tick` (`:227`), 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`, `tests/test_native_price_grid.cpp` | -| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13661`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14759`), `chart_day_key` (`pine_adapter.cpp:13558`), `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:242`) and `configure_native` (`pine_strategy_host.cpp:243`) 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:13026-13027`): an entry is refused once the book already holds the limit | design row MG2 (R5-1): the resulting-book cap is the generic one. Measured by N12's scenario `PS` in `tests/test_adapter_risk_relower.cpp`: two-unit entries against a limit of 3 leave the adapter at 4 and the kernel cap at 2 | `tests/test_native_resting_matching_contract.cpp`, `tests/test_native_run_spec.cpp` | +| `risk` | **native-only** | all of `strategy.risk.*` but the direction: `update_risk_state` (`pine_adapter.cpp:13691`), `SourceDayLedger`, `submit_intraday_loss_close` (`pine_adapter.cpp:14789`), `chart_day_key` (`pine_adapter.cpp:13588`), `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:242`) and `configure_native` (`pine_strategy_host.cpp:243`) 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:13056-13057`): 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:2194-2196`) | design row MG3 (R5-1); the contract comment in `project()`. Measured against TradingView by R5 lane H-MEASURE (`tests/test_pyramiding_count_differential.cpp`, 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 8 (it counts a resting entry, admits three market entries on a flat bar and keeps a slot another entry's exit drained). 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:2221-2229`, `margin.maintenance_long` `pine_adapter.cpp:2236`) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`, 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** | TradingView's range-end report re-marks the curve's last point and re-folds every extreme from it (`scheduler_record_range_end` pine_strategy_host.cpp:1306): report shape, not a mark-to-market row (`KernelRecordedAtHostMarks` pine_adapter.cpp:2191) | 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` | diff --git a/docs/ci.md b/docs/ci.md index e570fdd4..0ab624e4 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -102,10 +102,11 @@ requires the rows that actually ran to equal the second count, and records `registered`, `labelled = registered - selected`, and `ran` in `ctest-exclusion.log` and `ci-summary.json`. A skip, missing executable, lost registration below the PR registration floor, unreadable enumeration, or -missing label fails. The PR registration floors at INT26 are 683 for Debug -and sanitizers and 692 for native (653 and 662 at the INT24 base, plus wave G's -six rows and wave H's twenty-four). The full-run release and kernel floors are -702 and 286 rows that ran; full runs do not exclude a label. +missing label fails. The PR registration floors at INT26 are 684 for Debug +and sanitizers and 693 for native (653 and 662 at the INT24 base, plus wave G's +six rows, wave H's twenty-four and INT26's own tape row). The full-run release +and kernel floors are 703 and 286 rows that ran; full runs do not exclude a +label. Preflight also runs the detached-comment census of the kernel compile closure (`detached-comments`: `scripts/measure_detached_comments.py --check-ceiling`) diff --git a/docs/design/native-feature-parity.md b/docs/design/native-feature-parity.md index af4e2298..e1ab3b92 100644 --- a/docs/design/native-feature-parity.md +++ b/docs/design/native-feature-parity.md @@ -47,19 +47,19 @@ 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:2600-2611; `entry()` pine_adapter.cpp:5197, `order()` pine_adapter.cpp:10476) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5710-5757; `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:8333 → `Flatten{}` pine_adapter.cpp:8362-8363; `Reduce{ExplicitUnits}` pine_adapter.cpp:7449-7458; `ReverseTo` pine_adapter.cpp:6289-6290) | **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 | — | +| OL1 | Market / limit / stop / stop-limit / trail triggers (`trigger_for` pine_adapter.cpp:2600-2611; `entry()` pine_adapter.cpp:5227, `order()` pine_adapter.cpp:10506) | **yes** — `Trigger` native_order.hpp:296-315; geometric matching `first_region_entry` native_execution_consumer.cpp:5710-5757; `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:8363 → `Flatten{}` pine_adapter.cpp:8392-8393; `Reduce{ExplicitUnits}` pine_adapter.cpp:7479-7488; `ReverseTo` pine_adapter.cpp:6319-6320) | **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:2570-3777 → `host.replace` pine_adapter.cpp:2586-2666) | **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:4596-4642, `cancel_all` pine_adapter.cpp:17940-17955; `live_by_source_key_` pine_adapter.hpp:1996; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3927-3992) | **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:4052; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7121-7471) | **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:5421-5456, `cohort_for` pine_adapter.cpp:2335; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7454-7471) | **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) | +| OL4 | Cancel all / cancel by label; label → handle book (`cancel(id)` pine_adapter.cpp:4596-4642, `cancel_all` pine_adapter.cpp:17970-17985; `live_by_source_key_` pine_adapter.hpp:1999; predecessor state hand-carried `projection_predecessor` pine_adapter.cpp:3927-3992) | **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:4052; residual closes `enqueue_pooc_fifo_close` pine_adapter.cpp:7151-7501) | **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:5451-5486, `cohort_for` pine_adapter.cpp:2335; FIFO prefix computed adapter-side `source_fifo_prefix_openings` pine_adapter.cpp:7484-7501) | **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:2609-2615) | **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:8455; lowering `submit_one` pine_adapter.cpp:9037-9049, `BindCohort` pine_adapter.cpp:9538-9544, `submit_leg` pine_adapter.cpp:9562-9586; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | -| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8635-8638; `materialize_relative_exits` pine_adapter.cpp:17230-17271, pine_adapter.hpp:1810-1822) | **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:5636-5670. `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:1231-1247; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19030-19059) | **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:1998-2016); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:975-1054); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3331-3498, `materialize_snapshot` pine_adapter.cpp:10876-10900), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1563-1586, `CloseCallsiteState` pine_adapter.hpp:1657-1672; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15469, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15703) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:669-671 | A | — | F:A15 O:A15 S:O9 S:R3 | R5-1 names KI-62. S: the kernel exposes deterministic insertion order only. | +| OL10 | TP/SL bracket per entry, armed before the entry fills, OCA between legs (`exit()` pine_adapter.cpp:8485; lowering `submit_one` pine_adapter.cpp:9067-9079, `BindCohort` pine_adapter.cpp:9568-9574, `submit_leg` pine_adapter.cpp:9592-9616; lifecycle `select_exit_suspension` exit_lifecycle.cpp:10-66) | **partial** — every primitive is native (`WaitForApplied{parent}` native_order.hpp:436-438, `Reduce{OwnerOpenedUnits}` native_order.hpp:220-224, `Member{…, Cancel}` native_order.hpp:468-475, `waiting_children` native_order.hpp:1895); no builder **Closed:** the builder is `native_toolkit::submit_bracket(host, BracketSpec)` — take-profit, stop-loss and trail as one parent's `WaitForApplied` children in one OCA group, with `anchor_rounding` and `visibility` knobs — and it adds no kernel behaviour (`tests/test_native_toolkit_bracket.cpp`). | K (toolkit) | L7 | F:A10 O:A8 S:O6 | **Disagree:** F "yes (primitives)" + toolkit / S partial + new kernel bracket object, lane size L and prerequisite for everything. R5-2: builder over the existing primitives, size M. | +| OL11 | Bracket levels relative to the parent's fill (profit / loss ticks; derived `profit_ticks` pine_adapter.cpp:8665-8668; `materialize_relative_exits` pine_adapter.cpp:17260-17301, pine_adapter.hpp:1813-1825) | **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:5636-5670. `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:1231-1247; the POD is filled only by the adapter `pine_birth_reach` pine_adapter.cpp:19060-19089) | **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:2001-2019); KI-62 open-order priority (`select` order_priority.cpp:13-60, selected `update_l4c_priority` pine_adapter.cpp:975-1054); per-bar priced-entry throttle, BUY-before-SELL batch, short-seed plan / collision (`maybe_activate_short_seed_plan` pine_adapter.cpp:3331-3498, `materialize_snapshot` pine_adapter.cpp:10906-10930), reversal-gap bracket policy (`ShortSeedPlan` pine_adapter.hpp:1151-1178, `DeferredOpenMarketableSell` pine_adapter.hpp:1563-1586, `CloseCallsiteState` pine_adapter.hpp:1657-1672; `apply_reversal_gap_bracket_policy` pine_adapter.cpp:15499, `reaccept_gapped_bracket_behind_same_id_add` pine_adapter.cpp:15733) | n/a — native priority is acceptance / incarnation order `native_events` native-engine.md:669-671 | 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:42-88, enforced `leg_activation` pine_adapter.cpp:869-874; `select_exit_activation` exit_activation.cpp:18-88, `select_historical_birth_reach` order_birth.cpp:5-14, `capture_order_birth` pine_adapter.cpp:714-746) | 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:1677); the revival skips a withdrawn exit (`revive_brackets_after_margin` pine_adapter.cpp:1751). 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,26 +68,26 @@ 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:5722-5744; `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:39 + grid snap exit_lifecycle.cpp:22-27; `trail_activation_level` pine_adapter.hpp:284-286) | **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:11787-11799) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4473-4478) **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:11700-11837) stays A. | +| TR3 | Zero-distance trail, "ride the best, exit on any adverse tick" (adapter substitutes `tick * 0.5` pine_adapter.cpp:11817-11829) | **no** — the kernel fails the run when `offset <= 0` (`checked_trail_stop` → native_execution_consumer.cpp:4473-4478) **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:11730-11867) stays A. | | TR4 | Re-price a trail without losing the running extreme (adapter skips `replace` and reads `trail_state()` pine_adapter.cpp:3884-3930) | **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:287-289) is A. | -| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9588-9593, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11700-11837, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8605-8831, `trail_one_shot` pine_adapter.cpp:9677-9806 | n/a | A | — | F:B3 S:T2 | R5-1 | +| TR5 | *(quirk)* TV trail conventions: half-tick arm threshold against tick-quantized extremes `has_trail_request` pine_adapter.cpp:9618-9623, zero-offset pricing `zero_trail_sibling_stop` pine_adapter.cpp:11730-11867, retained best `retained_trail_best` pine_adapter.hpp:287-289, operand / restart rules `source_trail_points` pine_adapter.cpp:8635-8861, `trail_one_shot` pine_adapter.cpp:9707-9836 | 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:5832-5836, pine_adapter.cpp:5832) | **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:10989-12193) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:4958, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4935-4969 | 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. | +| SZ1 | Explicit units (`Transact{signed}` after a grid floor pine_adapter.cpp:5862-5866, pine_adapter.cpp:5862) | **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:11019-12223) | **yes** — `HostSized` native_order.hpp:73-78 + `resolve_execution_terms` native_host.hpp:945-949, invoked `resolve_execution_terms` native_execution_consumer.cpp:4958, guarded by `consume_matched_request`'s `try` native_execution_consumer.cpp:4935-4969 | 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:2458-2513) | **no** — `HostSized` without host units is `TermsUnresolved` native_execution_consumer.cpp:4987-4989 **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: rounded equity `source_money_round` pine_adapter.cpp:2403, money floor-to-lot `source_money_floor_lot` pine_adapter.cpp:378 (→ SZ10) | | SZ4 | Cash-value sizing (`default_qty_type` pine_adapter.cpp:2412-2421) | **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:2471, `typed_entry_units` pine_adapter.cpp:2489; `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:2450; `percent_commission_live_equity` pine_adapter.cpp:4191-4232) | **no** — only a dead primitive, `reserve_percent_commission` (since deleted), with no caller in `src/` **Closed:** `Sized::reserve_percent_fee` divides the sizing cash by `1 + fee_value / 100` under `NativeFeeKind::Percent` and is an exact no-op for every other fee kind; the dead `reserve_percent_commission` primitive is gone (`tests/test_native_sizing_bases.cpp`, `tests/test_native_sizing_price_rule.cpp`). | K | L3 | F:C3 O:C6 S:S3 | **Disagree:** F lists the reserve under quirk yet offers a knob / O kernel, "partial" / S knob. R5-1: fee-net option is generic. TradingView's cash-commission reserve stays adapter policy over the kernel's `CashValue`: a percent-of-equity default quantity under a cash-per-order or cash-per-contract fee takes its percentage of `strategy.equity` (`PineSizingSnapshot::strategy_equity`, the open entries' cash fees charged) and leaves out the fee its own order pays (`default_sizing_cash`, R5 lane PAR-CASHFEE, §3.10; `tests/test_cash_fee_sizing_tapes.cpp` on the tapes in `tests/fixtures/cash_fee_sizing`) | -| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2347-2362, `PineSizingSnapshot` pine_adapter.hpp:147-159; fill-time exceptions `default_sized` pine_adapter.cpp:6627-6653) | **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`. | +| SZ6 | Placement-time vs fill-time sizing (`sizing_snapshot` pine_adapter.cpp:2347-2362, `PineSizingSnapshot` pine_adapter.hpp:147-159; fill-time exceptions `default_sized` pine_adapter.cpp:6657-6683) | **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:592, 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:5004-5007; 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:12497-12525, `compute_exit_reservation` pine_adapter.cpp:4289-4408) | **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:4341-4660, `reserved_exit_qty` pine_adapter.cpp:8964-9274) | **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. | +| SZ8 | Fractional reduce of the position / a cohort (`quantize_close_units` pine_adapter.cpp:12527-12555, `compute_exit_reservation` pine_adapter.cpp:4289-4408) | **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:4341-4660, `reserved_exit_qty` pine_adapter.cpp:8994-9304) | **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:369-376, `source_money_floor_lot` pine_adapter.cpp:378; twin `tv_money_round` pine_policy_support.hpp:9-15); exact percent-exit rounding (`quantize_percent_exit_units` pine_adapter.cpp:4321-4336) | n/a — kernel money is plain binary64 `marked_equity` engine_execution.cpp:1638-1657 | A | — | O:C8 F:C3 S:M4 S:S5 | R5-1 | -| 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:6688), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7894), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12755), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13131, pine_adapter.cpp:13184) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15517) | 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:2458), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2471, `typed_entry_units` pine_adapter.cpp:2489), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18622) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7157-7507; ledger `close_batch_bar_` pine_adapter.hpp:2039-2050; 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. | +| 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:6718), in the paired-reversal whole drop (`close`, `affordable_price` pine_adapter.cpp:7924), at the terms boundary (`resolve_terms`, `affordable_price` pine_adapter.cpp:12785), in the precommit money scopes (`validate_precommit`, `affordable_price` pine_adapter.cpp:13161, pine_adapter.cpp:13214) and in the carried-money check (`apply_reversal_gap_bracket_policy`, `affordable_price` pine_adapter.cpp:15547) | 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:2458), the typed per-call quantity at placement, at the fill and in the probe (`typed_quantity_units` pine_adapter.cpp:2471, `typed_entry_units` pine_adapter.cpp:2489), and the pending-order fill-quantity probe (`probe_fill_qty` pine_adapter.cpp:18652) | A | — | AUDIT3-opus2 H4 (G2 R2, R3) | **Measured:** the quotient is TradingView's whole-drop PRICE, pinned by 445 famr3 tapes (`tests/test_tv_money_band_l4b.cpp`) and 202 famr tapes (`tests/test_tv_money_precision_l4b.cpp`); 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:7187-7537; ledger `close_batch_bar_` pine_adapter.hpp:2042-2053; 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:933) | n/a | A | — | S:O10 | (single-source) R5-4 confirms S's own split: concept → SZ9, predicate A. | ### 1.4 Margin, admission, liquidation, risk @@ -95,21 +95,21 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | ID | Feature (adapter mechanism) | Native today | Class | Lane | Sources | Notes | |---|---|---|---|---|---|---| | MG1 | Allowed opening directions (`allowed_open_directions` pine_adapter.cpp:2195) | **yes** — `allowed_open_directions` native_run_spec.hpp:597 → `MatchRejectReason::OpeningDirection` native_execution_consumer.cpp:2956-2964 | 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:595 tests the resulting book native_execution_consumer.cpp:2970-2973 | 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:13026-13029) is TV. | -| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2975-2978 | 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:6137-6186, cap left unset `max_open_lots` pine_adapter.cpp:2194-2196) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. 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:8888-8894). | -| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13079-13145 and leaves the kernel gate unset pine_adapter.cpp:13078-13080) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2993 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:2839 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | -| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13729-13730, `margin_pct` pine_adapter.cpp:13143) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:13834-14045, `submit_margin_call_units` pine_adapter.cpp:14006-14052, `schedule_margin_call_path` pine_adapter.cpp:18093-18194) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2952 **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:13760-13796) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; `tests/test_adapter_margin_schedule_differential.cpp` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). | +| MG2 | Max absolute position size | **yes** — `max_abs_units` native_run_spec.hpp:595 tests the resulting book native_execution_consumer.cpp:2970-2973 | 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:13056-13059) is TV. | +| MG3 | Open-lot cap (pyramiding) | **yes** — `max_open_lots` native_run_spec.hpp:596 → `MaxOpenLots` native_execution_consumer.cpp:2975-2978 | 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:6167-6216, cap left unset `max_open_lots` pine_adapter.cpp:2194-2196) is TV. **Measured (R5 lane H-MEASURE), and not TradingView's rule:** on 15 scenarios of three `lab tv` tapes (`tests/test_pyramiding_count_differential.cpp`) TradingView checks an entry once, at its first eligible point, against the trades then open; the kernel's `max_open_lots` books 14 as TradingView does, the adapter's count 8 (it counts a resting entry, admits three market entries on a flat bar, keeps a slot another entry's exit drained). The fifteenth, a resting entry accepted below the cap and filled at it, needs a first-eligible-point rule neither side has. 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:8918-8924). | +| MG4 | Initial-margin opening gate (adapter answers `AdmitWithHostMargin` from frozen signal-time equity pine_adapter.cpp:13109-13175 and leaves the kernel gate unset pine_adapter.cpp:13108-13110) | **yes** — `initial_margin_fraction` native_run_spec.hpp:598; the requirement `resulting_abs_notional * fraction` native_execution_consumer.cpp:2993 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:2839 hands the point's rate to the freeze, to `marked_equity_at` and to the engine's rate-explicit settlement inspection `inspect_native_settlement_scoped_at` engine.hpp:649, and the engine's presented clock is never written (`tests/test_native_margin_fx_clock.cpp` sections 8, 10 and 11) | K | — | F:D1 O:D1 S:M2 | **Disagree:** F, O partial (they fold per-side in) / S yes. Row split → MG5. **Measured (R5 lane H-MEASURE, M11):** the two gates part both ways on the same bars — the kernel's declines a 1× long whose entry fee the adapter admits, and admits a gap-up add the adapter refuses against its signal-time equity; `ab9714be` books the adapter's answer both times (`tests/test_adapter_margin_schedule_differential.cpp`). | +| MG5 | Per-side initial margin (`margin_pct` pine_adapter.cpp:13759-13760, `margin_pct` pine_adapter.cpp:13173) | **no** — one `std::optional` `initial_margin_fraction` native_run_spec.hpp:598 **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:13864-14075, `submit_margin_call_units` pine_adapter.cpp:14036-14082, `schedule_margin_call_path` pine_adapter.cpp:18123-18224) | **no** — "no maintenance liquidation" `initial_margin_fraction` native_run_spec.hpp:598-599; margin is checked only when `inspect.would_open` native_execution_consumer.cpp:2952 **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:13790-13826) | **no** **Closed:** `NativeLiquidationSizing::{RestoreMinimum, ShortfallMultiple, Flatten}` with `shortfall_multiple` and `liquidation_min_units`, capped at the held quantity and floored onto `quantity_grid`; `resolve_margin_call_units` native_host.hpp:995 gives a host the last word, clamped into `(0, held]` (`tests/test_native_margin_model.cpp`, `tests/test_native_margin_hooks.cpp`). | K+A | L4 | O:D4 F:D2 S:M4 | **Disagree:** F generic knob `ShortfallMultiple` / O "×4 is A" / S quirk, yet a 4.0 default inside its generic model. R5-1: policy knob in the kernel, plain default (restore-minimum). **Decided by R5 lane F7 (the D2 question):** the adapter selects NO kernel knob — `project()` leaves `sizing` / `shortfall_multiple` / `liquidation_min_units` unset and its `resolve_margin_call_units` answers every call with TradingView's slice (the restore floored onto the lot grid BEFORE the ×4, floored again, the one-contract band, the dust gate). Measured: on a one-unit grid, 20 long at 100 on 1000 at 50 % reaching 93.9, TradingView books 4 lots and the kernel's own `ShortfallMultiple 4` books 5 (`tests/test_native_margin_hooks_twin.cpp` MG-F3, an executed run, no longer a comment); without a grid the two agree bit for bit (MG-F/H, 4.2105263157894735), but declaring the knob for those runs alone folds `NativeMarginModel::sizing` into every margin run's identity — an epoch decision F7 does not take. Since R5 lane H-THIN the pre-open admission slice (`schedule_preopen_margin_slice`) sizes through the same `source_margin_units`, where it kept its own copy of the ×4 without the band or the dust gates (AUDIT4 §6 G2-12; `tests/test_adapter_margin_schedule_differential.cpp` M10: band and fudge now 1 and 1, the kernel's and `ab9714be`'s). | | MG8 | Liquidation price query (`margin_liquidation_price` pine_strategy_host.cpp:114-130) | **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:16121-16137, `apply_fx_opening_margin_slice` pine_adapter.cpp:18195-18223) | **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:16685-16724) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13115-13118) | **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:13690-13704, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14759-14789) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14802-15036) | **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:13506-13579) | **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:13718, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13777, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14523-14563, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14047-14174, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14047) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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, `tests/fixtures/margin_entry_bar/pm2-*`). 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 (`tests/fixtures/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; `tests/fixtures/half_tick_rounding`, PAR-MARGIN-2). | -| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3680-3747, `record_market_review` pine_adapter.cpp:15040-15098) | n/a | A | — | S:R2 | (single-source) | +| MG9 | FX revaluation of margin (`apply_fx_open_margin_slice` pine_adapter.cpp:16151-16167, `apply_fx_opening_margin_slice` pine_adapter.cpp:18225-18253) | **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:16715-16754) | **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:1197-1205; gate `max_cons_loss_days` pine_adapter.cpp:13145-13148) | **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:13720-13734, forced flatten `submit_intraday_loss_close` pine_adapter.cpp:14789-14819) | **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:83-241, `IntradayOrderBudget` intraday_order_budget.hpp:27-92; `execute_due_cap_close` pine_adapter.cpp:14832-15066) | **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:13536-13609) | **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:13748, 4× shortfall `4.0 * minimum` pine_adapter.cpp:13807, POOC chronology exceptions `competing_entry` pine_adapter.cpp:14553-14593, 1×-long money call `submit_tv_money_long_margin_call` pine_adapter.cpp:14077-14204, fill pricing (the slice books at its waypoint's mark, `submit_tv_money_long_margin_call` pine_adapter.cpp:14077) | 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 (`tests/test_adapter_margin_schedule_differential.cpp`): 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, `tests/fixtures/margin_entry_bar/pm-m10-*`, show TradingView does); the entry bar of a leveraged opening, where four `lab tv` tapes (`tests/fixtures/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, `tests/fixtures/margin_entry_bar/pm2-*`). 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 (`tests/fixtures/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; `tests/fixtures/half_tick_rounding`, PAR-MARGIN-2). | +| MG16 | *(quirk)* Historical market-admission review / retention journal (`explicit_pair_scope` src/compat/pine/market_admission.cpp:33 and the review fold it feeds (`original_pair_call` src/compat/pine/market_admission.cpp:47); `admission_allocation` pine_adapter.cpp:3680-3747, `record_market_review` pine_adapter.cpp:15070-15128) | n/a | A | — | S:R2 | (single-source) | ### 1.5 Calculation timing @@ -117,15 +117,15 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa |---|---|---|---|---|---|---| | CT1 | Calculate at script-bar close | **yes** — `on_native_bar` native_host.hpp:893, dispatched by `invoke_callback` native_execution_consumer.cpp:7222-7255 (call site `on_native_recalculate` native_execution_consumer.cpp:7247) | K | — | F:E1 O:F1 | — | | CT2 | Process orders on close (`close_execution` pine_adapter.cpp:2152-2153) | **yes** — `NativeCloseExecution::AfterCalculation` native_run_spec.hpp:35-38; `AfterCalculation` native_execution_consumer.cpp:7588-7592 | K | — | F:E2 O:F6 S:C1 | — | -| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15956) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7096-7100; `execute_current` legal native_host.hpp:1104 | K | — | F:E3 O:F2 | hazard → CT4 | +| CT3 | Pre-match hook at bar open (`on_bar_open` pine_adapter.cpp:15986) | **yes** — `on_native_bar_open` native_host.hpp:889 runs before the open match native_execution_consumer.cpp:7096-7100; `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:7039 with `engine.current_bar_ = open_view` native_execution_consumer.cpp:7090 (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:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663; `begin_coof_recalc` pine_adapter.cpp:5518, `flush_coof_tail` pine_adapter.cpp:16099, `end_coof_recalc` pine_adapter.cpp:5541) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6945, native_execution_consumer.cpp:6945-6975; commands are legal there `commands_allowed` native_execution_consumer.cpp:1407-1415; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5636-5670, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | +| CT6 | Calculate on order fills (scheduler `PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_recalculation_due` pine_scheduler_native.cpp:554, the first-open chain `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663; `begin_coof_recalc` pine_adapter.cpp:5548, `flush_coof_tail` pine_adapter.cpp:16129, `end_coof_recalc` pine_adapter.cpp:5571) | **partial** — `on_native_applied` native_host.hpp:938 fires mid-path after each fill native_execution_consumer.cpp:6945, native_execution_consumer.cpp:6945-6975; commands are legal there `commands_allowed` native_execution_consumer.cpp:1407-1415; a request born there is eligible on the unconsumed rest of the bar `born_on_remaining_path` native_execution_consumer.cpp:5636-5670, `remaining_path_delivery` native_order.cpp:2061-2069. Missing: calculation re-entry, a documented chronology, a bar-so-far view **Closed:** `NativeCalculationTrigger::BarCloseAndFills` native_run_spec.hpp:96 drives one `OrderFill` recalculation per applied execution at that event's own cursor, delivered as `on_native_recalculate(..., OrderFill, cause)` and bounded by `max_recalculations_per_point` (`tests/test_native_calc_timing.cpp`). | K+A | L5 | F:E4 O:F5 S:C2 | **Disagree:** F partial / O, S no; F, S v1 / O deferrable. R5-5: v1, sub-bar hook + chronology contract, adapter path unchanged. TV parts → CT11. | | CT7 | Calculate on every tick | **partial** — `on_native_tick` native_host.hpp:878 fires per accepted realtime print native_execution_consumer.cpp:7178-7182; 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:213), the Pine stream route refuses `calc_on_order_fills` pine_strategy_host.cpp:203-205. | | 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:7582-7701 **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:2281-2337) | **yes** — `IntrabarPath` native_run_spec.hpp:362-403, delivery `deliver_confirmed_script` native_execution_consumer.cpp:7475-7719; `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. | | CT10 | Batch → warmup → realtime forward execution | **yes** — `stream_begin` engine.hpp:1755, `stream_push_bar` engine.hpp:1761, `stream_end` engine.hpp:1765, `NativeRunPhase` native_host.hpp:41, C ABI `strategy_stream_begin` c_abi.cpp:613 | 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:152-238) 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:5687), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1638-1645 | 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:5717), two-fills-at-open rule, script-state rollback (`PineScheduler` pine_scheduler_native.cpp:52-104; hooks `snapshot_script_state` engine.hpp:1332-1333), the first-open guard `kFirstOpenLoopGuard` pine_scheduler_native.cpp:663, deferral queue of `PendingCoofRequest` pine_adapter.hpp:1638-1645 | 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:5698-5745, `first_region_entry` native_matching.hpp:229-257; kind `NativeCandidatePriceKind` native_execution_consumer.cpp:6293-6301; `Limit::fill_through` native_order.hpp:239-244 enforced where the matched price is limit-protected (`protect_limit` native_execution_consumer.cpp:6281-6291) and re-checked after host terms (`InvalidTerms` native_execution_consumer.cpp:5103-5121) | K | — | F:G3 O:H1 O:H2 O:H3 | — | -| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:584 applied once native_execution_consumer.cpp:4620-4621, clamped by `protect_limit` native_execution_consumer.cpp:6289 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11954-11963) is A | +| FP2 | Slippage in ticks, limit-clamped | **yes** — `slippage_ticks` native_run_spec.hpp:584 applied once native_execution_consumer.cpp:4620-4621, clamped by `protect_limit` native_execution_consumer.cpp:6289 | K+A | — | F:G1 O:H7 S:F2 | TV's round-then-slip order (`source_bar_fill` pine_adapter.cpp:11984-11993) is A | | FP3 | Commission: percent / cash per unit / cash per execution (`fee_kind_for` pine_adapter.cpp:388-395) | **yes** — `NativeFeeKind` native_run_spec.hpp:19-23; `quote_execution_commissions` engine_execution.cpp:1575-1611 | K | — | F:G2 O:H8 S:F1 | — | -| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:181-184, `source_bar_fill` pine_adapter.cpp:11970-11979, `directional_tick` pine_adapter.cpp:226-230) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4082; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6375-6376; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | +| FP4 | Booked fill quantized to the instrument tick, nearest or directional (`nearest_tick` pine_adapter.cpp:181-184, `source_bar_fill` pine_adapter.cpp:12000-12009, `directional_tick` pine_adapter.cpp:226-230) | **no** — `price_tick` is only a slippage multiplier native_execution_consumer.cpp:4082; `bar_fill_price` engine.hpp:771 is reachable only through `NativeCurrentPriceRule::NearestTick` native_execution_consumer.cpp:6375-6376; the reference helpers were dead (since deleted) **Closed:** `NativeRunSpec::price_grid` (`NativePriceGrid` native_run_spec.hpp:118) with `grid_rounding` (`NativeGridRounding` native_run_spec.hpp:128) rounds the fill basis onto `price_tick` before slippage, `HalfUp` or `Directional` (`tests/test_native_price_grid.cpp`). | K | L8 | O:H4 O:H6 F:G4 S:F3 | **Disagree:** O kernel / F, S "stays adapter, no promotion". R5-3: own kernel lane. | | FP5 | Trigger tested against the tick-quantized bar | **no** — the kernel tests raw prices against raw levels **Closed:** `NativePriceGrid::QuantizeFillsAndTriggers` additionally tests and re-validates every activation against the tick-quantized path, one rule for every trigger kind (`tests/test_native_price_grid.cpp`). | K+A | L8 | O:H5 F:G4 S:F3 | R5-3: the optional quantized trigger test is K; TV's exact half-tick rule (`source_trigger_threshold` pine_adapter.cpp:257-305) 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:8785). **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:264-312 (used `source_trigger_threshold` pine_adapter.cpp:9029-9034, `exit_limit_trigger` pine_adapter.cpp:9563-9584), `source_bar_fill_tick` pine_adapter.cpp:202-215, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:221-225, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13808-13814 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11679) | 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:264-312 (used `source_trigger_threshold` pine_adapter.cpp:9059-9064, `exit_limit_trigger` pine_adapter.cpp:9593-9614), `source_bar_fill_tick` pine_adapter.cpp:202-215, raw-vs-booked spelling `source_same_point` pine_adapter.cpp:221-225, margin-call fill pricing `source_margin_fill_price` pine_adapter.cpp:13838-13844 | n/a — already isolated behind the terms seam (adapter `resolve_terms` pine_adapter.cpp:11709) | A | — | F:G4 O:H5 S:F3 | R5-1, R5-3 | ### 1.8 Excursion, metrics, report, hash @@ -210,7 +210,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa | 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:1774 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:34-195 (live, e.g. `session_in_market`, which generated code reaches through `pine_session_ismarket` session_time.hpp:247), `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:19030-19059 | 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:19060-19089 | 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:528, 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:66) 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:308-585) 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:604. TradingView-specific toggling remains `source::PineStrategyHost::set_margin_call_enabled` pine_strategy_host.hpp:417; 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:1239 | 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) | @@ -324,7 +324,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:1730, `guard_native_mutation` engine_aux_security.cpp:78 | 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). No kernel TU includes a source header | `sha256:4c80b38f7193fcf8d72f5b10d04d2c468a21b6f771c682d8adc8dddcd99a73f2` engine_aux_security.cpp:6-13 | 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:370-376 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2024-2035; `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:370-376 and its twin `tv_money_round` pine_policy_support.hpp:9-15; `skip_entry_bar_high` engine.hpp:152, `skip_entry_bar_low` engine_state_hash.cpp:66, `declare_opened_lot_entry_bar_mask` engine_path_resolve.cpp:95; the ledger `close_logical_units_` pine_adapter.hpp:2027-2038; `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) | 58 runtime `PF_API` symbols: native configure, the 12-symbol stream family, pending-order views, broker-state hash, security feeds, FX curve, reports. The 58 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:13933f70987f6bfe37d3015ae065a8d5ee5937c37431d48e2ecd1f17a4c29958` c_abi.cpp:278-918, `sha256:4b7e398c78b276b34c4247e8271242b48aa195cea29126f4e809073f2665e1ea` check_c_abi_runtime.py:20-80, c_abi.cpp:851, `sha256:a9d7348a5a060e57c33fe9f537b9e7c21a18417c5d15569074bca299ea72f504` c_abi.cpp:579-698, `sha256:db54076d46a71efb0c7556c97494317e7fca443278b6963c3edf79ce918d72a1` c_abi.cpp:504-556, `sha256:f18d46ac323234f7e3082367d30840b704b8a7548f7ae95ee8cff8714f29fd50` c_abi.cpp:483-500, `sha256:dab6c5091fb32549abf866e5d71ed05f722ab4715defa4f0eca524c8f9238a2f` c_abi.cpp:791-822, c_abi.cpp:918 | 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 | @@ -522,11 +522,11 @@ line numbers are this tree's (683a82f); kernel semantics are | 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:2195); 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:13026-13027; 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:16685, `update_risk_state` pine_adapter.cpp:13661-13688): 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:13019), 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:1200-1208): +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:17858-17867); 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:16115); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16227), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14789-14798, `submit_intraday_loss_close` pine_adapter.cpp:14759-14787), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17039); 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:5891, `:7601`, `:8334`, `:8462`, `:11457`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:17938-17941) | `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:13652-13654); 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:13558-13579, `mday·100+month` in the CHART timezone) | `max_fills_per_day`: applied fills counted as they settle, evaluated at the three points; `FlattenAndBlock` at the drain's cursor; `SessionDay` / `CalendarDayInTimezone` on the SPEC timezone | (i) count + (ii) ticket + (iv) close price, charged-slot budget, placement scope, chart-day key | `MG13`: the second fill reaches the cap on a bar that closes above its open — adapter closes at that bar's HIGH (103) under its ticket, kernel at the close (101) under its own. `MG14`: chart timezone `America/New_York` over a UTC symbol — bar 4 (04:00 UTC) is a new chart day for the adapter and the same UTC day for the kernel. Corpus, kernel seeded with the declared cap: `cap-risk-gates-allow-max-intraday-01` identical 1464/1464 (the third fill of each day flattens the book, so block and latch coincide); `cap-max-intraday-filled-orders-isolate-01` 3840 of 3916 rows differ from row 77 (2025-04-08); `cap-gatekeeper-intraday-risk-01` 312 of 604 differ from row 293 (2025-10-25); `composite-bracket-cap-range-pending-stop-01` 2370 of 2384 differ from row 17 (2025-04-02 02:45), two extra rows | retained: the count alone is expressible, but the kernel's block would refuse fills the transfer admits and the close price is TV's; a report-only action would give the adapter a count it cannot use (charged slots ≠ applied fills) | +| `strategy.risk.max_position_size` | precommit refuses a flat/same-side ENTRY at its fill when the LIVE book already holds ≥ the limit (`max_position_size` pine_adapter.cpp:13056-13057; 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:16715, `update_risk_state` pine_adapter.cpp:13691-13718): 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:13049), 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:1200-1208): +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:17888-17897); 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:16145); checked at the bar open (`submit_intraday_loss_close(bar.open, context, true)` pine_adapter.cpp:16257), along the bar's PATH at its adverse extreme with a resting `Stop` flatten (`schedule_intraday_loss_path` pine_adapter.cpp:14819-14828, `submit_intraday_loss_close` pine_adapter.cpp:14789-14817), and at a closing fill with that fill's own P&L unbooked (`preclose_intraday_loss` pine_adapter.cpp:17069); 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:5921, `:7631`, `:8364`, `:8492`, `:11487`) and `cancel_all()` withdraws the working book (`cancel_all()` pine_adapter.cpp:17968-17971) | `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:13682-13684); 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:13588-13609, `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, @@ -945,7 +945,23 @@ this lane re-pins the affected v19 witnesses once. (`tests/test_pooc_reversing_stop_tapes.cpp`, chart and magnified; corpus `order-deferred-flip-pooc-cross-bar-01`: 57 of TradingView's 792 entries at its bar and price before, 734 a bar late and one trade missing; all 792 - trades exact after). That pass also never ran on an aggregated or magnified run: its + trades exact after). A stop entry the pass fills there can be the same id's + entry again, placed at the close of the bar whose bracket limit closed the + previous one, with a fresh `strategy.exit` of the same exit id. When that + limit was a retained leg the pre-script drain settled, the adapter reads its + terminal receipt only at the next open, and the sibling cancel it runs + (`cancel_bracket_siblings`: the working legs of that exit id, `from_entry` + and origin) took the fresh bracket too, so the re-entry never exited; since + R5 lane INT26 it skips a leg of a later `strategy.exit` call that the kernel + accepted after that fill. A leg of the filled leg's own call still goes, + however late it was accepted: the drain submits a bracket's legs one at a + time, executing each + (`tests/test_pooc_stop_reentry_bracket_tapes.cpp`, with a control; the gate + sweep's BTCUSDT probe, 820 of TradingView's 821 trades before, 821 after). Under the bar + magnifier the same script's short still differs, recorded in the row: the + drain leaves that retained limit to the flush below the body, whose book + still holds the short, so the re-entry is never placed. + That pass also never ran on an aggregated or magnified run: its quiet-bar gate tested the script-bar `interval_index` where the pass tests the input slot (`projection_bar_index`), so since K-IDX every such run skipped it, limit entries and exit legs included. @@ -1054,10 +1070,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:8746). Full-table scans + (`has_full_entry_bracket` pine_adapter.cpp:8776). Full-table scans rewrite rows as well (`suspend_brackets_for_reversal` pine_adapter.cpp:1529, `revive_brackets_after_margin` pine_adapter.cpp:1751, - `preserved_by_close_all` pine_adapter.cpp:11488), and + `preserved_by_close_all` pine_adapter.cpp:11518), 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. @@ -1521,10 +1537,10 @@ the rule gives 9.9 + 9.89). | # | Risk | Why it bites | Containment | Src | |---|---|---|---|---| | E1 | **Spec hashing.** `hash_spec` folds every field unconditionally (native_execution_consumer.cpp:234-256) | 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:1665); 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:13979); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14519 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:2441, the COOF `at_fill` exception pine_adapter.cpp:6633) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5687) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5641 and `drain_applied_notifications` (native_execution_consumer.cpp:6302-6315); 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:15956-15985) | 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:14009); its chronology lives at `schedule_margin_call_path` pine_adapter.cpp:14549 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:2441, the COOF `at_fill` exception pine_adapter.cpp:6663) | 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 (`PineScheduler::recalculate` pine_scheduler_native.cpp:579, `coof_next_waypoint` pine_adapter.cpp:5717) | Changing `born_on_remaining_path` or the notification order shifts adapter fills | Freeze `born_on_remaining_path` native_execution_consumer.cpp:5641 and `drain_applied_notifications` (native_execution_consumer.cpp:6302-6315); 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:15986-16015) | 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:1548) | 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:258) 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:46-78, *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:1213; 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 | @@ -1544,7 +1560,7 @@ the rule gives 9.9 + 9.89). | P5 | **Test asymmetry:** 430 adapter test files vs 14 native | native-only tests in every lane (§3.1 b) | O | | P6 | **Stream vs batch.** The stream refuses monthly input, mixed modes, in-session gaps, non-empty FX curves, security feeds and source setters (`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:298) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:585 → `set_risk_direction` pine_adapter.cpp:18463 (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:298) is the only live lifecycle (O); the `strategy.risk.allow_entry_in` setter chain beyond `set_pine_risk_direction` pine_strategy_host.cpp:585 → `set_risk_direction` pine_adapter.cpp:18493 (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) @@ -1569,7 +1585,7 @@ the rule gives 9.9 + 9.89). | **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:16121-16137) 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:2508) 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:16151-16167) 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:2508) 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. | @@ -1645,7 +1661,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:13269-13290). 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:2622), 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:13299-13320). 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:2622), 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:4684-4697 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). @@ -1755,7 +1771,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:13490-13582); 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:13520-13612); 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:4684-4697 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 e757f5eb..45f0da62 100644 --- a/docs/native-refactor-progress.md +++ b/docs/native-refactor-progress.md @@ -173,7 +173,7 @@ repository and were never pushed. | 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:268`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:5290`), which still runs about three times a bar | PERF-D2, spark 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 | Branch `perf-d2/proto-throwaway` (`92e85571`); `$EV/exec/PERF-D2-scratch/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:268`). PERF-L4 gated the read inside `observe_terminal_receipts` (`src/source/pine_adapter.cpp:5320`), which still runs about three times a bar | PERF-D2, spark 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 | Branch `perf-d2/proto-throwaway` (`92e85571`); `$EV/exec/PERF-D2-scratch/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, spark 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)" | Branch `perf-d3/proto-throwaway` (`a820b7df`, five commits on `b33ea659`); `$EV/exec/PERF-D3-scratch/patches/`; spark `~/perf-d3/` | 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:1029`) 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" | `$EV/exec/PERF-D3-scratch/patches/hold-ub-measurement-only.py` | As S1a. Size L, parity risk high: TradingView's margin money rules | @@ -183,7 +183,7 @@ repository and were never pushed. | 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:2197`) inside `native_order_v7`, so both are a layout and API change | L3, spark 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" | `r5/l3` (`b33ea659`, merged by INT22); `$EV/exec/L3-scratch/`; spark `~/l3/prof` | A `native_order` layout epoch | | A checked-values handoff for executions (L3) | `execution_values` runs twice, in `check_execution` (`src/native_order.cpp:4791`) and again in `apply_execution` (`src/native_order.cpp:4805`); handing the checked values across needs a public type, an API addition with no layout change | L3, as above: 2.21 % of bracket for the two together, about 1.1 % recoverable | L3 ruling 2: "a public checked-values handoff for executions (~1.1% bracket) -> DEFERRED" | As above | An owner decision on the API addition | | A chunked `trades_` store (L3) | Closed trades in chunks rather than one vector; `trades_` (`include/pineforge/engine.hpp:546`) is protected `BacktestEngine` state, so script ABI | L3, as above: on bracket, vector growth 0.64 %, `Trade` moves 0.74 % and `record_close_trade` 0.43 % (at most about 1.4 %); at most 0.35 % on the 14 probes | L3 ruling 4: "chunked trades_ store (ABI) -> DEFERRED" | As above | An `engine_script_run` epoch that changes the engine's protected layout | -| A zoned chart-day memo (D2-D finding 4) | On a chart with a timezone, `libc_chart_day_key` (`src/source/pine_adapter.cpp:13531`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:15956`) reads `chart_day_key` (`src/source/pine_adapter.cpp:16112`) 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:13515`) | Not measured on a zoned chart. The UTC path cost 134 instructions a bar before D2-D's memo and 68 after (D2-D, spark, 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:13561`) takes the process timezone lock through `ScopedTimezone` and calls `localtime_r` on every bar, because `on_bar_open` (`src/source/pine_adapter.cpp:15986`) reads `chart_day_key` (`src/source/pine_adapter.cpp:16142`) 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:13545`) | Not measured on a zoned chart. The UTC path cost 134 instructions a bar before D2-D's memo and 68 after (D2-D, spark, 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:443`). 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:559`), 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:248`) reads two thread-locals per call on a zoned clock, and the prepared order storage's `free_blocks` (`src/native_order.cpp:605`) one per prepared mutation. `ema_na_warmup_flag` is read once per EMA, at its first compute | One thread-local access per reader call. AUDIT4, macOS arm64, a dlopen'd tutorial-MACD probe, counted by `_tlv_get_addr` breakpoints and by descriptor patching: 1.000 a bar for one `ta::Highest` and for one default `ta::VWAP`, 1.999 for one `pine_hour` on a New York clock, 0.000 on the no-order floor and with orders. On Linux a TLS-descriptor call costs about 20 instructions: D2-C's 13 → 4 calls a bar was 260 → 80 instructions (spark 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 | `$EV/exec/AUDIT4-opus-scratch/perf/tls/` | 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 | @@ -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:5290`) returns at its receipt gate when no +(`src/source/pine_adapter.cpp:5320`) returns at its receipt gate when no command lies above its cursor. `every_gate_is_taken_both_ways` (`tests/test_adapter_quiet_bar.cpp:1036`) 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:1920-1921`), with their type + (`include/pineforge/source/pine_adapter.hpp:1923-1924`), 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/contributing-llm.md b/docs/pages/contributing-llm.md index afd2142f..dcd566ba 100644 --- a/docs/pages/contributing-llm.md +++ b/docs/pages/contributing-llm.md @@ -118,7 +118,7 @@ finding to report, not a step to take. 10. **A test row never silently disappears.** Each profile counts the rows that *ran* against a floor: `KERNEL_MIN_TESTS` ci_verify.py:260 and - `RELEASE_MIN_TESTS` ci_verify.py:390. Adding rows means raising the floor + `RELEASE_MIN_TESTS` ci_verify.py:393. Adding rows means raising the floor in the same commit. ## The recipe for a lane @@ -264,7 +264,7 @@ measurement that produced it, so a later change to it is visible as a change to the record, not as an edit to a literal. **floor** — the minimum number of CTest rows a profile must actually run -(`KERNEL_MIN_TESTS` ci_verify.py:260, `RELEASE_MIN_TESTS` ci_verify.py:390). It +(`KERNEL_MIN_TESTS` ci_verify.py:260, `RELEASE_MIN_TESTS` ci_verify.py:393). It counts rows that ran, so a skipped row does not pad it. **receipt** — the recorded evidence an ABI-comparison row needs (a prepared diff --git a/docs/pages/native-engine.md b/docs/pages/native-engine.md index d2e0efff..dfad6cac 100644 --- a/docs/pages/native-engine.md +++ b/docs/pages/native-engine.md @@ -1484,7 +1484,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:13868-13901`), so the adapter admits +(`margin_check_allowed` `pine_adapter.cpp:13898-13931`), 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 476b43a6..0cbc8372 100644 --- a/include/pineforge/source/pine_adapter.hpp +++ b/include/pineforge/source/pine_adapter.hpp @@ -1802,7 +1802,10 @@ class PineExecutionAdapter { // Whether an immediate strategy.close was placed on that bar. bool immediate_close_placed_on(std::int32_t bar) const noexcept; void cancel_bracket_origin(native_order::RequestHandle); - void cancel_bracket_siblings(native_order::RequestHandle); + // executed_at: the ordinal a terminal receipt executed at, when the + // observer reads it later; a later strategy.exit call's leg accepted + // after it is not a sibling. + void cancel_bracket_siblings(native_order::RequestHandle, std::uint64_t executed_at = 0); void cancel_exit_orders_for_full_close(const SourceId& from_entry); void retire_in_position_exits_at_flat(bool preserve_pending_parents, bool dormant_rows_only, diff --git a/scripts/ci_verify.py b/scripts/ci_verify.py index 31fc254b..d38a8168 100644 --- a/scripts/ci_verify.py +++ b/scripts/ci_verify.py @@ -387,7 +387,10 @@ # PAR-MARGIN's own count also held the margin differential, which that lane # had cherry-picked for its pins; it counts once, as H-MEASURE's. No release # row skips, so 702 registered is 702 run. -RELEASE_MIN_TESTS = 702 +# 703 = those 702 plus INT26 round 2's source-bound tape row, +# test_pooc_stop_reentry_bracket_tapes (the gate sweep's lost re-entry +# bracket); 703 registered is 703 run. +RELEASE_MIN_TESTS = 703 # ADR-0001 ruled-count floors, beside the ctest floors (R5 lane H-DOCGATES, # AUDIT4-opus X13 / docs-a N7). check_kernel_residuals.py counts the rulings # its vocabulary reads -- 174 identifiers and 45 texts on the lane's tree: the @@ -411,9 +414,10 @@ # KERNEL-EDGE +1, K-ULP4 +2, K-ULP5 +1, DOC-TRUTH-4 +1) in each; 683/683/692 # at INT26, each with wave H's twenty-four rows of RELEASE_MIN_TESTS # (H-MEASURE +12, H-DOCGATES +3, K-OCA-KEEP +1, PAR-ORDERS-2 +4, PAR-MARGIN -# +1, PAR-MARGIN-2 +1, PERF-KEDGE +1, PAR-CASHFEE +1), counted the same way. +# +1, PAR-MARGIN-2 +1, PERF-KEDGE +1, PAR-CASHFEE +1), counted the same way; +# 684/684/693 with INT26 round 2's tape row. # An excluded run must still discover at least this many rows before -LE. -EXCLUDED_REGISTERED_MIN = {'debug': 683, 'sanitizers': 683, 'native': 692} +EXCLUDED_REGISTERED_MIN = {'debug': 684, 'sanitizers': 684, 'native': 693} # The ctest stage's bound. A full sanitizers run (push to main, a manual # dispatch, the maintainers' verification) ran out of its 30 minutes twice on # main's four-core runner before every row had finished, so it gets an hour; a diff --git a/scripts/test_ci_verify.py b/scripts/test_ci_verify.py index fc74e49f..3274257d 100644 --- a/scripts/test_ci_verify.py +++ b/scripts/test_ci_verify.py @@ -1331,7 +1331,7 @@ def test_ctest_label_exclusion_is_forwarded(self): def test_pr_exclusion_proves_registered_minus_labelled_equals_ran(self): self.assertEqual(ci_verify.EXCLUDED_REGISTERED_MIN, - {'debug': 683, 'sanitizers': 683, 'native': 692}) + {'debug': 684, 'sanitizers': 684, 'native': 693}) for profile, registered in ci_verify.EXCLUDED_REGISTERED_MIN.items(): with self.subTest(profile=profile): code, summary, scripted, _ = self.run_profile( diff --git a/src/source/pine_adapter.cpp b/src/source/pine_adapter.cpp index 471636f7..e60aef41 100644 --- a/src/source/pine_adapter.cpp +++ b/src/source/pine_adapter.cpp @@ -5064,7 +5064,8 @@ void PineExecutionAdapter::cancel_bracket_origin(native_order::RequestHandle ori } } -void PineExecutionAdapter::cancel_bracket_siblings(native_order::RequestHandle handle) { +void PineExecutionAdapter::cancel_bracket_siblings(native_order::RequestHandle handle, + std::uint64_t executed_at) { std::optional snapshot_copy; if (const auto source = placement_.find(handle.incarnation); source != placement_.end()) { @@ -5087,6 +5088,35 @@ void PineExecutionAdapter::cancel_bracket_siblings(native_order::RequestHandle h matches.push_back(candidate); } } + // R5 lane INT26: a terminal receipt is observed at the adapter's next + // read, not when its leg executed. A bracket leg the pre-script drain + // settles on its parent's touch bar is read at the next bar open, after + // that bar's source body and its process_orders_on_close pass; by then a + // same-bar re-entry the close already reached has opened (PAR-ORDERS, + // H-MEASURE 6b) and its own strategy.exit legs of the same id are + // working. A leg of a later strategy.exit call (another command_sequence) + // that the kernel accepted after the execution is that later order, not a + // sibling of the leg that filled, and it stays. A leg of the filled leg's + // own call is its sibling however late it was accepted -- the drain + // submits a bracket's legs one at a time, executing each -- and goes. + const auto later_call = [&](const native_order::RequestHandle& sibling) { + const auto placement = placement_.find(sibling.incarnation); + return placement != placement_.end() + && placement->second.command_sequence != snapshot.command_sequence; + }; + if (executed_at != 0 && std::any_of(matches.begin(), matches.end(), later_call)) { + const auto working = require_host().native_working_requests(); + matches.erase(std::remove_if(matches.begin(), matches.end(), + [&](const native_order::RequestHandle& sibling) { + if (!later_call(sibling)) return false; + const auto row = std::find_if(working.begin(), working.end(), + [&](const NativeWorkingRequest& request) { + return request.definition->handle == sibling; + }); + return row != working.end() + && row->definition->birth.acceptance_ordinal > executed_at; + }), matches.end()); + } for (const auto& sibling : matches) { const auto result = require_host().cancel(sibling); if (result.status == native_order::CancelStatus::Cancelled) retire(sibling); @@ -5378,7 +5408,7 @@ void PineExecutionAdapter::observe_terminal_receipts() { } } } else if constexpr (std::is_same_v) { - if (event.terminal) cancel_bracket_siblings(event.handle()); + if (event.terminal) cancel_bracket_siblings(event.handle(), event.ordinal); } }, command); }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5a5e37bd..15040a4e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1395,6 +1395,12 @@ list(APPEND TEST_SOURCES test_pooc_reversing_stop_tapes) # `lab tv` tapes under tests/fixtures/cash_fee_sizing (README.md there names # them) replayed on their bars through the Pine adapter. Source-bound. list(APPEND TEST_SOURCES test_cash_fee_sizing_tapes) +# R5 lane INT26 (round 2): a process_orders_on_close stop re-entry its close +# reached keeps its fresh bracket when the previous bracket's same-bar limit is +# observed at the next open -- the `lab tv` tapes under +# tests/fixtures/pooc_stop_reentry_bracket (README.md there names them) replayed +# through the Pine adapter. Source-bound. +list(APPEND TEST_SOURCES test_pooc_stop_reentry_bracket_tapes) # R4-D L4b-fix restores every removed ShortSeed, affordability, sizing and # deferred-close oracle as an individually compiled native-route twin. Keep @@ -2156,6 +2162,11 @@ if(TARGET test_cash_fee_sizing_tapes) target_compile_definitions(test_cash_fee_sizing_tapes PRIVATE PINEFORGE_CASH_FEE_FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/fixtures/cash_fee_sizing") endif() +# R5 lane INT26 (round 2) reads its TradingView tapes from the fixture tree too. +if(TARGET test_pooc_stop_reentry_bracket_tapes) + target_compile_definitions(test_pooc_stop_reentry_bracket_tapes PRIVATE + PINEFORGE_INT26_REENTRY_FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/fixtures/pooc_stop_reentry_bracket") +endif() # R5 E21 reads the kernel's own sources to prove BacktestEngine::marked_equity_at # is the one implementation of the marked equity. Kernel files only, so the diff --git a/tests/fixtures/pooc_stop_reentry_bracket/README.md b/tests/fixtures/pooc_stop_reentry_bracket/README.md new file mode 100644 index 00000000..4c8c423d --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/README.md @@ -0,0 +1,34 @@ +# process_orders_on_close stop re-entry bracket tapes (R5 lane INT26, round 2) + +TradingView's own trades for the shape INT26's gate sweep lost a row on +(scrapper:data-BINANCE-BTCUSDT/standard/vasudevshenoy-manoj-betrayed-me, BTCUSDT +15m, 2025-08-03 10:00 UTC), as a synthetic script: under +`process_orders_on_close`, at each of three flat signal closes the script cancels +its pending stop entry and places it again with its `strategy.exit` bracket +(limit 3c beyond the close, stop 10c the other way) -- first 10c beyond the close +(unreached), then 1c beyond it (the next bar fills the entry and then the +bracket's limit, and closes flat), then at that close (reached: it fills there), +with a fresh bracket of the same exit id whose limit a later bar reaches. Long on +bars 39-41 (the limit on bar 42), short on bars 44-46 (the limit on bar 49). +`int26-reentry-noexit-chart` is the control: the same script, but the short's +re-entry on bar 46 places no `strategy.exit`, so its row 4 is still open when the +data ends (its exit signal empty) and no bracket leg is left to close it. +`tests/test_pooc_stop_reentry_bracket_tapes.cpp` replays each tape through the +Pine adapter on the 15m chart (`-chart`) or the 1m bars aggregated to 15m under +the magnifier (`-mag`; its row 4 is a recorded divergence, see the test). + +Each directory is one `lab tv` export (pineforge-workflow, channel `ws-report-v1`, +`rangeProof: covered`, NYSE:F 15, window 2025-07-01 .. 2025-07-08, `--no-note`), +byte-identical: `strategy.pine`, `tv_trades.csv` (times UTC+8), `meta.json`, +`metrics.json`. The scripts count chart bars from 2025-07-02 09:30 ET, the first +bar of `tests/fixtures/session_islastbar/bars.inc`, whose 15m and 1m bars the rows +replay. + +| probe | variant | trades | tvTradesSha256 | +|---|---|---|---| +| `int26-reentry-bracket-chart` | chart | 4 | `e0447b533954985426b7fa0d616c4d8455179d71ed206272e3212b1b4c1b6061` | +| `int26-reentry-bracket-mag` | bar magnifier on | 4 | `b7e89ce188ae1a3e67766b1e9093d9e207a8c618df4aaa91e66d48ca605d53e6` | +| `int26-reentry-noexit-chart` | chart, the short's re-entry without `strategy.exit` | 4 (the last open) | `efbd25e3c3f4f32101dd7524b551f898818a40a8c470128ee47c853d188e9039` | + +Every row of the two bracket tapes is closed and the book ends flat; the control +ends short 100. diff --git a/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/meta.json b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/meta.json new file mode 100644 index 00000000..0b0bf5fe --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/meta.json @@ -0,0 +1,16 @@ +{ + "title": "INT26 reentry-bracket-chart", + "slug": "int26-reentry-bracket-chart", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T09:28:50.677Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "86021c31ddaa24944c88ea41a5020d8b4e6583a32305f4139f50bdd9f0cb85ea", + "tv_trades_sha256": "e0447b533954985426b7fa0d616c4d8455179d71ed206272e3212b1b4c1b6061", + "tv_trades_rows": 8 +} diff --git a/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/metrics.json b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/metrics.json new file mode 100644 index 00000000..0f1f897d --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "int26-reentry-bracket-chart", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 4, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "86021c31ddaa24944c88ea41a5020d8b4e6583a32305f4139f50bdd9f0cb85ea", + "tvTradesCsvHash": "e0447b533954985426b7fa0d616c4d8455179d71ed206272e3212b1b4c1b6061", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.88415", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "fba360e19107f05ccd84ffc3eb94cbdc7a400efce137137677ba32b476b79756", + "decodedReportSha256": "f9567d2c0d050f3b5bdc0538bdae47a6b731e63e3ac6a1a998a57a606a3d13bc", + "closedTrades": 4, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T09:28:50.676Z" +} diff --git a/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/strategy.pine b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/strategy.pine new file mode 100644 index 00000000..75a81663 --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/strategy.pine @@ -0,0 +1,39 @@ +//@version=6 +// INT26 (R5 lane INT26, round 2): the gate sweep's leaver shape, synthetic. +// Under process_orders_on_close, at each of three signal closes on a flat book +// the script cancels its pending stop entry and places it again with its +// strategy.exit bracket (limit 3c beyond the close, stop 10c the other way): +// first 10c beyond the close (unreached), then 1c beyond it -- the next bar +// fills the entry and then the bracket's limit, and closes flat -- then AT +// that close (reached: it fills there), with a fresh bracket of the same exit +// id whose limit a later bar reaches. Long on bars 39-41 (the limit on bar +// 42), short on bars 44-46 (the limit on bar 49). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("INT26 reentry-bracket-chart", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=0, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +t = math.floor(close * 100 + 0.000001) +if time < tEnd and strategy.position_size == 0 + if k >= 39 and k <= 41 + strategy.cancel("L") + strategy.entry("L", strategy.long, stop=(k == 39 ? t + 10 : k == 40 ? t + 1 : t) / 100) + strategy.exit("LX", "L", stop=(t - 10) / 100, limit=(t + 3) / 100) + if k >= 44 and k <= 46 + strategy.cancel("S") + strategy.entry("S", strategy.short, stop=(k == 44 ? t - 10 : k == 45 ? t - 1 : t) / 100) + strategy.exit("SX", "S", stop=(t + 10) / 100, limit=(t - 3) / 100) +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/tv_trades.csv b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/tv_trades.csv new file mode 100644 index 00000000..0af571ac --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-chart/tv_trades.csv @@ -0,0 +1,9 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-07 21:45,LX,11.71,100,1169,1.766,0.15,0.234,1.8831,0.16,0,0.00,1.766,0.00,0 +1,Entry long,2025-07-07 21:45,L,11.69,100,1169,1.766,0.15,0.234,1.8831,0.16,0,0.00,1.766,0.00,0 +2,Exit long,2025-07-07 22:00,LX,11.75,100,1172,2.7653,0.24,0.2347,2.8828,0.25,-0.1172,-0.01,4.5313,0.00,1 +2,Entry long,2025-07-07 21:45,L,11.72,100,1172,2.7653,0.24,0.2347,2.8828,0.25,-0.1172,-0.01,4.5313,0.00,1 +3,Exit short,2025-07-07 23:00,SX,11.63,100,1165,1.7672,0.15,0.2328,1.8835,0.16,0,0.00,6.2985,0.01,0 +3,Entry short,2025-07-07 23:00,S,11.65,100,1165,1.7672,0.15,0.2328,1.8835,0.16,0,0.00,6.2985,0.01,0 +4,Exit short,2025-07-07 23:45,SX,11.59,100,1162,2.7679,0.24,0.2321,2.8838,0.25,-2.1162,-0.18,9.0664,0.01,3 +4,Entry short,2025-07-07 23:00,S,11.62,100,1162,2.7679,0.24,0.2321,2.8838,0.25,-2.1162,-0.18,9.0664,0.01,3 \ No newline at end of file diff --git a/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/meta.json b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/meta.json new file mode 100644 index 00000000..3a430830 --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/meta.json @@ -0,0 +1,16 @@ +{ + "title": "INT26 reentry-bracket-mag", + "slug": "int26-reentry-bracket-mag", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T09:28:53.937Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "a1360887984801155810291e1137a2240dfae99fb32dfe3d4e0a86029b2da7e4", + "tv_trades_sha256": "b7e89ce188ae1a3e67766b1e9093d9e207a8c618df4aaa91e66d48ca605d53e6", + "tv_trades_rows": 8 +} diff --git a/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/metrics.json b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/metrics.json new file mode 100644 index 00000000..9130ed01 --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "int26-reentry-bracket-mag", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 4, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "a1360887984801155810291e1137a2240dfae99fb32dfe3d4e0a86029b2da7e4", + "tvTradesCsvHash": "b7e89ce188ae1a3e67766b1e9093d9e207a8c618df4aaa91e66d48ca605d53e6", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.88416", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "4b306ea700c531464362737f9eb8d1484568eaeafe3c81ed7c5bef1b2aa0cc02", + "decodedReportSha256": "3fad0f521cefbcfe0a01a80ec28937e5b8ebe057faaa63a61ec067a5fd60cd07", + "closedTrades": 4, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T09:28:53.936Z" +} diff --git a/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/strategy.pine b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/strategy.pine new file mode 100644 index 00000000..6e8ace60 --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/strategy.pine @@ -0,0 +1,39 @@ +//@version=6 +// INT26 (R5 lane INT26, round 2): the gate sweep's leaver shape, synthetic. +// Under process_orders_on_close, at each of three signal closes on a flat book +// the script cancels its pending stop entry and places it again with its +// strategy.exit bracket (limit 3c beyond the close, stop 10c the other way): +// first 10c beyond the close (unreached), then 1c beyond it -- the next bar +// fills the entry and then the bracket's limit, and closes flat -- then AT +// that close (reached: it fills there), with a fresh bracket of the same exit +// id whose limit a later bar reaches. Long on bars 39-41 (the limit on bar +// 42), short on bars 44-46 (the limit on bar 49). +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("INT26 reentry-bracket-mag", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=0, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=true) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +t = math.floor(close * 100 + 0.000001) +if time < tEnd and strategy.position_size == 0 + if k >= 39 and k <= 41 + strategy.cancel("L") + strategy.entry("L", strategy.long, stop=(k == 39 ? t + 10 : k == 40 ? t + 1 : t) / 100) + strategy.exit("LX", "L", stop=(t - 10) / 100, limit=(t + 3) / 100) + if k >= 44 and k <= 46 + strategy.cancel("S") + strategy.entry("S", strategy.short, stop=(k == 44 ? t - 10 : k == 45 ? t - 1 : t) / 100) + strategy.exit("SX", "S", stop=(t + 10) / 100, limit=(t - 3) / 100) +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/tv_trades.csv b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/tv_trades.csv new file mode 100644 index 00000000..5ff4cda4 --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-bracket-mag/tv_trades.csv @@ -0,0 +1,9 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-07 21:45,LX,11.71,100,1169,1.766,0.15,0.234,1.8831,0.16,-2.1169,-0.18,1.766,0.00,0 +1,Entry long,2025-07-07 21:45,L,11.69,100,1169,1.766,0.15,0.234,1.8831,0.16,-2.1169,-0.18,1.766,0.00,0 +2,Exit long,2025-07-07 22:00,LX,11.75,100,1172,2.7653,0.24,0.2347,2.8828,0.25,-0.1172,-0.01,4.5313,0.00,1 +2,Entry long,2025-07-07 21:45,L,11.72,100,1172,2.7653,0.24,0.2347,2.8828,0.25,-0.1172,-0.01,4.5313,0.00,1 +3,Exit short,2025-07-07 23:00,SX,11.63,100,1165,1.7672,0.15,0.2328,1.8835,0.16,-1.1165,-0.10,6.2985,0.01,0 +3,Entry short,2025-07-07 23:00,S,11.65,100,1165,1.7672,0.15,0.2328,1.8835,0.16,-1.1165,-0.10,6.2985,0.01,0 +4,Exit short,2025-07-07 23:45,SX,11.59,100,1162,2.7679,0.24,0.2321,2.8838,0.25,-2.1162,-0.18,9.0664,0.01,3 +4,Entry short,2025-07-07 23:00,S,11.62,100,1162,2.7679,0.24,0.2321,2.8838,0.25,-2.1162,-0.18,9.0664,0.01,3 \ No newline at end of file diff --git a/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/meta.json b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/meta.json new file mode 100644 index 00000000..7623b572 --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/meta.json @@ -0,0 +1,16 @@ +{ + "title": "INT26 reentry-noexit-chart", + "slug": "int26-reentry-noexit-chart", + "source": "pineforge-workflow campaign lab tv", + "channel": "ws-report-v1", + "exportedAt": "2026-09-26T10:49:51.823Z", + "chart": { + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08" + }, + "pine_sha256": "e40641288665033de22586d72afb610bd6aca2138962645617a5a5b4712356e7", + "tv_trades_sha256": "efbd25e3c3f4f32101dd7524b551f898818a40a8c470128ee47c853d188e9039", + "tv_trades_rows": 8 +} diff --git a/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/metrics.json b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/metrics.json new file mode 100644 index 00000000..5cb09ceb --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/metrics.json @@ -0,0 +1,40 @@ +{ + "validationEligible": true, + "strategy": "int26-reentry-noexit-chart", + "symbol": "NYSE:F", + "interval": "15", + "from": "2025-07-01", + "to": "2025-07-08", + "deepBacktesting": true, + "trades": 4, + "tvTradesCsv": "tv_trades.csv", + "tapeChannel": "ws-report-v1", + "sourceCanonicalization": "pine-facade-v1", + "sourceArtifactHash": "e40641288665033de22586d72afb610bd6aca2138962645617a5a5b4712356e7", + "tvTradesCsvHash": "efbd25e3c3f4f32101dd7524b551f898818a40a8c470128ee47c853d188e9039", + "wsProvenance": { + "schemaVersion": 1, + "wsEndpoint": "wss://history-data.tradingview.com/socket.io/websocket?from=chart%2F&type=chart&auth=sessionid", + "authUser": "luisleo52655", + "accountType": "pro_premium_expert", + "pineId": "USER;3c3ee73838da415d9f6fb36ccd7cbe97", + "pineVersion": "0.88604", + "symbol": "NYSE:F", + "interval": "15", + "requestedRange": { + "from": 1751328000000, + "to": 1751932800000 + }, + "returnedRange": { + "from": 1751376600000, + "to": 1751917500000 + }, + "rangeProof": "covered", + "rawFrameSha256": "b1f976143ccc0b52e888b160ef0eeec13f61e4156f30029adb8921fa9da15801", + "decodedReportSha256": "2cb439f21b960f8c00da1ae18f34510232afb2ab10107fb8f604b2b3edd565fc", + "closedTrades": 4, + "proves": "which compile bytecode and which from/to window were sent to TradingView, and the exact trades TradingView returned for it (re-derivable from rawFrameSha256)", + "doesNotProve": "chart activation or the displayed applied range (browser-only); CSV byte identity and priceExact are lab renderings, screen-only until a parity study" + }, + "backtestedAt": "2026-09-26T10:49:51.823Z" +} diff --git a/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/strategy.pine b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/strategy.pine new file mode 100644 index 00000000..87617ec5 --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/strategy.pine @@ -0,0 +1,42 @@ +//@version=6 +// INT26 (R5 lane INT26, round 2): the control of the re-entry bracket tapes. +// The same script as int26-reentry-bracket-chart -- under +// process_orders_on_close, at each of three flat signal closes it cancels its +// pending stop entry and places it again with a strategy.exit bracket (limit +// 3c beyond the close, stop 10c the other way): 10c beyond the close +// (unreached), then 1c beyond it (the next bar fills the entry and then the +// bracket's limit, and closes flat), then AT that close (reached: it fills +// there) -- except that the short's re-entry on bar 46 places no +// strategy.exit: no order is left to close it, and the filled bracket's stop +// is gone with its limit. Long on bars 39-41 (the limit on bar 42), short on +// bars 44-46. +// NYSE:F 15m, the three replayed days of tests/fixtures/session_islastbar +// (2025-07-02, 07-03 half day, 07-07); k counts chart bars from 2025-07-02 +// 09:30 ET. No order is placed after 07-07 15:45 ET; whatever is open then is +// closed on the next bar, outside the replay. +strategy("INT26 reentry-noexit-chart", overlay=true, + initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100, + pyramiding=0, commission_type=strategy.commission.percent, commission_value=0.01, + slippage=0, process_orders_on_close=true, calc_on_order_fills=false, + margin_long=100, margin_short=100, use_bar_magnifier=false) + +t0 = timestamp("America/New_York", 2025, 7, 2, 9, 30) +tEnd = timestamp("America/New_York", 2025, 7, 7, 16, 0) +var int b0 = na +if na(b0) and time >= t0 + b0 := bar_index +k = na(b0) ? -1 : bar_index - b0 +t = math.floor(close * 100 + 0.000001) +if time < tEnd and strategy.position_size == 0 + if k >= 39 and k <= 41 + strategy.cancel("L") + strategy.entry("L", strategy.long, stop=(k == 39 ? t + 10 : k == 40 ? t + 1 : t) / 100) + strategy.exit("LX", "L", stop=(t - 10) / 100, limit=(t + 3) / 100) + if k >= 44 and k <= 46 + strategy.cancel("S") + strategy.entry("S", strategy.short, stop=(k == 44 ? t - 10 : k == 45 ? t - 1 : t) / 100) + if k < 46 + strategy.exit("SX", "S", stop=(t + 10) / 100, limit=(t - 3) / 100) +if time >= tEnd and strategy.position_size != 0 + strategy.cancel_all() + strategy.close_all(comment="after-window") diff --git a/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/tv_trades.csv b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/tv_trades.csv new file mode 100644 index 00000000..ff33d6b8 --- /dev/null +++ b/tests/fixtures/pooc_stop_reentry_bracket/int26-reentry-noexit-chart/tv_trades.csv @@ -0,0 +1,9 @@ +Trade number,Type,Date and time,Signal,Price USD,Size (qty),Size (value),Net PnL USD,Return %,Commission USD,Favorable excursion USD,Favorable excursion %,Adverse excursion USD,Adverse excursion %,Cumulative PnL USD,Cumulative PnL %,Duration (bars) +1,Exit long,2025-07-07 21:45,LX,11.71,100,1169,1.766,0.15,0.234,1.8831,0.16,0,0.00,1.766,0.00,0 +1,Entry long,2025-07-07 21:45,L,11.69,100,1169,1.766,0.15,0.234,1.8831,0.16,0,0.00,1.766,0.00,0 +2,Exit long,2025-07-07 22:00,LX,11.75,100,1172,2.7653,0.24,0.2347,2.8828,0.25,-0.1172,-0.01,4.5313,0.00,1 +2,Entry long,2025-07-07 21:45,L,11.72,100,1172,2.7653,0.24,0.2347,2.8828,0.25,-0.1172,-0.01,4.5313,0.00,1 +3,Exit short,2025-07-07 23:00,SX,11.63,100,1165,1.7672,0.15,0.2328,1.8835,0.16,0,0.00,6.2985,0.01,0 +3,Entry short,2025-07-07 23:00,S,11.65,100,1165,1.7672,0.15,0.2328,1.8835,0.16,0,0.00,6.2985,0.01,0 +4,Exit short,2025-07-08 03:45,,11.6,100,1162,1.7678,0.15,0.1162,12.8838,1.11,-2.1162,-0.18,7.9501,0.01,19 +4,Entry short,2025-07-07 23:00,S,11.62,100,1162,1.7678,0.15,0.1162,12.8838,1.11,-2.1162,-0.18,7.9501,0.01,19 \ No newline at end of file diff --git a/tests/test_pooc_stop_reentry_bracket_tapes.cpp b/tests/test_pooc_stop_reentry_bracket_tapes.cpp new file mode 100644 index 00000000..565a645f --- /dev/null +++ b/tests/test_pooc_stop_reentry_bracket_tapes.cpp @@ -0,0 +1,346 @@ +/* + * test_pooc_stop_reentry_bracket_tapes.cpp -- R5 lane INT26 (round 2). + * + * Under process_orders_on_close, a stop entry the script cancels and places + * again with its strategy.exit bracket at each flat signal close fills on the + * next bar, and so does the bracket's limit; that bar closes flat, and its + * close places the entry again, its stop AT the close, which has reached it, + * with a fresh bracket of the same exit id. R5 lane PAR-ORDERS (H-MEASURE + * Finding 6b) fills that re-entry at the close, as TradingView does, so the + * fresh bracket is working when the next bar opens. The first bracket's limit + * was a retained leg the pre-script drain settled after that bar's receipt + * read; the adapter observed its terminal receipt only at the next bar's open, + * and its sibling cancel -- every working leg of the same exit id, from_entry + * and origin -- withdrew the fresh bracket too, so the re-entry never exited. + * That is how INT26's gate sweep lost trade 275 of + * scrapper:data-BINANCE-BTCUSDT/standard/vasudevshenoy-manoj-betrayed-me + * (BTCUSDT 15m, 2025-08-03 10:00 UTC): 820 rows against TradingView's 821. + * + * The shape, synthetic, taped on NYSE:F 15m over the three days of + * tests/fixtures/session_islastbar (`lab tv`, ws-report-v1, rangeProof covered; + * tests/fixtures/pooc_stop_reentry_bracket, whose README names each tape): long + * on bars 39-41 (the fresh bracket's limit on bar 42), short on bars 44-46 (the + * limit on bar 49). TradingView books the same four rows on the chart and under + * the bar magnifier. Every row is compared field by field -- side, instants, + * prices, quantity, signals, bar indices -- and the book the replay ends on, + * its working bracket legs included. + * + * int26-reentry-bracket-chart replayed on the 15m chart: all four rows, and + * a flat book. The short is the leaver's shape; the long, + * whose first limit is read before the body, is a control + * the fix leaves alone. + * int26-reentry-bracket-mag replayed on the 1m bars aggregated to 15m + * under the magnifier: rows 1-3. Row 4 is a RECORDED + * divergence, asserted, so a fix flips it deliberately: + * there the drain leaves the short's retained limit to the + * flush below the body, the body still reads a short book + * at bar 46's close, and the re-entry is never placed (3 + * rows against TradingView's 4, both books flat). It is the + * same with or without this lane's fix. + * int26-reentry-noexit-chart the control, on the chart: the same script + * but the short's re-entry places no strategy.exit, so it + * is still open when the data ends, as TradingView's row 4 + * is. The drain submits the short bracket's legs one at a + * time, executing each: its stop is accepted after its + * limit filled, and it is still that filled order's leg -- + * the late read withdraws it, and no bracket leg is left + * working. + * + * Fail-before, this TU against the lane's previous head ebe67484 (2 of 27 + * checks fail): on the chart the short re-entry loses its fresh bracket at bar + * 47's open and is still open when the data ends -- 3 closed rows against + * TradingView's 4, short 100 against flat. Against a rule that kept every leg + * accepted after the fill (1 of 28 fail): the control's stop stays working + * under the re-entry that has no bracket of its own. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pineforge; + +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + std::printf(" FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \ + ++tests_failed; \ + } else { \ + ++tests_passed; \ + } \ + } while (0) + +#ifndef PINEFORGE_INT26_REENTRY_FIXTURE_DIR +#error "PINEFORGE_INT26_REENTRY_FIXTURE_DIR must name tests/fixtures/pooc_stop_reentry_bracket" +#endif + +namespace { + +struct FeedBar { + std::int64_t ts; + double open, high, low, close, volume; +}; + +#include "fixtures/session_islastbar/bars.inc" + +constexpr std::int64_t kMinute = 60'000; +const double kNaN = std::numeric_limits::quiet_NaN(); + +std::int64_t days_from_civil(int y, unsigned m, unsigned d) { + y -= m <= 2; + const int era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +// "YYYY-MM-DD HH:MM" in the tape's UTC+8 -> UTC milliseconds. +std::int64_t tape_ms(const std::string& text) { + int y = 0, mo = 0, d = 0, h = 0, mi = 0; + if (std::sscanf(text.c_str(), "%d-%d-%d %d:%d", &y, &mo, &d, &h, &mi) != 5) return -1; + const std::int64_t days = days_from_civil(y, static_cast(mo), + static_cast(d)); + return ((days * 24 + h - 8) * 60 + mi) * kMinute; +} + +struct TapeTrade { + bool is_long = true; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = 0.0, exit_price = 0.0, qty = 0.0; + std::string entry_signal, exit_signal; +}; + +std::vector read_tape(const std::string& slug) { + std::ifstream in(std::string(PINEFORGE_INT26_REENTRY_FIXTURE_DIR) + "/" + slug + + "/tv_trades.csv"); + std::vector trades; + std::string line; + bool header = true; + while (std::getline(in, line)) { + if (header) { header = false; continue; } + std::vector cell; + std::stringstream fields(line); + std::string field; + while (std::getline(fields, field, ',')) cell.push_back(field); + if (cell.size() < 17) continue; + const std::size_t n = static_cast(std::stoi(cell[0])); + if (trades.size() < n) trades.resize(n); + TapeTrade& t = trades[n - 1]; + const bool entry = cell[1].rfind("Entry", 0) == 0; + t.is_long = cell[1].find("long") != std::string::npos; + (entry ? t.entry_ms : t.exit_ms) = tape_ms(cell[2]); + (entry ? t.entry_signal : t.exit_signal) = cell[3]; + (entry ? t.entry_price : t.exit_price) = std::stod(cell[4]); + t.qty = std::stod(cell[5]); + } + return trades; +} + +// floor(x * 100 + 1e-6): the script's cent grid (NYSE:F mintick 0.01). +double cents(double x) { return std::floor(x * 100.0 + 1e-6); } + +struct Variant { + const char* slug; + bool magnifier; + // Closed rows of TradingView's tape the replay books, from the first: all + // of them, or three of four where row 4 is the recorded magnified + // divergence. + std::size_t booked; + bool short_reentry_exit; // the short's re-entry on bar 46 places its own bracket +}; + +// The Pine probe's body (tests/fixtures/pooc_stop_reentry_bracket//strategy.pine). +class ReentryHost final : public source::PineStrategyHost { +public: + explicit ReentryHost(const Variant& v) : v_(v) { + // As the generated strategy does: the retained-parent ordering of the + // cancelled-and-placed-again entry is the attached adapter's. + attach_pine_execution_adapter(); + set_syminfo_session("0930-1600"); + set_syminfo_timezone("America/New_York"); + source::PineStrategyConfig c; + c.initial_capital = 100000; + c.default_qty_type = static_cast(QtyType::FIXED); + c.default_qty_value = 100; + c.pyramiding = 0; + c.process_orders_on_close = true; + c.calc_on_order_fills = false; + c.slippage = 0; + c.commission_type = static_cast(CommissionType::PERCENT); + c.commission_value = 0.01; + c.margin_long = 100; + c.margin_short = 100; + configure_pine_strategy(c); + syminfo_mintick_ = 0.01; + } + void on_source_bar(const Bar& b) override { + const int k = bar_index_; // the replay's first bar is the Pine counter's origin + if (signed_position_size() != 0.0) return; + const double t = cents(b.close); + // Three signal closes each: the stop 10c beyond the close, 1c beyond + // it, then AT it; each cancels the pending entry and places it again. + if (k >= 39 && k <= 41) { + strategy_cancel("L"); + strategy_entry("L", true, kNaN, (k == 39 ? t + 10.0 : k == 40 ? t + 1.0 : t) / 100.0); + strategy_exit("LX", "L", (t + 3.0) / 100.0, (t - 10.0) / 100.0); + } + if (k >= 44 && k <= 46) { + strategy_cancel("S"); + strategy_entry("S", false, kNaN, (k == 44 ? t - 10.0 : k == 45 ? t - 1.0 : t) / 100.0); + if (v_.short_reentry_exit || k < 46) + strategy_exit("SX", "S", (t - 3.0) / 100.0, (t + 10.0) / 100.0); + } + } + // The bracket legs still working: TradingView's filled strategy.exit + // takes its other leg with it, and a bracket that never filled is the + // position's own. + int working_exit_legs() const { + int legs = 0; + for (const auto& row : native_working_requests()) { + const auto& label = row.definition->request.label; + if (label == "LX" || label == "SX") ++legs; + } + return legs; + } + + struct Row { + bool is_long = true; + std::int64_t entry_ms = 0, exit_ms = 0; + double entry_price = 0.0, exit_price = 0.0, qty = 0.0; + int entry_bar = 0, exit_bar = 0; + std::string entry_id, exit_id; + }; + std::vector rows() const { + std::vector out; + for (int i = 0; i < trade_count(); ++i) { + Row r; + r.is_long = trades_[static_cast(i)].is_long; + r.entry_ms = closed_trade_entry_time(i); + r.exit_ms = closed_trade_exit_time(i); + r.entry_price = closed_trade_entry_price(i); + r.exit_price = closed_trade_exit_price(i); + r.qty = closed_trade_size(i); + r.entry_bar = closed_trade_entry_bar_index(i); + r.exit_bar = closed_trade_exit_bar_index(i); + r.entry_id = closed_trade_entry_id(i); + r.exit_id = closed_trade_exit_id(i); + out.push_back(r); + } + return out; + } + double open_units() const { return physical_position().signed_units; } + +private: + Variant v_; +}; + +template +std::vector feed(const FeedBar (&rows)[N]) { + std::vector bars; + for (const FeedBar& row : rows) { + Bar b{}; + b.timestamp = row.ts; + b.open = row.open; b.high = row.high; b.low = row.low; b.close = row.close; + b.volume = row.volume; + bars.push_back(b); + } + return bars; +} + +// The chart bar (fixture order) whose open is `ms`; -1 when none is. +int chart_bar_of(std::int64_t ms) { + for (std::size_t i = 0; i < sizeof(kFord15) / sizeof(kFord15[0]); ++i) + if (kFord15[i].ts == ms) return static_cast(i); + return -1; +} + +bool near(double a, double b) { return std::fabs(a - b) <= 1e-9; } + +void replay(const Variant& v) { + std::printf("-- %s [%s]\n", v.slug, v.magnifier ? "aggregated 1 -> 15, magnifier" + : "chart 15 -> 15"); + static const std::vector fifteen = feed(kFord15); + static const std::vector one = feed(kFord1m); + ReentryHost host(v); + if (v.magnifier) { + host.run(one.data(), static_cast(one.size()), "1", "15", true); + } else { + host.run(fifteen.data(), static_cast(fifteen.size()), "15", "15", false); + } + CHECK(host.last_error().empty()); + std::vector closed; + const TapeTrade* open_at_end = nullptr; + const auto tape = read_tape(v.slug); + for (const TapeTrade& t : tape) { + if (t.exit_signal.empty()) open_at_end = &t; + else closed.push_back(t); + } + // Four rows, the control's last one still open when the data ends. + CHECK(closed.size() == (v.short_reentry_exit ? 4u : 3u)); + CHECK((open_at_end != nullptr) == !v.short_reentry_exit); + const auto rows = host.rows(); + CHECK(rows.size() == v.booked); + int differing = 0; + for (std::size_t i = 0; i < rows.size() && i < closed.size(); ++i) { + const TapeTrade& tv = closed[i]; + const ReentryHost::Row& r = rows[i]; + const bool ok = r.is_long == tv.is_long && r.entry_ms == tv.entry_ms + && r.exit_ms == tv.exit_ms && near(r.entry_price, tv.entry_price) + && near(r.exit_price, tv.exit_price) && r.qty == tv.qty + && r.entry_id == tv.entry_signal && r.exit_id == tv.exit_signal + && r.entry_bar == chart_bar_of(tv.entry_ms) && r.exit_bar == chart_bar_of(tv.exit_ms); + CHECK(ok); + if (!ok) { + ++differing; + std::printf(" row %zu: TradingView %s %s bar %d @%.2f -> bar %d @%.2f %s q%g\n" + " engine %s %s bar %d @%.2f -> bar %d @%.2f %s q%g\n", + i + 1, tv.entry_signal.c_str(), tv.is_long ? "L" : "S", + chart_bar_of(tv.entry_ms), tv.entry_price, chart_bar_of(tv.exit_ms), + tv.exit_price, tv.exit_signal.c_str(), tv.qty, r.entry_id.c_str(), + r.is_long ? "L" : "S", r.entry_bar, r.entry_price, r.exit_bar, + r.exit_price, r.exit_id.c_str(), r.qty); + } + } + // TradingView's open trade at the end of the data (its exit signal + // empty) is the engine's open position: same side and quantity. The + // bracket tapes end flat, the recorded magnified divergence included (its + // re-entry is never placed); the control ends short 100, the re-entry + // with no bracket. + const double tv_open = open_at_end + ? (open_at_end->is_long ? open_at_end->qty : -open_at_end->qty) : 0.0; + CHECK(host.open_units() == tv_open); + // No bracket leg is left working: a filled strategy.exit took its other + // leg with it however late its receipt was read -- in the control, the + // stop of the short's bracket, which the drain placed after its limit had + // filled. + CHECK(host.working_exit_legs() == 0); + std::printf(" %zu closed rows (TradingView %zu), %d differ; open at the end %g" + " (TradingView %g); exit legs working %d\n", rows.size(), closed.size(), + differing, host.open_units(), tv_open, host.working_exit_legs()); +} + +} // namespace + +int main() { + const Variant variants[] = { + {"int26-reentry-bracket-chart", false, 4, true}, + {"int26-reentry-bracket-mag", true, 3, true}, + {"int26-reentry-noexit-chart", false, 3, false}, + }; + for (const Variant& v : variants) replay(v); + std::printf("\n%s process_orders_on_close stop re-entry bracket tapes: %d checks, %d failures\n", + tests_failed == 0 ? "PASS" : "FAIL", tests_passed + tests_failed, tests_failed); + return tests_failed == 0 ? 0 : 1; +}